浅曦Vue源码-34-挂载阶段-$mount-渲染函数帮助函数(23)

124 阅读5分钟

「这是我参与2022首次更文挑战的第38天,活动详情查看:2022首次更文挑战」。

一、前情回顾 & 背景

上一篇小作文我们给大家展示了根实例的最终得到的 render 函数主体:是一个 _c() 的一个调用,他的第一个参数是标签名,第二个是行内属性,第三个是代表子元素的数组;

从这个 render 函数的主体可以看出,他是一个递归调用的过程,最终完成顶层 div#app 的渲染工作;

获得 render 函数主体之后,又经过 createFunction 得到最终的 render 函数,接着就是一些列的返回结果并出栈,回到了 Vue.prototype.$mount 方法,将获得的 render、renderStaticFn 赋值到 this.$options 上,以备 Vue.prototype._render 调用进行挂载;

经历过上面的步骤我们已经获取到 render 函数渲染函数)代码,其中包含了很多的以 _ 开头的方法,这些方法我们在生成渲染函数的时候有提到,称他们为渲染函数的运行时帮助函数,包括 _l/_t/_s/_i/_c...

编译时将模板编译成 ast,再由 ast 生成渲染函数,而渲染函数就是有多个运行时帮助函数组织起来的调用;所以可以这么说,Vue编译时+运行时辅助的框架;本篇小作文的笔墨放在介绍这些帮助函数的功能和注册时机;

二、renderMixin

renderMixin 是在 Vue 的注册过程中被调用的一个方法,其作用是在 Vue 实例上添加运行时的渲染函数的辅助函数,即将 _t/_l/_i/_s 等属性添加到 Vue.prototype 上;

2.1 renderMixin

方法位置:src/core/instance/render.js -> renderMixin

方法参数:Vue, Vue 构造函数

方法作用:

  1. 执行 installRenderHelper 方法,注册渲染函数的运行时帮助方法;
  2. Vue 的原型对象添加 $nextTick_render 方法,其中 _render 方法会在后面的创建渲染 watcher 时被调用,我们前面得到的 render 函数将会在 Vue.prototype._render 中调用获取 VNode
export function renderMixin (Vue: Class<Component>) {
  // 在 Vue 实例上挂载一些运行时渲染函数的帮助函数
  installRenderHelpers(Vue.prototype)

  Vue.prototype.$nextTick = function (fn: Function) {
    return nextTick(fn, this)
  }

  // 通过执行 render 函数生成 VNode,并加入处理异常逻辑
  Vue.prototype._render = function (): VNode {
    
  }
}

2.2 installRenderHelpers

方法位置:src/core/instance/render-helpers/index.js -> export function installRenderHelpers

方法参数:targetVue.prototype 对象;

方法作用:向 Vue 的原型扩展运行时帮助函数,这些函数有各自不同的能力,最终编译所得的渲染函数能力依靠这些帮助函数实现 Vue 的能力,比如插槽、条件渲染、列表渲染...

export function installRenderHelpers (target: any) {
  // v-once 帮助函数,为 VNode 加上静态标记
  target._o = markOnce

  // 将值转换为数字
  target._n = toNumber

  // 将值转成字符串,
  // 普通值 String(val) ,对象 JSON.stringify(val)
  target._s = toString

  // 列表渲染 v-for 指令的帮助函数
  target._l = renderList

  // 插槽的帮助函数,<slot /> 标签的渲染
  target._t = renderSlot

  // 判断两个值是否相等
  target._q = looseEqual

  // indexOf 方法
  target._i = looseIndexOf

  // 运行时负责生成 VNode 静态树的帮助函数:
  // 1. 执行 staticRenderFns 数组中对应下标的渲染函数,生成静态 VNode 树并缓存
  // 2. 为静态 VNode 树加静态标识
  target._m = renderStatic
  target._f = resolveFilter
  target._k = checkKeyCodes
  target._b = bindObjectProps

  // 创建文本节点 VNode
  target._v = createTextVNode

  // 创建空节点 VNode
  target._e = createEmptyVNode
  target._u = resolveScopedSlots
  target._g = bindObjectListeners
  target._d = bindDynamicKeys
  target._p = prependModifier
}

在前面的 render 函数 中,我们见过其中的很多方法:

  1. _v: createTextVNode
  2. _i: looseIndexOf
  3. _m: renderStatic
  4. _t: renderSlot
  5. _s: toString
  6. _l: renderList

接下来我们就会一一讨论这些方法,不知道你发现没发现,这里面居然没有 render 函数中出场频率最高的 _c 方法,这个 _c 方法的注册是在另一个时机了,后面我们再讨论他;

三、_c & createTextVNode

方法位置:src/core/vdom/vnode.js -> createTextVNode

方法参数:

  1. val,字符串的字面量

方法作用:字符串创建 VNode 实例;

