v-if 与 v-for 背后的奥秘(一)

228 阅读12分钟

v-if

<template>
  <div id="app">
    <span v-if="isShow">hello2</span>   
  </div>
</template>

上面这段模板会被 Vue 编译器编译为下面的渲染函数

var render = function render() {
  var _vm = this,
    _c = _vm._self._c;
  return _c('div', {
    attrs: {
      "id": "app"
    }
  }, [_vm.isShow ? _c('span', [_vm._v("hello2")]) : _vm._e()]);
};

笔者采用的 Vue2 版本为 2.7.15

由于 Vue2 的官方文档中没有提供在线的演练场,Vue2 要得到模板编译后的渲染函数会稍微麻烦一些,需要手动调用 Vue2 提供的 @vue/compiler-sfc 包。主要代码如下:

const compiler = require("@vue/compiler-sfc");

const fs = require("fs/promises");

// 使用 readFile 获取 Vue 单文件组件的代码内容
async function getVueString() {
  const vueData = await fs.readFile("./App.vue", { encoding: "utf8" });
  return vueData;
}

// 编译单文件组件的模板
async function compileSfc() {
  const vFileString = await getVueString();
  const res = compiler.parse({
    source: vFileString,
    filename: "App.vue",
  });
  const templateContent = res.template.content;
  const templateRes = compiler.compileTemplate({
    source: templateContent,
    filename: "App.vue",
    prettify: true,
  });
  const templateCode = templateRes.code;
  return templateCode
}

compileSfc();
  • compiler.parse 函数会把组件源码解析为单文件组件的描述对象

  • compiler.compileTemplate 函数也是返回一个对象,该对象包含 ast 、code 和 source 属性,其中的 code 属性就是模板编译后的渲染函数。

可以看到 v-if 经过 Vue 的编译器编译为了三元表达式。条件表达式为 true ,则正常渲染元素,否则就渲染空注释节点。

_c 实际上就是 createElement 函数,作用是创建虚拟 DOM 。

// src/core/instance/render.ts

export function initRender(vm: Component) {
  // ...
  vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
}

_v 实际上就是 createTextVNode 函数,作用是创建文本类型的虚拟 DOM。

_e 实际上就是 createEmptyVNode 函数,作用是创建一个空的注释节点

// src/core/instance/render-helpers/index.ts

import { createTextVNode, createEmptyVNode } from 'core/vdom/vnode'

export function installRenderHelpers(target: any) {
  target._v = createTextVNode
  target._e = createEmptyVNode
}

Vue 模板的编译过程首先会对模板做解析,解析生成 AST 树,包含 v-if 信息的 AST 元素会由 processIf 函数解析

// src/compiler/parser/index.ts

function processIf(el) {
  const exp = getAndRemoveAttr(el, 'v-if')
  if (exp) {
    // 处理 v-if 指令
    el.if = exp
    addIfCondition(el, {
      exp: exp,
      block: el
    })
  } else {
    // 处理 v-else 指令
    if (getAndRemoveAttr(el, 'v-else') != null) {
      el.else = true
    }
    // 处理 v-else-if 指令
    const elseif = getAndRemoveAttr(el, 'v-else-if')
    if (elseif) {
      el.elseif = elseif
    }
  }
}

本文 Vue2 中的源码均摘自 2.7.15 版本

processIf 函数会调用 addIfCondition 收集 v-if 模板中的条件表达式和虚拟节点信息,存储到 AST 元素对象的 ifConditions 数组中

// src/compiler/parser/index.ts

export function addIfCondition(el: ASTElement, condition: ASTIfCondition) {
  if (!el.ifConditions) {
    el.ifConditions = []
  }
  el.ifConditions.push(condition)
}

Vue 中的模板经过解析(parse)输出生成 AST 树后会对这颗树做优化(optimize),做优化的过程实际上就是深度遍历这个 AST 树,去检测他的每一棵子树是不是静态节点。优化完后会进入代码生成的阶段。

含有 v-if 信息的 AST 元素会传给 genIf 函数,最终由该函数生成三元表达式

