从 vue 源码看问题 —— vue 中的 render helper 是什么?(一)

简介: 从 vue 源码看问题 —— vue 中的 render helper 是什么?

image.png


前言

前面的文章中提到组件更新时,需要先执行编译器生成的 渲染函数 得到组件的 vnode,而 渲染函数 生成 vnode 则是通过其中的 _c、_l、、_v、_s 等方法实现的. 比如:

  • 普通节点 被编译成了可执行 _c 函数
  • v-for 节点被编译成了可执行的 _l 函数
  • ... 那么下面就一起来了解一下,什么是 render helper ?

深入源码

入口位置

通过 generate() 方法的返回值中的 { render: 'with(this){return ${code}}' } 可知,_c、_l、、_v、_s 等方法是属于实例上的方法,当然这需要你对 jswith(){} 方法有一定的了解.

既然是实例方法,那么在之前 vue 中的实例 文章中有提到对应文件位置:src\core\instance\render.js.

export function generate(
  ast: ASTElement | void, // ast 对象
  options: CompilerOptions // 编译选项
): CodegenResult {
  /*
    实例化 CodegenState 对象,参数是编译选项,最终得到 state ,其中大部分属性和 options 一样
  */
  const state = new CodegenState(options)
  /* 
   生成字符串格式的代码,比如:'_c(tag, data, children, normalizationType)'
    - data 为节点上的属性组成 JSON 字符串,比如 '{ key: xx, ref: xx, ... }'
    - children 为所有子节点的字符串格式的代码组成的字符串数组,格式:
      `['_c(tag, data, children)', ...],normalizationType`,
    - normalization 是 _c 的第四个参数,表示节点的规范化类型(非重点,可跳过)
    注意:code 并不一定就是 _c,也有可能是其它的,比如整个组件都是静态的,则结果就为 _m(0)
  */
  const code = ast ? (ast.tag === 'script' ? 'null' : genElement(ast, state)) : '_c("div")'
  return {
    render: `with(this){return ${code}}`,
    staticRenderFns: state.staticRenderFns
  }
}
复制代码

installRenderHelpers() 方法

根据上面的 src\core\instance\render.js 找到了对应的 renderMixin(),其中很明显 installRenderHelpers() 就是我们需要找的.

export function renderMixin (Vue: Class<Component>) { 
    // install runtime convenience helpers 
    // 在组件实例上挂载一些运行时需要用到的工具方法
    installRenderHelpers(Vue.prototype) 
    // ... 
}
复制代码

文件位置:src\core\instance\render-helpers\index.js

/**
 * 在实例上挂载简写的渲染工具函数,这些都是运行时代码
 * 这些工具函数在编译器生成的渲染函数中被使用到了
 * @param target Vue 实例
 */
export function installRenderHelpers (target: any) {
  /**
   v-once 指令的运行时帮助程序,为 VNode 加上打上静态标记
    注意:包含 v-once 指令的节点都被当作静态节点处理: 
      node.isStatic = true
      node.key = key
      node.isOnce = true
  */
  target._o = markOnce
  // 使用 parseFloat() 将值转换为数字
  target._n = toNumber
  /*
    如果传入值为 null,直接返回 ''
    如果是数组或者是普通对象,调用 JSON.Stringify() 序列化引用类型
    否则使用 String 转换为字符串
  */ 
  target._s = toString
  /*
   运行时渲染 v-for 列表的帮助函数,循环遍历 val 值,
   依次为每一项执行 render 方法生成 VNode,最终返回一个 VNode 数组
  */
  target._l = renderList
  target._t = renderSlot
  /*
    判断两个值是否相等:数组、普通对象、Date 对象、基本类型(全部转成字符串比较)
  */
  target._q = looseEqual
  /**
    相当于数组的 indexOf() 方法,本质就是调用 looseEqual()
  */
  target._i = looseIndexOf
  /*
    运行时负责生成静态树的 VNode 的帮助程序:
     1、执行 staticRenderFns 数组中指定下标的渲染函数,生成静态树的 VNode 并缓存,
        下次在渲染时从缓存中直接读取(isInFor 必须为 true)
     2、为静态树的 VNode 打静态标记
   */
  target._m = renderStatic
  target._f = resolveFilter
  target._k = checkKeyCodes
  target._b = bindObjectProps
  /*
    为文本节点创建 VNode,其中:
     VNode.text = 'xxx'
     VNode.isComment = false
  */ 
  target._v = createTextVNode
  /*
    为空节点创建 VNode,其中: 
     VNode.text = 'xxx' 
     VNode.isComment = true
  */ 
  target._e = createEmptyVNode
  target._u = resolveScopedSlots
  target._g = bindObjectListeners
  target._d = bindDynamicKeys
  target._p = prependModifier
}
复制代码