export function createTextVNode (val: string | number) {
  return new VNode(undefined, undefined, undefined, String(val))
}

四、_i & looseIndexOf

方法位置:src/shared/util.js -> functions looseIndexOf

方法参数:

  1. arr,数组
  2. val,某个值

方法作用:判断 val 在数组 arr 中首次出现的位置索引,相当于 Array.prototype.indexOf,如果没有出现过,返回 -1

但是其比较原则是不相同的,如果是对象,则比较的是字面量,具体比较规则见 looseEqual 方法;

export function looseIndexOf (arr: Array<mixed>, val: mixed): number {
  for (let i = 0; i < arr.length; i++) {
    if (looseEqual(arr[i], val)) return i
  }
  return -1
}

五、_m & renderStatic

方法位置:src/core/instance/render-helpers/render-static.js -> function renderStatic

方法参数:

  1. index,静态渲染函数在 staticRenderFns 数组的索引;
  2. isInFor,是否被 v-for 包裹;

方法作用:

  1. 调用 vm.$options.staticRenderFns 指定索引的 render 函数,得到静态根节点对应的渲染树并将树缓存到 cached 对象;
  2. 判断缓存,如果命中 cached 中的缓存,则不再调用静态渲染函数,直接从缓存中返回;
  3. 给前面生成的静态树添加静态标识:isStatic: true
export function renderStatic (
  index: number,
  isInFor: boolean
): VNode | Array<VNode> {
  // 缓存对象,静态节点二次渲染时从缓存中走缓存
  const cached = this._staticTrees || (this._staticTrees = [])
  let tree = cached[index]

  // 如果当前静态树已经被渲染过一次,即命中缓存,且没有被 v-for 节点包裹,则直接返回缓存的 tree
  // if has already-rendered static tree and not inside v-for,
  // we can reuse the same tree.
  if (tree && !isInFor) {
    return tree
  }

  // 执行 staticRenderFns 数组中指定索引对应的 render 函数,
  // 生成该静态树的 VNode,并缓存
  tree = cached[index] = this.$options.staticRenderFns[index].call(
    this._renderProxy,
    null,
    this // for render fns generated for functional component templates
  )

  // 为静态树的 VNode 加标记,即添加 { isStatic: true, key: `__static__${index}`, isOnce: false }
  markStatic(tree, `__static__${index}`, false)
  return tree
}

六、_t & renderSlot

方法位置:src/core/instance/render-helpers/render-slot.js -> function renderSlot

方法参数:

  1. name: slotName,插槽名
  2. fallbackRender: 兜底渲染函数,当不传入插槽时展示兜底内容;
  3. props: slot 标签的 props
  4. bindObject: slot-scope 绑定的对象

方法作用:处理普通 slotslot-scope 的渲染工作;

在我们的例子中只有简答的不带作用域的插槽:

// <slot name="namedSlot"></slot>
"_t(\"namedSlot\")
export function renderSlot (
  name: string,
  fallbackRender: ?((() => Array<VNode>) | Array<VNode>),
  props: ?Object,
  bindObject: ?Object
): ?Array<VNode> {
  // 优先从 scopedSlots 取值,scopedSlots 是作用域插槽
  const scopedSlotFn = this.$scopedSlots[name]
  let nodes
  if (scopedSlotFn) {
    // scoped slot
    props = props || {}
    if (bindObject) {
      if (process.env.NODE_ENV !== 'production' && !isObject(bindObject)) {
        warn('slot v-bind without argument expects an Object', this)
      }
      props = extend(extend({}, bindObject), props)
    }
    nodes =
      scopedSlotFn(props) ||
      (typeof fallbackRender === 'function' ? fallbackRender() : fallbackRender)
  } else {
    // 我们的例子的是这里
    nodes =
      this.$slots[name] ||
      (typeof fallbackRender === 'function' ? fallbackRender() : fallbackRender)
  }

  const target = props && props.slot
  if (target) {
    // 组件有 slot 属性,说明部分内容需要被分发到插槽
    // 创建 slot 是 targe 的 template 元素,nodes 是作为子元素
    return this.$createElement('template', { slot: target }, nodes)
  } else {
    // 没有插槽内容,显示兜底内容
    return nodes
  }
}

七、_s & toString

方法位置:src/shared/util.js -> function toString

方法参数:val,需要变成字符串的值

方法作用:将给定的值变成字符串形式:

  • 基本类型 String(val)
  • 复杂类型 JSON.stringify(val, null, 2)
export function toString (val: any): string {
  return val == null
    ? ''
    : Array.isArray(val) || (isPlainObject(val) && val.toString === _toString)
      ? JSON.stringify(val, null, 2)
      : String(val)
}

八、_l & renderList

方法位置:src/core/instance/render-helpers/render-list.js -> function renderList

方法参数:

  1. val,可迭代对象;
  2. render,渲染列表中的每个元素所需的渲染函数;

