前面说到模板编译完会生成一个render
函数,这篇文章要讲的是如何根据render
函数生成对应的vnode
。入口代码位于src/core/instance/render.js
的Vue.prototype._render
:
1 | vnode = render.call(vm._renderProxy, vm.$createElement); |
一个render
函数的格式在前面也说到过,类似于:
1 | with (this) { |
看到这里调用了vm._c
,而$createElement
是我们自己编写render
函数作为参数传递的。看看$createElement
及_c
的格式:
1 | // bind the createElement fn to this instance |
二者底层都是调用的createElement
这个函数,唯一差别在于最后一个参数alwaysNormalize
的赋值不一样,这个参数表示是否做深层归一化,后面会说。
不知道大家有没有注意到render
函数的细节:它是被with(this)
包围起来的,同时在调用render
时传入了vm._renderProxy
。暂时可以把vm._renderProxy
当做vm
,这样我们render
函数内部所有变量如url
都是在vm
上来查找,这也就是模板上的变量如何与我们组件中的数据如何关联起来的关键!
至此,我们知道生成vnode
的绝大部分逻辑都在这个createElement
里。不过在此之前还是说一下vnode
是个什么。
vnode
它的构造函数位于src/core/vdom/vnode.js
,含有的成员变量非常多,大部分变量已经加了注释。
1 | export default class VNode { |
createElement
位于src/core/vdom/create-element.js
:
1 | const SIMPLE_NORMALIZE = 1; |
所以可以看出来$createElement
对应的normalizationType
值为 2,_c
对应的是 1。这个函数只是针对性的处理了参数传递并没有实质逻辑,干活的是_createElement
:
1 | export function _createElement( |
data
参数就是我们在generate -> genData
中的返回值。 归一化涉及到两个函数normalizeChildren
和simpleNormalizeChildren
,会单独用一篇文章来描述。
后面判断了 tag 的类型,如果是字符串,那么分为 3 种情况:
如果是平台保留标签名,则直接创建 vnode 对象
如果
resolveAsset(context.$options, 'components', tag)
能够拿到值,那么执行createComponent
函数。resolveAsset
其实就是在获取我们的自定义组件选项,同样createComponent
也是在生成我们自定义组件的vnode
。resolveAsset
的逻辑比较简单,获取通过各种方式去尝试获取vm.$options['components'][tag]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22/**
* Resolve an asset.
* This function is used because child instances need access
* to assets defined in its ancestor chain.
*/
export function resolveAsset(options: Object, type: string, id: string, warnMissing?: boolean): any {
if (typeof id !== 'string') {
return;
}
const assets = options[type];
// check local registration variations first
if (hasOwn(assets, id)) return assets[id];
const camelizedId = camelize(id);
if (hasOwn(assets, camelizedId)) return assets[camelizedId];
const PascalCaseId = capitalize(camelizedId);
if (hasOwn(assets, PascalCaseId)) return assets[PascalCaseId];
const res = assets[id] || assets[camelizedId] || assets[PascalCaseId];
if (process.env.NODE_ENV !== 'production' && warnMissing && !res) {
warn('Failed to resolve ' + type.slice(0, -1) + ': ' + id, options);
}
return res;
}如果既不是平台保留标签也不是自定义组件标签,那么也是直接创建
vnode
如果tag
的类型不是字符串,那么也是当做自定义组件来处理。最后返回我们的vnode
。现在我们就是剩下createComponent
这一种情况需要了解。
createComponent 生成自定义组件 vnode
代码位于src/core/vdom/create-component.js
,有点长:
1 | const componentVNodeHooks = { |
大部分代码都打了注释,专门看看一些帮助函数。
extractPropsFromVNodeData
用于解析子组件定义的props
的实际值,这些实际值都是在父组件的template
中放到子组件标签上的。
1 | /** |
我们看到解析props
的值是从子组件标签的props
或attrs
上找,而且优先级是props
>attrs
.并且在checkProps
中可以看到,如果在props
中找到了,还会从props
中删掉它。
另外一个小细节是altKey
是烤串形式书写的,所以这就要求props
和attrs
中的名称也是烤串形式的。
installComponentHooks
用于将 data 上的钩子和默认钩子进行合并,合并后的钩子再放回 data 上。
1 | function installComponentHooks(data: VNodeData) { |
有 4 种默认钩子init
、prepatch
、insert
、destroy
,它们分别会在patch
过程中的vnode
对象初始化、patch
之前、插入到dom
中、vnode
销毁的时候调用。合并后的钩子会再调用时依次执行两个子钩子。
最后createComponent
函数执行完后就会调用VNode
的构造函数,返回的vnode
的tag
格式为vue-component-cid-name
。至此我们的render
生成vnode
流程就讲完了。可以看到花费篇幅最大的还是自定义组件的 vnode 生成。