前面说到模板编译完会生成一个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 生成。