方法作用:遍历给定可迭代对象,这其中处理了数字、字符串的场景,为每一项调用渲染函数生成一个节点并放入结果列表。最后将得到的 VNode 结果列表返回,完成列表渲染;

我们的例子中有一个列表渲染:

// 处理 <span v-for="item in someArr" :key="index">{{item}}</span>
_l( 
  (someArr), // renderList 的 val 参数
  function (item) { // renderList 的 render 参数
    return _c(
      'span',
      { key:index },
      [
        _v(_s(item))
      ]
     )
  }
),

以下是 renderList 代码:

export function renderList (
  val: any,
  render: (
    val: any,
    keyOrIndex: string | number,
    index?: number
  ) => VNode
): ?Array<VNode> {
  let ret: ?Array<VNode>, i, l, keys, key
  if (Array.isArray(val) || typeof val === 'string') {
    // 抹平 val 是数组或者字符串
    ret = new Array(val.length)
    for (i = 0, l = val.length; i < l; i++) {
      ret[i] = render(val[i], i)
    }
  } else if (typeof val === 'number') {
    // val 是数字,遍历 0 - (val - 1) 的所有数字
    ret = new Array(val)
    for (i = 0; i < val; i++) {
      ret[i] = render(i + 1, i)
    }
  } else if (isObject(val)) {
    // val 为一个对象
    if (hasSymbol && val[Symbol.iterator]) {
      // val 部署了迭代器接口
      ret = []
      const iterator: Iterator<any> = val[Symbol.iterator]()
      let result = iterator.next()
      while (!result.done) {
        ret.push(render(result.value, ret.length))
        result = iterator.next()
      }
    } else {
      // val 是普通的未部署迭代器接口的对象
      keys = Object.keys(val)
      ret = new Array(keys.length)
      for (i = 0, l = keys.length; i < l; i++) {
        key = keys[i]
        ret[i] = render(val[key], key, i)
      }
    }
  }
  if (!isDef(ret)) {
    ret = []
  }
  (ret: any)._isVList = true

  // 返回 VNode 数组
  return ret
}

九、initRender 和 _c

前面说了很多的渲染函数的运行时帮助函数,但是其中没有 _c,这是因为 _c 的初始位置和之前的有所不同。

9.1 initRender 调用

initRenderVue.prototype._init 方法中的一个初始化步骤,这个已经很久远了。。。

方法调用:

export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    // ...
    // 解析组件中的插槽信息,得到 vm.$slot,
    // 得到 vm.$createElement、vm._c 方法
    
    initRender(vm)

    // ....
    if (vm.$options.el) {
      // 调用 $mount 方法,进入到挂载阶段
      vm.$mount(vm.$options.el)
    }
  }
}

9.2 initRender 方法

方法位置:src/core/instance/render.js -> export function initRender

方法参数:vmVue 实例

方法作用:

  1. 解析组件中的插槽信息;
  2. 创建 vm._c 方法,它是 createElement 的一个科里化方法,这样一来可以方便的为渲染函数绑定 vm 实例;
  3. 创建 vm.$createElement 方法,这个方法就是我们写 render 选项时的 h 方法;
export function initRender (vm: Component) {
  vm._vnode = null 
  vm._staticTrees = null 
  const options = vm.$options
  const parentVnode = vm.$vnode = options._parentVnode // the placeholder node in parent tree
  const renderContext = parentVnode && parentVnode.context
  
  // 解析插槽信息
  vm.$slots = resolveSlots(options._renderChildren, renderContext)
  vm.$scopedSlots = emptyObject

  // 内部使用
  vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
 
  // 用户创建 render 时所需要的 h
  // render(h) { return h(div, attrs, children)}
  vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)

 
  const parentData = parentVnode && parentVnode.data

  /* istanbul ignore else */
  if (process.env.NODE_ENV !== 'production') {
    defineReactive(vm, '$attrs', parentData && parentData.attrs || em
  } else {
    defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, null, true)
    defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true)
  }
}

十、总结

本篇小作文介绍了常见的渲染函数的帮助函数的功能:

  1. _c:创建元素的科里化方法
  2. _l:处理列表渲染
  3. _s:转成字符串
  4. _t:渲染插槽 slot 标签
  5. _ilooseIndexOf,判断某个值在数组中的位置,判断字面量
  6. _m:渲染静态树,缓存、静态标记

还说了一下两种初始化的时机:

  1. vm._c 的初始化是在执行 Vue 的初始化逻辑的方法 _init 方法中完成的创建,是 vm 实例的私有方法,再次期间还初始了 vm.$createElement 也就是大家熟知的 h
  2. 其余的帮助函数都是在 renderMixin 中,挂载到 Vue.prototype 对象的公有方法;

因为篇幅的原因,也由于 vm._c 和前面的几个不太一样,所以很多的的细节并没有展开讲。vm._c 将作为重中之重单独开篇讨论!