Vue 初始化之 initRender

2022/11/17 Vue

_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响应式。