target._o = markOnce

文件位置:src\core\instance\render-helpers\index.js

markOnce() 方法

/**
 * Runtime helper for v-once.
 * Effectively it means marking the node as static with a unique key.
 * 使用唯一的 key 将节点标记为静态节点
 */
export function markOnce (
  tree: VNode | Array<VNode>,
  index: number,
  key: string
) {
  /*
   <div v-once key="mydiv"><div>
   保证 key 的唯一,即 key = `__once__${index}_mydiv`
  */
  markStatic(tree, `__once__${index}${key ? `_${key}` : ``}`, true)
  return tree
}
复制代码

markStatic() 方法

function markStatic (
  tree: VNode | Array<VNode>,
  key: string,
  isOnce: boolean
) {
  // tree 为数组
  if (Array.isArray(tree)) {
    // 遍历 tree 为每个 tree[i] 通过 markStaticNode() 进行标记
    for (let i = 0; i < tree.length; i++) {
      if (tree[i] && typeof tree[i] !== 'string') {
        markStaticNode(tree[i], `${key}_${i}`, isOnce)
      }
    }
  } else {
    // tree 不是数组直接调用 markStaticNode() 进行标记
    markStaticNode(tree, key, isOnce)
  }
}
复制代码

markStaticNode() 方法

/*
  为 VNode 打静态标记,在 VNode 上添加三个属性:
 { isStatick: true, key: xx, isOnce: true or false } 
*/
function markStaticNode (node, key, isOnce) {
  node.isStatic = true
  node.key = key
  node.isOnce = isOnce
}
复制代码

target._l = renderList

文件位置:src\core\instance\render-helpers\render-list.js

/**
 * Runtime helper for rendering v-for lists.
 * 
 * 用于呈现 v-for 列表的运行时帮助程序
 * v-for 原理:
 *  通过循环为类型为 数组、字符串、数字、对象 调用 render 方法,
 *  得到一个 VNode 数组
 */
export function renderList (
  val: any,
  render: (
    val: any,
    keyOrIndex: string | number,
    index?: number
  ) => VNode
): ?Array<VNode> {
  let ret: ?Array<VNode>, i, l, keys, key
  /*
   val 是数组 或者 是字符串,如:
    <div v-for="item in [xxx,xxx,...]"></div>
    <div v-for="item in 'hello'"></div>
  */
  if (Array.isArray(val) || typeof val === 'string') {
    ret = new Array(val.length)
    // 通过循环为每个元素或者字符调用 render 方法,得到 VNode 节点
    for (i = 0, l = val.length; i < l; i++) {
      ret[i] = render(val[i], i)
    }
  } else if (typeof val === 'number') {
    /*
    val 是数字,如:
      <div v-for="item in 10"></div>
    */
    ret = new Array(val)
    // 循环为每个数字调用 render 方法,得到 VNode 节点
    for (i = 0; i < val; i++) {
      ret[i] = render(i + 1, i)
    }
  } else if (isObject(val)) {
    // val 是对象
    // val 是个可迭代的对象
    if (hasSymbol && val[Symbol.iterator]) {
      ret = []
      /*
       获取可迭代对象实例,通过 while 循环为当前迭代对象,
       调用 render 方法,得到 VNode 节点
      */ 
      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 是非可迭代对象
       通过 Object.keys() 为对象上的每个 key 对应 value 通过循环调用 render 方法
       得到 VNode 节点
      */
      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)
      }
    }
  }
   /*
    isDef(ret) --> ret !== undefined && ret !== null
    !isDef(ret) --> ret === undefined || ret === null
  */ 
  if (!isDef(ret)) {
    ret = []
  }
  // 返回 VNode 数组
  (ret: any)._isVList = true
  return ret
}
复制代码

