前言
还是这幅图:
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到底发生了什么,要从parse、optimize、generate三个阶段说起。
parse:生成 AST
在 src/compiler/parser/index.ts,parse调用parseHTML时传入了start、end的钩子处理标签开始和结束时的 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- export function processFor(el: ASTElement) {
- let exp
- if ((exp = getAndRemoveAttr(el, 'v-for'))) {
- const res = parseFor(exp)
- if (res) {
- extend(el, res)
- }
- }
- }
exp就是v-if的表达式,例如<div v-for="(item, index) in arr"></div>,有表达式(item, index) in arr。
extend函数就是把两个对象合并:
ts- 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
- }
下面来看parseFor,用正则表达式匹配v-for的内容:
ts- export const forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/
- export function parseFor(exp: string): ForParseResult | undefined {
- const inMatch = exp.match(forAliasRE)
- if (!inMatch) return
- const res: any = {}
- res.for = inMatch[2].trim()
- // ...
- }
forAliasRE用于匹配in或者of前后的内容(其实v-for里面是in是可以写成of的,详见文档),例如(item, index) in arr,可以得到:
js- {
- 0: "(item, index) in arr",
- 1: "(item, index)",
- 2: "arr",
- groups: undefined,
- index: 0,
- input: "(item, index) in arr"
- }
节点的for字段就是v-for中的数组,例如上面的arr。然后处理v-if的in或者of前面的参数:
ts- export const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/
- const stripParensRE = /^\(|\)$/g
- export function parseFor(exp: string): ForParseResult | undefined {
- // ...
- // 去掉括号,(item, index) → item, index
- const alias = inMatch[1].trim().replace(stripParensRE, '')
- const iteratorMatch = alias.match(forIteratorRE)
- if (iteratorMatch) {
- // in 或者 of 前面有多个参数
- 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
- }
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
- <div v-for="(value, key, index) in object">
- Current key is {{key}}. Value is {{ value }}. Index is {{index}}.
- </div>
key就是对象的键名,value是键值,index就是遍历的次序。
运行完最终返回出去,(value, index) in arr,可以得到:
json- {
- "for": "arr",
- "alias":"value",
- "iterator1":"index",
- }
(value, key, index) in object,可以得到:
json- {
- "for": "object",
- "alias":"value",
- "iterator1":"key",
- "iterator2":"index"
- }
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- 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}` : ''
- el.forProcessed = true // avoid recursion
- return (
- `${altHelper || '_l'}((${exp}),` +
- `function(${alias}${iterator1}${iterator2}){` +
- `return ${(altGen || genElement)(el, state)}` +
- '})'
- )
- }
逻辑比较简单,就用上面解析的节点属性拼成字符串,例如:
ts- <div v-for="(item, index) in arr">
- Current Value {{index}} is {{ item }}
- </div>
可以生成
ts- `_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){ ... })的字符串。
