coverPiccoverPic

v-for 编译时发生了什么(v2)

前言

还是这幅图:

graph LR
O[baseCompile] -->|"①"| B[parse\n生成抽象语法树]
A[template 模板]-->|args|B-->|return|C[ast]
O-->|"②"|D[optimize\n标记静态节点]
C-->|args|D
C-->|args|E
O-->|"③"|E["generate\n生成 render 函数"]-->|return|F[render]

分析v-for到底发生了什么,要从parseoptimizegenerate三个阶段说起。

parse:生成 AST

在 src/compiler/parser/index.ts,parse调用parseHTML时传入了startend的钩子处理标签开始和结束时的 AST 节点,下面标出涉及v-for系列指令的逻辑:

graph LR
C[parse]
C-->|call|D
A[start]-->|args|D
D[parseHTML]-->|标签开始|A
A-->|"v-for"|E[processFor]-->F[parseFor\n处理 v-for 指令]

start

processFor调用parseFor解析v-for指令的内容,然后合并到 AST 节点上:

ts
  1. export function processFor(el: ASTElement) {
  2. let exp
  3. if ((exp = getAndRemoveAttr(el, 'v-for'))) {
  4. const res = parseFor(exp)
  5. if (res) {
  6. extend(el, res)
  7. }
  8. }
  9. }

exp就是v-if的表达式,例如<div v-for="(item, index) in arr"></div>,有表达式(item, index) in arr

extend函数就是把两个对象合并:

ts
  1. export function extend(
  2. to: Record<PropertyKey, any>,
  3. _from?: Record<PropertyKey, any>
  4. ): Record<PropertyKey, any> {
  5. for (const key in _from) {
  6. to[key] = _from[key]
  7. }
  8. return to
  9. }

下面来看parseFor,用正则表达式匹配v-for的内容:

ts
  1. export const forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/
  2. export function parseFor(exp: string): ForParseResult | undefined {
  3. const inMatch = exp.match(forAliasRE)
  4. if (!inMatch) return
  5. const res: any = {}
  6. res.for = inMatch[2].trim()
  7. // ...
  8. }

forAliasRE用于匹配in或者of前后的内容(其实v-for里面是in是可以写成of的,详见文档),例如(item, index) in arr,可以得到:

js
  1. {
  2. 0: "(item, index) in arr",
  3. 1: "(item, index)",
  4. 2: "arr",
  5. groups: undefined,
  6. index: 0,
  7. input: "(item, index) in arr"
  8. }

节点的for字段就是v-for中的数组,例如上面的arr。然后处理v-ifin或者of前面的参数:

ts
  1. export const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/
  2. const stripParensRE = /^\(|\)$/g
  3. export function parseFor(exp: string): ForParseResult | undefined {
  4. // ...
  5. // 去掉括号,(item, index) → item, index
  6. const alias = inMatch[1].trim().replace(stripParensRE, '')
  7. const iteratorMatch = alias.match(forIteratorRE)
  8. if (iteratorMatch) {
  9. // in 或者 of 前面有多个参数
  10. res.alias = alias.replace(forIteratorRE, '').trim()
  11. res.iterator1 = iteratorMatch[1].trim()
  12. if (iteratorMatch[2]) {
  13. res.iterator2 = iteratorMatch[2].trim()
  14. }
  15. } else {
  16. // 只有一个参数的情况
  17. res.alias = alias
  18. }
  19. return res
  20. }

forIteratorRE这个表达式没太看懂,用来匹配第二项以后的参数。以(item, index) in arr为例,res.alias = alias.replace(forIteratorRE, '').trim()就是把后面的参数去掉留下第一个,也就是第一个参数item。匹配结果res.iterator1 = iteratorMatch[1].trim()其实就是v-for中作为下标的第二个参数index,类似的iteratorMatch[2]其实是第三个参数。

Q:有第三个参数吗?
A:像这样,用于遍历对象:

html
  1. <div v-for="(value, key, index) in object">
  2. Current key is {{key}}. Value is {{ value }}. Index is {{index}}.
  3. </div>

key就是对象的键名,value是键值,index就是遍历的次序。

运行完最终返回出去,(value, index) in arr,可以得到:

json
  1. {
  2. "for": "arr",
  3. "alias":"value",
  4. "iterator1":"index",
  5. }

(value, key, index) in object,可以得到:

json
  1. {
  2. "for": "object",
  3. "alias":"value",
  4. "iterator1":"key",
  5. "iterator2":"index"
  6. }

generate:生成 render 函数

src/compiler/codegen/index.ts,涉及到v-for的流程:

graph LR
A[generate]-->|call|B[genElement]-->|"el.for && !el.forProcessed"|C[genFor\n生成函数字符串]-->|call|B

先来看genFor

ts
  1. export function genFor(
  2. el: any,
  3. state: CodegenState,
  4. altGen?: Function,
  5. altHelper?: string
  6. ): string {
  7. const exp = el.for
  8. const alias = el.alias
  9. const iterator1 = el.iterator1 ? `,${el.iterator1}` : ''
  10. const iterator2 = el.iterator2 ? `,${el.iterator2}` : ''
  11. el.forProcessed = true // avoid recursion
  12. return (
  13. `${altHelper || '_l'}((${exp}),` +
  14. `function(${alias}${iterator1}${iterator2}){` +
  15. `return ${(altGen || genElement)(el, state)}` +
  16. '})'
  17. )
  18. }

逻辑比较简单,就用上面解析的节点属性拼成字符串,例如:

ts
  1. <div v-for="(item, index) in arr">
  2. Current Value {{index}} is {{ item }}
  3. </div>

可以生成

ts
  1. `_l((arr),function(item,index){return _c('div',[_v("\n Current Value "+_s(index)+" is "+_s(item)+"\n")])})`

可以看出,_l是一个遍历第一个参数的函数,_s用来取对应的循环变量,这些下次到调用render的时候再说。_c_v之前提到是用来生成标签和文本的。

总结

本文讨论了v-for在模板编译阶段怎么被转换为 AST 节点的属性的,parse通过正则表达式匹配得到v-for中各变量,在generate中,被合成为由l((arr),function(item,index){ ... })的字符串。

0 条评论未登录用户
Ctrl or + Enter 评论
© 2023-2025 LittleRangiferTarandus, All rights reserved.
🌸 Run