模板生成虚拟 VNode 的过程
上文说到在实例化渲染 watcher 时会调用 vm._render() 函数生成虚拟 VNode,然后调用 vm._update() 函数更新 DOM,接下来就分析一下 VNode 的生成过程,下一节再说一下 VNode 生成真实 DOM 的过程,部分代码就暂时不上图了!
vm._render() => VNode 流程:
执行 vm._render() 函数
1、vm._render() 函数就是 Vue.prototype._render() 函数定义在 Vue 的原型对象上。
2、在该函数内首先从 vm.$options 上获取 render()、_parentVnode 属性,而 _parentVnode 表示当前实例的父实例对应的 VNode 对象,然后又赋值 vm.$vnode = _parentVnode,则 vm.$vnode 也表示当前实例的父实例对应的 VNode 对象。
3、接着执行 vnode = render.call(vm, vm.$createElement) 函数并返回当前实例所对应的 VNode 对象。然后又进行校验 vnode instanceof VNode 是否成立,若不成立则变量 vnode 被赋值为一个空 VNode 对象。
4、最后执行 vnode.parent = _parentVnode; 建立父子 vnode 关系树,最后返回变量 vnode。
重点分析一下 vm.$createElement() 函数
1、上文提到在执行 vnode = render.call(vm, vm.$createElement) 时传入参数 vm.$createElement() 函数,而我们发现在当前文件的 initRender() 函数内定义 vm.$createElement()/vm._c() 两个函数,并且两个函数都调用了 createElement() 函数,不同的是传参不一样,说明如下:
vm._c()函数是为给模板被编译生成的render()函数所调用,定义如下:
vm._c = function(a,b,c,d) { createElement(vm,a,b,c,d,false) }
vm.$createElemen()是为用户手写 render() 函数所调用,定义如下
vm._$createElemen = function(a,b,c,d) { createElement(vm,a,b,c,d,true) }
- 按照官方文档来看,
createElement()函数只需要传入三个参数(第一个参数必填String/Objetc/Function类型,第二个为Objetc类型,第三个为String/Array类型,后两个参数可选),而源码中这个函数却定义了四个参数(a,b,c,d),从后续代码来看,第四个参数是为处理前三个参数而保留的,使createElement(a,b,c,d)函数更加灵活。
2、以下就是用户手写 render() 与框架模板编译生成 render() 函数的区别。通过多次调用 createElement() 函数生成对应的 VNode 对象。
3、回归上文,接着执行 createElement(vm,a,b,c,d,flag) 函数。首先进行判断若 b 为数组类型或原始数据类型,则将四个参数的值后移一位(d=c,c=b,b=undefined),这样设计确保 c 为数组类型或原始数据类型。然后再判断若为用户手写 render() 函数则将 d 赋值为 2,最后调用 _createElement(vm,a,b,c,d) 函数。
4、接着执行 _createElement(vm,tag,data,children,nType) 函数,然后进行规则校验。如下:
- 如果
data && data.__ob__都存在且不为null || undefined,则直接return一个空VNode对象。不能用响应式的data对象作为VNode数据。 - 若
data && data.is都存在且不为null || undefined,则设置tag = data.is;,而此处data.is是组件的属性。若tag不存在则直接return一个空VNode对象。
5、创建 VNode,对 tag 类型做判断, 如下:
- 若为
String类型且为原生Html标签,则直接创建对象vnode = new VNode()。 - 若为
String类型且为已经注册的组件名,则通过vnode = createComponet()函数创建组件VNode对象。 - 若为
String类型且不符合上述两条则直接创建一个未知标签的VNode对象。 - 若为非
String类型,则直接通过vnode = createComponet()函数创建组件VNode对象。
又上图 2 可知,创建 vnode 的同时,由于是多次递归调用 createElement() => _createElement() 函数,所以当前生成的 vnode 就会当做参数自动传入 createElement() 函数内作为生成下一个 vnode 的子节点,所以就绑定 children vnode 属性,就这样形成 vnode tree。递归创建 vnode,先创建子 vnode,再创建父 vnode 节点。最后返回 vnode 树.