Vue 初始化之 initRender
_init 函数执行完 initEvents 之后,就开始执行 initRender 函数了,这篇文章我们就来看一下 src/core/instance/render.ts 里的 initRender 函数
export function initRender(vm: Component) {
// 为 vm 实例添加 _vnode 和 _staticTrees 属性
vm._vnode = null; // 子组件树的 root,是组件的vnode对象
vm._staticTrees = null; // 不会变化的组件树
// 获取父组件 和 执行上下午
const options = vm.$options;
// 添加 $vnode
const parentVnode = (vm.$vnode = options._parentVnode!); // $vnode 是子组件在父节点的占位时的节点
// 父组件的vm实例
const renderContext = parentVnode && (parentVnode.context as Component);
// 添加 slots 储存着父组件传来的slot
vm.$slots = resolveSlots(options._renderChildren, renderContext);
// 添加 scopedSlots
vm.$scopedSlots = parentVnode
? normalizeScopedSlots(vm.$parent!, parentVnode.data!.scopedSlots, vm.$slots)
: emptyObject;
// 将createElement fn绑定到该实例
// 以便我们在里面得到适当的渲染上下文。
// args order: tag, data, children, normalizationType, alwaysNormalize
// 从模板编译的渲染函数使用内部版本
// @ts-expect-error
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false);
// 规范化总是应用于公共版本,用于
// 用户编写的渲染函数。
// @ts-expect-error
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true);
// $attrs & $listeners 是为了更好的创建高阶组件.
// 他们应该是响应式的,所以高阶组件可以一直更新
const parentData = parentVnode && parentVnode.data;
/* istanbul ignore else */
if (__DEV__) {
// 开发环境给提示
defineReactive(
vm,
'$attrs',
(parentData && parentData.attrs) || emptyObject,
() => {
!isUpdatingChildComponent && warn(`$attrs is readonly.`, vm);
},
true,
);
defineReactive(
vm,
'$listeners',
options._parentListeners || emptyObject,
() => {
!isUpdatingChildComponent && warn(`$listeners is readonly.`, vm);
},
true,
);
} else {
defineReactive(vm, '$attrs', (parentData && parentData.attrs) || emptyObject, null, true);
defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true);
}
}
// 为 vm 实例添加 _vnode 和 _staticTrees 属性
vm._vnode = null; // 子组件树的 root,是组件的vnode对象
vm._staticTrees = null; // 不会变化的组件树
一上来,先给 Vue 的实例添加两个属性:_vnode 和 _staticTrees。这两个属性一个是子组件树的根节点,一个是静态渲染的组件树。
// 获取父组件 和 执行上下午
const options = vm.$options;
// 添加 $vnode
const parentVnode = (vm.$vnode = options._parentVnode!); // $vnode 是子组件在父节点的占位时的节点
// 父组件的vnode
const renderContext = parentVnode && (parentVnode.context as Component);
// 添加 slots 储存着父组件传来的slot
vm.$slots = resolveSlots(options._renderChildren, renderContext);
// 添加 scopedSlots
vm.$scopedSlots = parentVnode
? normalizeScopedSlots(vm.$parent!, parentVnode.data!.scopedSlots, vm.$slots)
: emptyObject;
然后给父组件添加$vnode,获取父组件的vm实例。
$vnode 和 _vnode 这两个属性都是vnode对象,$vnode 是子组件在父组件占位时的节点,_vnode 是组件的vnode对象。两个属性的tag不一样,_vnode的tag时template 下的第一个标签,而$vnode的tag 则是外层写的<child>
这样的。
$vnode在初始化的时候通过$options._parentVnode传入,并且自身组件挂载前(mounted前)存在,父组件挂载前(mounted前)不存在;而_vnode在自身组件挂载前(mounted前)不存在。
parentVnode.context,这个变量其实就是父组件的vnode,而它也等于子组件的 $parent,
$slots 就是用来存储父组件的传来的slot,我们来看看resolveSlots。
export function resolveSlots(
children: Array<VNode> | null | undefined,
context: Component | null
): { [key: string]: Array<VNode> } {
if (!children || !children.length) {
return {}
}
const slots: Record<string, any> = {}
// 循环传入的slot,根据带有或不带slot属性,区分成对应的slot储存
for (let i = 0, l = children.length; i < l; i++) {
const child = children[i]
const data = child.data
// 如果节点解析为Vue的slot节点,则删除slot属性
if (data && data.attrs && data.attrs.slot) {
delete data.attrs.slot
}
// child.context === context || child.functionalContext === context: 判断是否来自同一个域
// data 标签是否有属性值 <div class="aaa" slot="header"></div>
if (
(child.context === context || child.fnContext === context) &&
data &&
data.slot != null
) {
const name = data.slot
// slots.传入的slot名称
const slot = slots[name] || (slots[name] = [])
// 忽略template标签
if (child.tag === 'template') {
slot.push.apply(slot, child.children || [])
} else {
slot.push(child)
}
} else {
// 没有当做默认slot defaultSlot
;(slots.default || (slots.default = [])).push(child)
}
}
// 忽略只包含空格的slot
for (const name in slots) {
if (slots[name].every(isWhitespace)) {
delete slots[name]
}
}
return slots
}
function isWhitespace(node: VNode): boolean {
return (node.isComment && !node.asyncFactory) || node.text === ' '
}
resolveSlots方法,主要是找到当前组件下在父组件传入的slot,并且返回;
children是一个vnode列表,子组件下一级所有的vnode. context是传入组件的父组件实例,用于确定的slot的时候对比当前的上下文 函数的作用是循环传入的vnode,将有slot属性的值放到对应的slot,没有的放到default。然后删除只包含空格的slot。
$scopedSlots 则是判断一下有没有父组件,然后规范化一下$slots,没有父组件就是一个Object.freeze({})
// 将createElement fn绑定到该实例
// 以便我们在里面得到适当的渲染上下文。
// args order: tag, data, children, normalizationType, alwaysNormalize
// 从模板编译的渲染函数使用内部版本
// @ts-expect-error
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false);
// 规范化总是应用于公共版本,用于
// 用户编写的渲染函数。
// @ts-expect-error
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true);
这部分是把_c和$createElement,放到Vue实例上,这俩都是生成vnode的方法,_c 用于从模板<template>
编译得到的组件中使用,通常是vue内部使用生成vnode,而$createElement是放给用户用作render渲染写法。这部分等render的时候,在来看一下。
// $attrs & $listeners 是为了更好的创建高阶组件.
// 他们应该是响应式的,所以高阶组件可以一直更新
const parentData = parentVnode && parentVnode.data;
/* istanbul ignore else */
if (__DEV__) {
// 开发环境给提示
defineReactive(
vm,
'$attrs',
(parentData && parentData.attrs) || emptyObject,
() => {
!isUpdatingChildComponent && warn(`$attrs is readonly.`, vm);
},
true,
);
defineReactive(
vm,
'$listeners',
options._parentListeners || emptyObject,
() => {
!isUpdatingChildComponent && warn(`$listeners is readonly.`, vm);
},
true,
);
} else {
defineReactive(vm, '$attrs', (parentData && parentData.attrs) || emptyObject, null, true);
defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true);
}
上面的代码主要作用就是在 Vue 实例对象上定义两个属性:vm.$attrs 以及 vm.$listeners。这两个属性的存在使得在 Vue 中创建高阶组件变得更容易。
- $attrs:父组件可以无限级向子组件传递不作为 prop 被识别的属性。当父组件想给孙子组件,孙孙孙子组件传值时不用一层一层的去定义props
- $listeners:可以从孙孙子组件直接传递方法给父组件
# 总结
所以initRender主要做的就是初始化插槽信息$slots以及初始化$createElement方法,使用defineReactive方法让$attrs、$listeners响应式。