前言
本文是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是在initRender函数中初始化的