浅曦Vue源码-29-挂载阶段-$mount-(18)genFor 和 v-for

187 阅读4分钟

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

一、前情回顾 & 背景

上一篇小作文讨论了 genElement 的第一个情况——genStatic 获取渲染结果的过程,genStatic 主要做了以下工作:

  1. 标记当前 el.staticProcessedtrue,防止被重复处理;
  2. 递归调用 genElement,产生各个节点的渲染函数;
  3. genElement 最后的 else 就是处理普通元素的渲染的。首先调用 genData 获取元素上的 data, 然后调用 genChildren() 将子元素都处理成由 render 函数代码组成的数组项,每一项都渲染一个子元素;
  4. genStatic 获取 genElement 返回的结果,包裹 with(this) 语句,然后 pushstaticRenderFns
  5. genStatic 的最后用将当前静态根节点处理成 _m 方法的运行时调用_m(当前元素在 staticRenderFns 的索引, true 或 '')

本篇小作文将要讨论 Vue 处理 v-for 指令的 genFor 方法;从上面的 genStatic 的返回值 _m(index, true) 能看出,ast 节点都被转变成了运行时渲染帮助函数的调用,这些帮助函数就会创建真实的 DOM 节点;v-for 也不例外,最终也会被处理成 _l 方法的调用;

二、v-for 和 genFor 方法

前面在说 parse 阶段将模板转换成 ast 节点时讲过的, v-for 被处理成 el.forel.alias 两个 el 上的属性,el.for 的值指向 可迭代对象el.alias 指向 可迭代对象的条目名,比如下面例子中的 someArr 就是可迭代对象,item 就是可迭代对象条目名称:

<span v-for="item in someArr" :key="index">{{item}}</span>

genFor 方法用于在 ast 生成 render 函数的 generate 阶段,被 genElement 调用,生成处理 v-for 的渲染函数代码,即渲染函数 "_l(exp, funciton(alias, iterator1, iterator2) {return _c(tag, data, children)})";genFor 调用的前提条件时是 el.for 属性有值,且 el.forProcessed 不为 true;

export function genElement (): string {
  if(....){
  } else if (el.for && !el.forProcessed) {
    // genFor 被调用,用于处理带有 v-for 的 ast 节点
    return genFor(el, state)
  } 
  
  return code
}

三、genFor 方法

方法位置:src/compiler/codegen/index.js -> function genFor

方法参数:

  1. elast 节点对象;
  2. stateCodegenState 对象
  3. altGen/altHelper 暂时忽略

方法作用:根据 el.forel.alias 处理 v-for 指令得到运行时的渲染函数代码,形如:

"_l(exp, function (alias, iterator1, iterator2) { return _c(tag, data, chilrend)})";具体步骤如下:

  1. 获取可迭代对象即 el.for 属性值赋值给 exp 常量;
  2. 获取可迭代对象的条目名称 即 el.alias 属性值赋值给 alias
  3. 获取迭代器 el.iterator1/el.iterator2;迭代器是个啥?即 v-for="(val, key) in obj",此时 iterator1keyiterator2val
  4. 检测使用 v-for 指令时节点是否相应的设置了 key 属性,如果未设置则抛出警告;
  5. 标记当前 el.forProcessedtrue,标识已经被处理过,防止递归 genElement 时重复处理导致死循环;
  6. genStatic 一样,这里并不处理具体的生成 render 函数的逻辑,genFor 只处理标记 el.forProcessed 和 拼接,真正的生成 render 函数的部分还是递归调用 genElement 方法;
export function genFor (
  el: any,
  state: CodegenState,
  altGen?: Function,
  altHelper?: string
): string {
  // v-for 的迭代器,比如例子中的 someArr
  const exp = el.for

  // 可迭代对象条目名
  const alias = el.alias

  // iterator 为 v-for="(val, key) in obj" 这种形式是,比如 iterator1 = key
  const iterator1 = el.iterator1 ? `,${el.iterator1}` : ''
  const iterator2 = el.iterator2 ? `,${el.iterator2}` : ''

  // 提示,v-for 指令在组件上时必须使用 key
  if (process.env.NODE_ENV !== 'production' &&
    state.maybeComponent(el) &&
    el.tag !== 'slot' &&
    el.tag !== 'template' &&
    !el.key
  ) {
    // 抛出警告,没有 key
  }

  // 标记当前节点上的 v-for 被处理过了
  el.forProcessed = true // avoid recursion
  return `${altHelper || '_l'}((${exp}),` +
    `function(${alias}${iterator1}${iterator2}){` +
      `return ${(altGen || genElement)(el, state)}` + // 递归调用 genElement()
    '})'
}

3.1 genFor 返回值

return `${altHelper || '_l'}((${exp}),` +
    `function(${alias}${iterator1}${iterator2}){` +
      `return ${(altGen || genElement)(el, state)}` + 
    '})'

上面的 genFor 的返回值中用到的各个变量和常量:

  • altHelperundefined
  • aliasitem
  • iterator1、iterator2 是空字符串 ''`
  • alterGenundefined
  • genElement(el, state) 返回值:"_c('span',{key:index},[_v(_s(item))])"

结合前面的分析,得到 <span v-for="item in someArr" :key="index">{{item}}</span> 得到的渲染函数(render函数):

return `_l(someArr, function (item) { return _c('span', { key: index }, [_v(_s(item))])})`

这里其实我们可以大胆的猜测一下 _l 方法的工作:遍历 someArr,遍历过程中调用 function(item) { _c(...)}, 传入 someArr 的每一项, 最终得到 someArr 每一项对应的 DOM 元素;当然这只是猜测,后面我们会去验证这个猜想;

四、总结

本篇小作文讨论了 Vue 在编译时处理另一个常用指令 —— v-for 的渲染函数生成的方法 genFor,它的主要工作如下:

  1. 首先明确 genForgenElement 的一个分支流程,用于处理有 v-for 指令的 ast 节点;
  2. 标记 el.processedtrue,防止 genFor 递归 genElement 时进入死循环;
  3. 根据前面 parse 阶段所得到的 el.for/el.alias/el.iterator1/el.iterator2 信息拼接得到 v-for 的渲染函数代码结构,期间处理生成元素的具体逻辑还是有 genElement 方法实现的;