前言
本文是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:Objectchildren:Array(1)parent:undefinedplain:falsestatic:falsestaticRoot:falsetag:"div"type:1__proto__:Object//其中的children[0],即child的AST如下:attrs:Array(1) //这里存放了name:'message'attrsList:Array(1)attrsMap:Objectchildren:Array(0)hasBindings:trueparent:Objectplain:falsestatic:falsestaticRoot:falsetag:"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 elementsvnode = new VNode(config.parsePlatformTagName(tag), data, children,undefined, undefined, context);} else if ((Ctor = resolveAsset(context.$options, 'components', tag))) {//这里取到了child的构造函数// 创建child的vnodevnode = 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 childrenvnode = new VNode(tag, data, children,undefined, undefined, context);}} else {// direct component options / constructorvnode = 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) {...// 异步组件处理...// 处理optionresolveConstructorOptions(Ctor);data = data || {};// 将组件v-model的data转成props & events...// 提取propsvar propsData = extractProps(data, Ctor, tag);// 函数组件...// 提取事件监听var listeners = data.on;// 用.native modifier替换data.on = data.nativeOn;// 合并组件管理钩子到placeholder vnodemergeHooks(data);// 返回placeholder vnodevar 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:undefinedcomponentInstance:undefinedcomponentOptions:Objectcontext:Vue$3data:Objectelm:undefinedfunctionalContext:undefinedisCloned:falseisComment:falseisOnce:falseisRootInsert:trueisStatic:falsekey:undefinedns:undefinedparent:undefinedraw:falsetag:"vue-component-1-child"text:undefinedchild:(...)__proto__:Object//componentOptions对象如下:Ctor:function VueComponent(options)children:undefinedlisteners:undefinedpropsData: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会返回trueif (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 patchvar mountedNode = vnode; // work around flowcomponentVNodeHooks.prepatch(mountedNode, mountedNode);}}
看下createComponentInstanceForVnode函数
function createComponentInstanceForVnode (vnode, // we know it's MountedComponentVNode but flow doesn'tparent, // activeInstance in lifecycle stateparentElm,refElm) {// vnode.componentOptions如下:// Ctor:function VueComponent(options)// children:undefined// listeners:undefined// propsData:Object// tag:"child"// __proto__:Objectvar 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 functionsvar 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/propsinitState(vm);initProvide(vm); // resolve provide after data/propscallHook(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是在initRender函数中初始化的