target._m = renderStatic

文件位置:src\core\instance\render-helpers\render-list.js

/**
 * Runtime helper for rendering static trees.
 * 
 * 运行时负责生成静态树的 VNode 的帮助程序:
 *   1、执行 staticRenderFns 数组中指定下标的渲染函数,
 *      生成静态树的 VNode 并缓存,下次在渲染时从缓存中直接读取(isInFor 必须为 true)
 *   2、为静态树的 VNode 打静态标记
 * 
 * @param { number} index 表示当前静态节点的渲染函数在 staticRenderFns 数组中的下标索引
 * @param { boolean} isInFor 表示当前静态节点是否被包裹在含有 v-for 指令的节点内部
 */
export function renderStatic (
  index: number,
  isInFor: boolean
): VNode | Array<VNode> {
  // 缓存,静态节点第二次被渲染时就从缓存中直接获取已缓存的 VNode
  const cached = this._staticTrees || (this._staticTrees = [])
  let tree = cached[index]
  /*
   如果已渲染静态树且不在 v-for 内,就可以重复使用同一棵树。
  */
  if (tree && !isInFor) {
    return tree
  }
  /*
   如果已渲染静态树且在 v-for 内,那么就需要重新渲染这个树
    通过 staticRenderFns 数组,获取并执行对应的 静态渲染函数,
    得到新的 VNode 节点,并缓存最新的结果
  */
  tree = cached[index] = this.$options.staticRenderFns[index].call(
    this._renderProxy,
    null,
    this // for render fns generated for functional component templates
  )
  /*
  通过 markStatic 方法,标记静态节点,接收三个参数:
    tree: VNode | Array<VNode>
    key: string
    isOnce: boolean
  */ 
  markStatic(tree, `__static__${index}`, false)
  return tree
}
复制代码

vm._c = createElement()

文件位置:/src/core/instance/render.js

export function initRender (vm: Component) {
   ...
  /*
    将 createElement 方法绑定到此实例,方便在其中获得当前的渲染上下文
    参数顺序:tag, data, children, normalizationType, alwaysNormalize
    内部版本由从模板编译的渲染函数使用
  */ 
  vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
  ...
}



目录
相关文章
|
5天前
|
JavaScript 前端开发 安全
Vue响应式设计
【5月更文挑战第30天】Vue响应式设计
25 1
|
2天前
|
JavaScript API
vue组合式和选项式
vue组合式和选项式
4 2
|
4天前
|
JavaScript 程序员 网络架构
vue路由从入门到进阶 --- 声明式导航详细教程
vue路由从入门到进阶 --- 声明式导航详细教程
vue路由从入门到进阶 --- 声明式导航详细教程
|
4天前
|
资源调度 JavaScript UED
vue路由的基础知识总结,vueRouter插件的安装与使用
vue路由的基础知识总结,vueRouter插件的安装与使用
|
4天前
|
JavaScript
|
5天前
|
编解码 JavaScript API
Vue在移动端的表现如何?
【5月更文挑战第30天】Vue在移动端的表现如何?
12 2
|
5天前
|
JavaScript 前端开发 API
Vue与其他框架的对比优势
【5月更文挑战第30天】Vue与其他框架的对比优势
11 1
|
5天前
|
JavaScript
Vue常用知识点总结
Vue常用知识点总结
13 0
|
6天前
|
JavaScript Java 测试技术
基于vue和微信小程序的校园自助打印系统+springboot+vue.js附带文章和源代码设计说明文档ppt
基于vue和微信小程序的校园自助打印系统+springboot+vue.js附带文章和源代码设计说明文档ppt
26 7
|
6天前
|
JSON JavaScript 前端开发