前言
本文是vue2.x源碼分析的第十一篇,主要看component,props,slot的處理過程!
實例代碼
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Vue</title>
<script src="./vue11.js" type="text/javascript" charset="utf-8" ></script>
</head>
<body>
<div id="app">
<child :name='message'>
<!-- <span>span from parent</span> -->
</child>
</div>
<script>
debugger;
var child=Vue.component('child',{
template:'<div>{{name}}<slot></slot></div>',
props:['name']
})
var vm=new Vue({
el:'#app',
name:'app',
data:{
message:'message from parent'
},
});
</script>
</body>
</html>
1、三個全局API(Vue.component、Vue.directive、Vue.filter)
initAssetRegisters函數在initGlobalAPI時被調用
function initAssetRegisters (Vue) {
//config._assetTypes=['component','directive','filter']
config._assetTypes.forEach(function (type) {
Vue[type] = function (id,definition) {
if (!definition) {
return this.options[type + 's'][id]
} else {
{
if (type === 'component' && config.isReservedTag(id)) {
warn(
'Do not use built-in or reserved HTML elements as component ' +
'id: ' + id
);
}
}
if (type === 'component' && isPlainObject(definition)) {
definition.name = definition.name || id;
definition = this.options._base.extend(definition);
}
if (type === 'directive' && typeof definition === 'function') {
definition = { bind: definition, update: definition };
}
this.options[type + 's'][id] = definition;
return definition
}
};
});
}
首先要知道,當vue庫文件加載完后,vue的初始化中已有這個東西:
Vue.options={
components:{
KeepAlive:Object,
Transition:Object,
TransitionGroup:Object
},
directives:{
show:Object,
model:Object
},
filter:{},
_base:function Vue$3(options){...}
}
這些都是vue庫內置的組件和指令,當執行Vue.component、Vue.directive、Vue.filter時就是在對這些內置組件、指令、過濾器進行擴充,所以:
var child=Vue.component('child',{
template:'<div>child</div>',
props:['name']
})
執行完后
Vue.options.components={
KeepAlive:Object,
Transition:Object,
TransitionGroup:Object,
child:function VueComponent(options)
}
child的配置項存放在VueComponent.options中,在該函數中還對props,computed進行了處理:
//對child的props的處理,key='name'
proxy(VueComponent.prototype, "_props", key);
//proxy函數如下:
function proxy (target, sourceKey, key) {
sharedPropertyDefinition.get = function proxyGetter () {
return this[sourceKey][key]
};
sharedPropertyDefinition.set = function proxySetter (val) {
this[sourceKey][key] = val;
};
//在VueComponent.prototype定義存取器屬性'name'
Object.defineProperty(target, key, sharedPropertyDefinition);
}
順便提一句,當定義全局指令時
Vue.directive('v-focus',function(){...})
//會將定義的函數當作bind和update函數,運行完是這樣:
Vue.options.directives={
show:Object,
model:Object,
v-focus:{
bind:function(){...},
update:function(){...},
}
}
2、詳細分析
vue的渲染過程可分為四步:
當child=Vue.component('child',...)執行完后,開始new Vue的過程,經過一系列的處理,得到根節點的AST結構:
attrs:Array(1) //這里存放了id:'app'
attrsList:Array(1)
attrsMap:Object
children:Array(1)
parent:undefined
plain:false
static:false
staticRoot:false
tag:"div"
type:1
__proto__:Object
//其中的children[0],即child的AST如下:
attrs:Array(1) //這里存放了name:'message'
attrsList:Array(1)
attrsMap:Object
children:Array(0)
hasBindings:true
parent:Object
plain:false
static:false
staticRoot:false
tag:"child"
type:1 //得到AST時,vue將child看成普通html標簽,未做特殊處理
__proto__:Object
此時得到的render函數如下:
with(this){
return _c(
'div',
{attrs:{"id":"app"}},
[_c(
'child',
{attrs:{"name":message}}
)
],
1)
}
render函數執行時,message取到了'message from parent',下面主要看_c函數的處理:
vm._c = function (a, b, c, d) { return createElement(vm, a, b, c, d, false); };
//createElement函數如下:
function createElement (context,tag,data,children,normalizationType,alwaysNormalize) {
if (Array.isArray(data) || isPrimitive(data)) {
normalizationType = children;
children = data;
data = undefined;
}
if (alwaysNormalize) { normalizationType = ALWAYS_NORMALIZE; }
return _createElement(context, tag, data, children, normalizationType)
}
function _createElement (context,tag,data,children,normalizationType) {
...
// support single function children as default scoped slot
//
... 暫時略過slot的處理
//
var vnode, ns;
if (typeof tag === 'string') {
var Ctor;
ns = config.getTagNamespace(tag);
if (config.isReservedTag(tag)) {
// platform built-in elements
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
);
} else if ((Ctor = resolveAsset(context.$options, 'components', tag))) {
//這里取到了child的構造函數
// 創建child的vnode
vnode = createComponent(Ctor, data, context, children, tag);
} else {
// unknown or unlisted namespaced elements
// check at runtime because it may get assigned a namespace when its
// parent normalizes children
vnode = new VNode(
tag, data, children,
undefined, undefined, context
);
}
} else {
// direct component options / constructor
vnode = createComponent(tag, data, context, children);
}
if (vnode) {
if (ns) { applyNS(vnode, ns); }
return vnode
} else {
return createEmptyVNode()
}
}
看下createComponent函數
function createComponent (Ctor,data,context,children,tag) {
...
// 異步組件處理
...
// 處理option
resolveConstructorOptions(Ctor);
data = data || {};
// 將組件v-model的data轉成props & events
...
// 提取props
var propsData = extractProps(data, Ctor, tag);
// 函數組件
...
// 提取事件監聽
var listeners = data.on;
// 用.native modifier替換
data.on = data.nativeOn;
// 合并組件管理鉤子到placeholder vnode
mergeHooks(data);
// 返回placeholder vnode
var name = Ctor.options.name || tag;
var vnode = new VNode(
("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : '')),
data, undefined, undefined, undefined, context,
{ Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children }
);
return vnode
}
最終child的Vnode結構如下:
children:undefined
componentInstance:undefined
componentOptions:Object
context:Vue$3
data:Object
elm:undefined
functionalContext:undefined
isCloned:false
isComment:false
isOnce:false
isRootInsert:true
isStatic:false
key:undefined
ns:undefined
parent:undefined
raw:false
tag:"vue-component-1-child"
text:undefined
child:(...)
__proto__:Object
//componentOptions對象如下:
Ctor:function VueComponent(options)
children:undefined
listeners:undefined
propsData:Object //包含name:'message from parent'
tag:"child"
__proto__:Object
組件的Vnode和普通html標簽的Vnode的主要差異在componentOptions
在得到child的Vnode后,再得到根節點的Vnode,然后根節點的render函數執行完畢,返回根節點的Vnode(children里存放了child的Vnode)。然后執行vm._update函數,該函數調用vm.__patch__,patch又調用createElm函數:
function createElm (vnode, insertedVnodeQueue, parentElm, refElm, nested) {
vnode.isRootInsert = !nested; // for transition enter check
//注意createComponent函數,對于普通html標簽對應的vnode會返回false,但對于組件的vnode會返回true
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
var data = vnode.data;
var children = vnode.children;
var tag = vnode.tag;
if (isDef(tag)) {
...
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode);
setScope(vnode);
/* istanbul ignore if */
{
createChildren(vnode, children, insertedVnodeQueue);
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue);
}
insert(parentElm, vnode.elm, refElm);
}
...
}
...
}
首先是處理根節點的Vnode,當執行到createChildren(vnode, children, insertedVnodeQueue)時,開始處理child的Vnode,createChildren是直接調用createElm函數的,所以createElm函數再次執行,不同的是這次處理child的Vnode,來看下createComponent函數:
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
var i = vnode.data; //
if (isDef(i)) {
var isReactivated = isDef(vnode.componentInstance) && i.keepAlive;
if (isDef(i = i.hook) && isDef(i = i.init)) {
//這里開始執行vnode.data.hook.init函數
i(vnode, false /* hydrating */, parentElm, refElm);
}
// after calling the init hook, if the vnode is a child component
// it should've created a child instance and mounted it. the child
// component also has set the placeholder vnode's elm.
// in that case we can just return the element and be done.
if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue);
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);
}
return true
}
}
}
看下vnode.data.hook.init函數
init: function init (vnode,hydrating,parentElm,refElm) {
if (!vnode.componentInstance || vnode.componentInstance._isDestroyed) {
//這里開始創建vnode的組件實例
var child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance,
parentElm,
refElm
);
child.$mount(hydrating ? vnode.elm : undefined, hydrating);
} else if (vnode.data.keepAlive) {
// kept-alive components, treat as a patch
var mountedNode = vnode; // work around flow
componentVNodeHooks.prepatch(mountedNode, mountedNode);
}
}
看下createComponentInstanceForVnode函數
function createComponentInstanceForVnode (
vnode, // we know it's MountedComponentVNode but flow doesn't
parent, // activeInstance in lifecycle state
parentElm,
refElm
) {
// vnode.componentOptions如下:
// Ctor:function VueComponent(options)
// children:undefined
// listeners:undefined
// propsData:Object
// tag:"child"
// __proto__:Object
var vnodeComponentOptions = vnode.componentOptions;
var options = {
_isComponent: true,
parent: parent,
propsData: vnodeComponentOptions.propsData,
_componentTag: vnodeComponentOptions.tag,
_parentVnode: vnode,
_parentListeners: vnodeComponentOptions.listeners,
_renderChildren: vnodeComponentOptions.children,
_parentElm: parentElm || null,
_refElm: refElm || null
};
// check inline-template render functions
var inlineTemplate = vnode.data.inlineTemplate;
if (inlineTemplate) {
options.render = inlineTemplate.render;
options.staticRenderFns = inlineTemplate.staticRenderFns;
}
//這里的Ctor就是 function VueComponent(options){
// this._init(options);
// }
return new vnodeComponentOptions.Ctor(options)
}
接著就進入了和new Vue()一樣的處理過程了
...
initLifecycle(vm);
initEvents(vm);
initRender(vm);
callHook(vm, 'beforeCreate');
initInjections(vm); // resolve injections before data/props
initState(vm);
initProvide(vm); // resolve provide after data/props
callHook(vm, 'created');
//這里沒有vm.$options.el還未生成,故不執行
if (vm.$options.el) {
vm.$mount(vm.$options.el);
}
...
此時createComponentInstanceForVnode函數執行完畢,接著執行vnode.data.hook.init中的 child.$mount(hydrating ? vnode.elm : undefined, hydrating),然后又開始了這個過程:
這個過程結束后vnode.data.hook.init函數執行完畢,接著執行createComponent函數中的 initComponent(vnode, insertedVnodeQueue)函數,然后createComponent函數執行完畢,從而createChildren執行完,然后
執行invokeCreateHooks(vnode, insertedVnodeQueue)和insert(parentElm, vnode.elm, refElm),頁面就渲染完畢了。
3、總結component的處理過程
- 解析模板得到根節點的AST(根節點的AST的children包含了組件節點的AST);
- 由根節點的AST和組件節點的AST得到render函數;
- render函數在執行時,_c函數對普通html標簽和組件標簽的處理不一樣,不過最終都返回了Vnode結構;
- 執行createElm函數,該函數處理兩種情況:組件Vnode和普通Vnode
- 對于組件Vnode,會再次經歷模板解析->AST->render->Vnode->createElm->真實DOM
4、props和slot的原理
從component的處理過程不難發現props和slot的原理
4.1 props
- Vue.component(child,{props:['name']})執行中執行了initProps$1(Sub),該函數執行了proxy(VueComponent.prototype, "_props", 'name'),即把name定義成VueComponent.prototype的存取器屬性,其中get函數是這樣的:
sharedPropertyDefinition.get = function proxyGetter () {
return this[sourceKey][key]
};
注:這里尚未實現this._props
- 組件Vnode在執行this._init過程中執行了initState,該函數又執行了initProps,這個函數實現了將'name'作為this._props的存取器屬性,于是在子組件中vm.name相當于vm._props.name
4.2 slot
- 根節點AST
- 根節點render
- 根節點Vnode的children是子節點的Vnode,子節點Vnode的children應該是span節點,但實際上并不是,做了特殊處理,將span的Vnode放入了子節點Vnode.componentOptions.children中
- 子節點模板的AST
- 子節點render又做了特殊處理,slot應該是_v('slot',...),但實際被處理成_t('default')
with(this){
return _c('div',[_v(_s(name)),_t("default")],2)
}
- 子節點模板Vnode的children中已經有兩個Vnode,_t('default')被轉成了
從this.$slots['default']中取的Vnode,該Vnode即是span的Vnode,this.$slots是在initRender函數中初始化的