// src/compiler/codegen/index.ts

export function genIf(
  el: any,
  state: CodegenState,
  altGen?: Function,
  altEmpty?: string
): string {
  el.ifProcessed = true // avoid recursion
  return genIfConditions(el.ifConditions.slice(), state, altGen, altEmpty)
}

genIf 函数实际上的调用 genIfConditions 函数。

// src/compiler/codegen/index.ts

function genIfConditions(
  conditions: ASTIfConditions,
  state: CodegenState,
  altGen?: Function,
  altEmpty?: string
): string {
  if (!conditions.length) {
    return altEmpty || '_e()'
  }

  const condition = conditions.shift()!
  if (condition.exp) {
    // 生成三元表达式
    return `(${condition.exp})?${genTernaryExp(
      condition.block
    )}:${genIfConditions(conditions, state, altGen, altEmpty)}`
  } else {
    return `${genTernaryExp(condition.block)}`
  }

  // v-if with v-once should generate code like (a)?_m(0):_m(1)
  function genTernaryExp(el) {
    return altGen
      ? altGen(el, state)
      : el.once
      ? genOnce(el, state)
      : genElement(el, state)
  }
}

v-if 总结

Vue 模板经过解析(parse),生成 AST 树(抽象语法树),然后由 addIfCondition 函数收集 v-if 的 AST 元素上的条件表达式和虚拟节点信息,存储到 AST 元素对象的 ifConditions 数组中,然后 Vue 编译器会对整个 AST 树做优化(optimize),优化的过程实际上就是深度遍历这个 AST 树,去检测它的每一颗子树是不是静态节点,最后就是把优化后的 AST 树转换成可执行的代码,v-if 的 AST 元素会传入 genIf 函数,最终生成三元表达式。

v-for

<template>
  <div id="app">
    <li v-for="todo in todos" :key="todo.name">
      {{ todo.name }}
    </li>
  </div>
</template>

上面这段模板会被 Vue 编译器编译为下面的渲染函数

var render = function render() {
  var _vm = this,
    _c = _vm._self._c;
  return _c('div', {
    attrs: {
      "id": "app"
    }
  }, _vm._l(_vm.todos, function (todo) {
    return _c('li', {
      key: todo.name
    }, [_vm._v(" " + _vm._s(todo.name) + " ")]);
  }), 0);
};

_c_v 函数的作用在上文已有说明,这里不在赘述

_s 实际上是 Vue 内部的 toString 函数

// src/core/instance/render-helpers/index.ts

import { toString } from 'shared/util'

export function installRenderHelpers(target: any) {
  target._s = toString
}

toString 函数的源码实现比较简单,当传入的参数是 null ,则直接返回空字符串,传入的是数组或对象,则调用 JSON.stringify 方法将数组或对象转为字符串,其他类型的值,则调用 js 原生的 String() 函数,将传入的值转为字符串。

// src/shared/util.ts

const _toString = Object.prototype.toString

/**
 * Convert a value to a string that is actually rendered.
 */
export function toString(val: any): string {
  return val == null
    ? ''
    : Array.isArray(val) || (isPlainObject(val) && val.toString === _toString)
    ? JSON.stringify(val, null, 2)
    : String(val)
}

