注:此章只讨论初始化过程,对render具体渲染不做讨论,在后面会有文章解析render过程。
/*初始化render*/
export function initRender(vm: Component) {
// _vnode 组件的真实节点,它的tag就是标签下的第一个节点
vm._vnode = null; // the root of the child tree
vm._staticTrees = null;
// $vnode _parentVnode: 父树中的占位符节点,它的tag就是在父组件中的组件名 $vnode可以说是_vnode的父级
const parentVnode = (vm.$vnode = vm.$options._parentVnode); // the placeholder node in parent tree 父树中的占位符节点
// 父组件的vm实例
const renderContext = parentVnode && parentVnode.context;
// 储存着父组件传来的slot
vm.$slots = resolveSlots(vm.$options._renderChildren, renderContext);
// 这里调用emptyObject Object.freeze({}) 冻结只是当做一个空值,空对象与我们平常的null差不多
vm.$scopedSlots = emptyObject;
// bind the createElement fn to this instance
// so that we get proper render context inside it.
// args order: tag, data, children, normalizationType, alwaysNormalize
// internal version is used by render functions compiled from templates
/*将createElement函数绑定到该实例上,该vm存在闭包中,不可修改,vm实例则固定。这样我们就可以得到正确的上下文渲染*/
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false);
// normalization is always applied for the public version, used in
// user-written render functions.
/*常规方法被用于公共版本,被用来作为用户界面的渲染方法*/
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true);
}
initRender函数里的代码不多,寥寥几行,但里面要理清楚还是有很多东西要理解。
初看时,这两个命名只差一个符号,所以很容易将读者搞混,我们可以输出一个例子来帮助我们弄懂这两个变量。
这里需要注意的是_vnode变量在created的时候是为null,只有去到mounted即挂载组件后才有值。
我们可以先看下这两个对象打印出来的结果
// app
<div id="app">
<child></child>
</div>
// child
<div class="child-wrap">child组件</div>
export default {
data() {
return {
}
},
mounted() {
console.log('child组件_vnode', this._vnode);
console.log('child组件$vnode', this.$vnode);
}
}
编译前: , 编译后:
,在初始化的时候通过$options._parentVnode传入,并且自身组件挂载前(mounted前)存在,父组件挂载前(mounted前)不存在,$vnode的指向是从组件在父节点的占位时的节点。const parentVnode = (vm.$vnode = vm.$options._parentVnode);
const renderContext = parentVnode && parentVnode.context;
这一段代码是找到组件的slot传入,做保存。
vm.$vnode = vm.$options._parentVnode,$vnode在上面讲过,所以不做二次解释了。
parentVnode.context,先说结论,这个变量其实就是父组件的vnode,而它也等于子组件的 $parent,直接放图。
// child组件
console.log(this.$options._parentVnode.context === this.$parent);
console.log(this.$options._parentVnode.context);
resolveSlots主要是找出当前组件下在父组件传入的slot,并且返回;
children是一个vnode列表,子组件下一级所有的vnode.
context是传入组件的父组件实例,用于确定的slot的时候对比当前的上下文
函数的作用是循环传入的vnode,将有slot属性的值放到对应的slot,没有的放到default
这里有一个注意点:当标签组有空格也会被当成一个文本vnode,所以这里做了处理,当只有一个空格的节点时,直接忽略
//rerdner.js
vm.$slots = resolveSlots(vm.$options._renderChildren, renderContext);
// resolve-slots.js
/**
* Runtime helper for resolving raw children VNodes into a slot object.
*/
export function resolveSlots(children: ?Array<VNode>, context: ?Component): { [key: string]: Array<VNode> } {
const slots = {};
if (!children) {
return slots;
}
const defaultSlot = [];
// 循环传入的slot,根据带有或不带slot属性,区分成对应的slot储存
for (let i = 0, l = children.length; i < l; i++) {
const child = children[i];
// named slots should only be respected if the vnode was rendered in the
// same context.
// child.context === context || child.functionalContext === context: 判断是否来自同一个域
// child.data 标签是否有属性值
if ((child.context === context || child.functionalContext === context) && child.data && child.data.slot != null) {
const name = child.data.slot;
// slots.传入的slot名称
const slot = slots[name] || (slots[name] = []);
if (child.tag === "template") {
// 忽略template标签
slot.push.apply(slot, child.children);
} else {
slot.push(child);
}
} else {
// 没有当做默认slot defaultSlot
defaultSlot.push(child);
}
}
// 不是一个注释或者只有一个空格的文本节点
if (!defaultSlot.every(isWhitespace)) {
slots.default = defaultSlot;
}
return slots;
}
// 是否为注释或者只有一个空格的文本节点
function isWhitespace(node: VNode): boolean {
return node.isComment || node.text === " ";
}
最终生成实例的$slot如下
<child>
<div class="slot-defalt-1">aaaaa</div>
<div class="slot-defalt-2">bbbbb</div>
<div slot="ccc">cccccc</div>
</child>
ps: 这里对createElement里面函数生成原理不做讨论,因为在这个阶段只是赋值,而非运行renderVnode,后面有专门讲这里。
// a: 标签
// b: 标签属性
// c: children
// d: normalizationType(是否需要额外兜底处理)
vm._c = (a, b, c,d) => createElement(vm, a, b, c, d, false);
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true);
最后两行代码,在这里赋值创建了_c以及$createElement函数,多将当前vm绑定到第一个参数,唯一的区别只有最后一个参数alwaysNormalize不同。
它们都是生成vnode的方法,_c 用于从模板编译得到的组件中使用,通常是vue内部使用生成vnode,而$createElement是放给用户用作render渲染写法。
而$createElement作为公共函数,具有一定开放性,下面有几个例子,用户可能会传入不同的children,比如文本,数字,数组,单一子节点,所以需要去处理不同的情况。
// vue 的render写法
render: function (createElement) {
return createElement(
'h1', // tag name 标签名称
{},
'这是显示文本'
)
},
// 或者文本是第二个参数
render: function (createElement) {
return createElement(
'h1', // tag name 标签名称
'这是显示文本'
)
},
// 子节点
render: function (createElement) {
return createElement(
'h1', // tag name 标签名称
{},
[createElement('p', 'hi'), createElement('p', '你好')]
)
},
vue官方文档-createElement
到这里initRender基本已经解释完毕,已经将我看源码时的疑问写在上面,如有其他疑问不明白,可留言或私信,我将补充到博客中。