可以看到,v-for 指令会被编译为 _l 函数。_l 函数实际上是 Vue 内部渲染列表的函数(renderList

// src/core/instance/render-helpers/index.ts

import { renderList } from './render-list'

export function installRenderHelpers(target: any) {
  target._l = renderList
}

源码的注释也说得很清楚,renderList 是用于渲染 v-for 列表的运行时辅助函数。可以这样理解,v-for 指令最终会被编译为 renderList 函数来执行。

// src/core/instance/render-helpers/render-list.ts

/**
 * Runtime helper for rendering v-for lists.
 */
export function renderList(
  val: any,
  render: (val: any, keyOrIndex: string | number, index?: number) => VNode
): Array<VNode> | null {
  let ret: Array<VNode> | null = null,
    i,
    l,
    keys,
    key
  if (isArray(val) || typeof val === 'string') {
    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') {
    ret = new Array(val)
    for (i = 0; i < val; i++) {
      ret[i] = render(i + 1, i)
    }
  } else if (isObject(val)) {
    if (hasSymbol && val[Symbol.iterator]) {
      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 {
      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 as any)._isVList = true
  return ret
}

renderList 函数中:

isDef 作用是判断传入的值是否有被定义

// src/shared/util.ts

export function isDef<T>(v: T): v is NonNullable<T> {
  return v !== undefined && v !== null
}

isObject 作用是判断传入的值是否为非 null 的对象

// src/shared/util.ts

export function isObject(obj: any): boolean {
  return obj !== null && typeof obj === 'object'
}

hasSymbol 作用是判断当前宿主环境是否支持原生 Symbol 和 Reflect.ownKeys。首先判断 Symbol 和 Reflect 是否存在,并使用 isNative 函数保证 Symbol 与 Reflect.ownKeys 全部是原生定义。

// src/core/util/env.ts

export const hasSymbol =
  typeof Symbol !== 'undefined' &&
  isNative(Symbol) &&
  typeof Reflect !== 'undefined' &&
  isNative(Reflect.ownKeys)
// src/core/util/env.ts

// 判断是否是浏览器原生支持的函数
export function isNative(Ctor: any): boolean {
  return typeof Ctor === 'function' && /native code/.test(Ctor.toString())
}

对于浏览器原生支持的函数,将该函数转为字符串时,该函数体会变为 native code 字符串。可利用这个特性来判断该函数是否为浏览器原生支持的函数。88.png

renderList 函数的逻辑很清晰,分四种情况。

当传入的值(val)为数组或字符串时,for 循环直接遍历:

// src/core/instance/render-helpers/render-list.ts

if (isArray(val) || typeof val === 'string') {
  ret = new Array(val.length)
  for (i = 0, l = val.length; i < l; i++) {
    ret[i] = render(val[i], i)
  }
}

当传入的值(val)为数字时,for 循环直接遍历,传入的数字是多少则遍历多少次:

// src/core/instance/render-helpers/render-list.ts

if (isArray(val) || typeof val === 'string') {
  // ...
} else if (typeof val === 'number') {
  // 传入的值为数字
  ret = new Array(val)
  for (i = 0; i < val; i++) {
    ret[i] = render(i + 1, i)
  }
}

当传入的值(val)为对象时,则分为两种情况,当宿主环境支持 Symbol ,且传入的值含有迭代器是,则调用该对象内置的迭代器的 next() 方法进行遍历。否则,通过 Object.key 生成对象的属性数组,然后通过 for 循环遍历

// src/core/instance/render-helpers/render-list.ts

if (isArray(val) || typeof val === 'string') {
  // ...
} else if (typeof val === 'number') {
  // ...
} else if (isObject(val)) {
  // 传入的值是对象
  if (hasSymbol && val[Symbol.iterator]) {
    // 宿主环境支持 Symbol 并且支持迭代器
    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 {
    // 通过 Object.key 生成对象的属性数组,然后通过 for 循环遍历
    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)
    }
  }
}

当传入的值不是数组、字符串、数字和对象时,则将返回结果 (ret)设置为空数组,即 v-for 渲染的为空数组

// src/core/instance/render-helpers/render-list.ts

if (!isDef(ret)) {
  ret = []
}

通过上面的分析可知,v-for 本质是调用 renderList 函数。那么 v-for 是如何被编译为 renderList 函数的?

Vue 模板的编译过程首先会对模板做解析,解析生成 AST 树,包含 v-for 信息的 AST 元素会由 processFor 函数解析

// src/compiler/parser/index.ts

export function processFor(el: ASTElement) {
  let exp
  if ((exp = getAndRemoveAttr(el, 'v-for'))) {
    const res = parseFor(exp)
    if (res) {
      extend(el, res)
    } else if (__DEV__) {
      warn(`Invalid v-for expression: ${exp}`, el.rawAttrsMap['v-for'])
    }
  }
}

getAndRemoveAttr 函数用于获取并删除 AST 元素上的属性,在这里则是获取 v-for 的属性值,即 todo in todos

89.png

getAndRemoveAttr 函数会遍历 AST 元素上 attrsList 属性,如果传入了 removeFromMap 参数,则会将属性从 AST 元素上的 attrsMap 属性中删除:

// src/compiler/helpers.ts

export function getAndRemoveAttr(
  el: ASTElement,
  name: string,
  removeFromMap?: boolean
): string | undefined {
  let val
  // attrsMap 用于快速查找有没有某个属性,attrsList 用于存放所有的属性
  if ((val = el.attrsMap[name]) != null) {
    const list = el.attrsList
    for (let i = 0, l = list.length; i < l; i++) {
      if (list[i].name === name) {
        list.splice(i, 1)
        break
      }
    }
  }
  if (removeFromMap) {
    delete el.attrsMap[name]
  }
  return val
}

调用 getAndRemoveAttr 函数获得 v-for 指令的表达式后,会将该表达式传给 parseFor 函数,解析出 v-for 遍历的变量和定义的局部变量(alias) 。

90.png

// src/compiler/parser/index.ts

export const forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/
export const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/
const stripParensRE = /^\(|\)$/g


export function parseFor(exp: string): ForParseResult | undefined {
  const inMatch = exp.match(forAliasRE)
  if (!inMatch) return
  const res: any = {}
  res.for = inMatch[2].trim()
  const alias = inMatch[1].trim().replace(stripParensRE, '')
  const iteratorMatch = alias.match(forIteratorRE)
  if (iteratorMatch) {
    res.alias = alias.replace(forIteratorRE, '').trim()
    res.iterator1 = iteratorMatch[1].trim()
    if (iteratorMatch[2]) {
      res.iterator2 = iteratorMatch[2].trim()
    }
  } else {
    res.alias = alias
  }
  return res
}

parseFor 函数使用了三个正则表达式:

forAliasRE 用于匹配类似于 for 循环的语法结构,例如 "item in array" 或 "key of object" 。

export const forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/
  • ([sS]*?),这是一个捕获组,用来匹配任意空白字符或非空白字符,并且是非贪婪的,也就是说会尽可能少地匹配字符。这个捕获组的作用是匹配循环语句中的循环变量(例如 "item"、"key")。

  • s+, 匹配至少一个空白字符。

  • ([sS]*),这是第二个捕获组,字符或非空白字符,这表示循环中的数组或对象(例如 "array"、"object")

forIteratorRE 用于匹配一个逗号分隔的字符串,该字符串由逗号开头,并且后续由逗号和非逗号、非右大括号、非右中括号的字符组成。

export const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/

借助正则表达式可视化工具可视化上面的正则表达式,则更加容易理解上面正则表达式的作用:

91.png

例如对于如下模板:

<div id="app">
  <li v-for="(value, name, index) in object" :key="value">
    {{ index }}. {{ name }}: {{ value }}
  </li>
</div>

forIteratorRE 会匹配到 '(value, name, index) in object' 字符串中的 ', name, index'

92.png

match 方法返回的数组中,在索引 0 处,为该正则完整的匹配项;在索引 1 处,第一个捕获括号匹配的内容,在索引 2 处,第二个捕获括号匹配的内容,以此类推。

forIteratorRE 正则中有两个捕获组和一个非捕获组:

93.png

捕获组匹配到的结果会作为 match 方法的返回数组中的元素,在索引 1 处为第一个捕获括号匹配的内容。在索引 2 处为第二个捕获括号匹配的内容,以此类推。

94.png

非捕获组匹配到的结果不会出现在 match 方法返回的数组中。

stripParensRE 用于匹配字符串开头的左括号'(' 或者字符串末尾的右括号 ')'。使用正则可视化工具可视化后的结果:

95.png

const stripParensRE = /^\(|\)$/g

^:表示匹配输入的开始位置,在这里表示左括号('(')开头的字符串

(|):表示匹配左括号或者右括号

|:是一个逻辑或操作符,表示在匹配时可以选择两个条件中的任意一个

$:表示匹配输入的结束位置,在这里表示以括号结尾的字符串

g:表示进行全局匹配,即匹配所有符合条件的子字符串

假如 parseFor 函数传入的参数 exp'(value, name, index) in object' ,则执行过程如下:


// export const forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/
// 使用 forAliasRE 匹配 exp 字符串
var inMatch = exp.match(forAliasRE);

得到 inMatch 为如下结果数组

96.png

// 如果 inMatch 为空,则直接返回
if (!inMatch) return
// 定义要返回的对象
const res: any = {}
// 取 inMatch 数组中索引为 2 的值,即 forAliasRE 正则
// 第二个捕获括号匹配的结果,也即 v-for 遍历的那个变量
// 在这里是 object
res.for = inMatch[2].trim()

97.png

// 取 inMatch 数组中索引为 1 的值,即 forAliasRE 正则
// 第一个捕获括号匹配的结果,在这里是 '(value, name, index)' ,
// 使用 stripParensRE 正则去除该字符串的左右括号,
// 即 alias 的值为 'value, name, index' 
const alias = inMatch[1].trim().replace(stripParensRE, '')

98.png

// 使用 forIteratorRE 正则匹配,
// 目的是为了得到 v-for 定义的遍历指针
const iteratorMatch = alias.match(forIteratorRE)

得到 iteratorMatch 为如下结果数组

99.png

// 如果 `iteratorMatch` 有值
if (iteratorMatch) {
  // 则将 alias 中匹配 forIteratorRE 正则的字符串替换为空字符串,
  // 得到 v-for 指令中定义的第一个局部变量,保存在 res 的 alias 属性中
  res.alias = alias.replace(forIteratorRE, '').trim()
  // 取 forIteratorRE 正则第一个捕获括号中匹配到的结果,
  // 去除左右两边空字符串,得到 v-for 指令中的第二遍历指针,
  // 并保存到 res 的 iterator1 属性中
  res.iterator1 = iteratorMatch[1].trim()
  if (iteratorMatch[2]) {
    // 取 forIteratorRE 正则第二个捕获括号中匹配到的结果,
    // 去除左右两边空字符串,得到 v-for 指令中的第三遍历指针,
    // 并保存到 res 的 iterator2 属性中
    res.iterator2 = iteratorMatch[2].trim()
  }
}
if (iteratorMatch) {
  // ...
} else {
  // iteratorMatch 数组不存在,则 v-for 指令只定义了一个
  // 局部变量,直接将 alias 保存在 res 的 alias 属性中
  res.alias = alias
}
// 最后返回 res
return res

最后得到 res 对象为:

100.png

其中包含了 v-for 指令需要遍历的变量和迭代指针。

processFor 函数中调用 parseFor 解析完 v-for 指针相关信息后,会将解析到的信息存储到 AST 元素中:

// src/compiler/parser/index.ts

export function processFor(el: ASTElement) {
  let exp
  if ((exp = getAndRemoveAttr(el, 'v-for'))) {
    const res = parseFor(exp)
    if (res) {
      // 将解析到的 v-for 指令相关信息,
      // 存储到 AST 元素中
      extend(el, res)
    } else if (__DEV__) {
      warn(`Invalid v-for expression: ${exp}`, el.rawAttrsMap['v-for'])
    }
  }
}

extend 函数的逻辑很简单,就是接收两个对象,将后一个对象合并到前一个对象中。如果遇到 key 相同的情况,则后一个对象的值会覆盖前一个对象的值。

// src/shared/util.ts

/**
 * Mix properties into target object.
 */
export function extend(
  to: Record<PropertyKey, any>,
  _from?: Record<PropertyKey, any>
): Record<PropertyKey, any> {
  for (const key in _from) {
    to[key] = _from[key]
  }
  return to
}

v-if 指令一样,解析(parse)输出生成 AST 树后会对这棵树做优化(optimize),做优化的过程实际上就是深度遍历这个 AST 树,去检测他的每一棵子树是不是静态节点。优化完后会进入代码生成的阶段。

含有 v-for 信息的 AST 元素会传给 genFor 函数,最终由该函数生成执行 renderList 函数的代码。

最终本示例的模板:

<div id="app">
  <li v-for="(value, name, index) in object" :key="value">
    {{ index }}. {{ name }}: {{ value }}
  </li>
</div>

会编译为如下代码执行:

var render = function render() {
  var _vm = this,
    _c = _vm._self._c;
  return _c('div', {
    attrs: {
      "id": "app"
    }
  }, /*v-for 指令最终编译为的代码*/_vm._l(_vm.object, function (val, name, index) {
    return _c('li', {
      key: val
    }, [_vm._v(" " + _vm._s(index) + ". " + _vm._s(name) + ": " + _vm._s(val) + " ")]);
  }), 0);
};

genFor 函数就是处理 AST 节点上与 v-for 相关的属性,生成对应字符串,该字符串就是最终执行的代码。

// src/compiler/codegen/index.ts

export function genFor(
  el: any,
  state: CodegenState,
  altGen?: Function,
  altHelper?: string
): string {
  const exp = el.for
  const alias = el.alias
  const iterator1 = el.iterator1 ? `,${el.iterator1}` : ''
  const iterator2 = el.iterator2 ? `,${el.iterator2}` : ''

  if (
    __DEV__ &&
    state.maybeComponent(el) &&
    el.tag !== 'slot' &&
    el.tag !== 'template' &&
    !el.key
  ) {
    state.warn(
      `<${el.tag} v-for="${alias} in ${exp}">: component lists rendered with ` +
        `v-for should have explicit keys. ` +
        `See https://v2.vuejs.org/v2/guide/list.html#key for more info.`,
      el.rawAttrsMap['v-for'],
      true /* tip */
    )
  }

  el.forProcessed = true // avoid recursion
  return (
    `${altHelper || '_l'}((${exp}),` +
    `function(${alias}${iterator1}${iterator2}){` +
    `return ${(altGen || genElement)(el, state)}` +
    '})'
  )
}

v-for 总结

通过阅读源码,可知道 v-for 可以对数组、字符串、数字和对象进行遍历。当遍历的是数字是,他会把模板重复对应次数。

v-for 里面做了异常处理,当传入了不属于数组、字符串、数字和对象的值时,v-for 渲染的是一个空数组。

Vue 模板经过解析(parse),生成 AST 树(抽象语法树),然后由 parseFor 解析 AST 元素上 v-for 指令表达式中相关信息,并将其存储到 AST 的属性中,然后 Vue 编译器会对整个 AST 树做优化(optimize),优化的过程实际上就是深度遍历这个 AST 树,去检测他的每一棵子树是不是静态节点,最后就是把优化后的 AST 树转换成可执行代码,v-for 的 AST 元素会传入 genFor 函数,最终生成执行 renderList 函数的代码。

所以说,v-for 指令本质上是执行 Vue 内部的 renderList 函数。

总结

v-ifv-for 都是 Vue 提供的指令。

v-if 指令最终会被编译为三元表达式,v-for 指令最终被编译为内部的 renderList 函数执行。

Vue 提供的指令是在模板中使用的,Vue 模板经过解析(parse),生成 AST 树(抽象语法树)。

对于 v-if 来说,会由 addIfCondition 函数收集 v-if 的 AST 元素上的条件表达式和虚拟节点信息,存储到 AST 元素对象的 ifConditions 数组中,然后 Vue 编译器会对整个 AST 树做优化(optimize),优化的过程实际上就是深度遍历这个 AST 树,去检测它的每一颗子树是不是静态节点,最后就是把优化后的 AST 树转换成可执行的代码,v-if 的 AST 元素会传入 genIf 函数,最终生成三元表达式。

对于 v-for 来说,会由 parseFor 解析 AST 元素上 v-for 指令表达式中相关信息,并将其存储到 AST 的属性中。然后跟 v-if 一样,进入 AST 树的优化(optimize)阶段,最终进入代码生成阶段,v-for 的 AST 元素会传入 genFor 函数,最终生成执行 renderList 函数的代码。v-for 指令本质上是执行 Vue 内部的 renderList 函数。