v-for的原理分析

大纲

前言

  • 使用parse解析模板生成astv-for相关的属性;
  • 使用generate,结合ast生成函数文本(code),包含v-for的函数文本是_l(/* ... */);
  • 结合code构造render_watcher.update(),从而渲染v-for元素。

接下来使用一下例子结合源码进行学习:

1
2
3
4
5
6
7
8
9
10
11
12
13
<main id="app">
<dl v-for="(name, idx) in names" :key="idx">
<dt>name:</dt>
<dd>{{name}}</dd>
</dl>
</main>
<script>
const vm = new Vue({
data: {
names: ['isaac', 'frank', 'rick'],
}
}).$mount('#app');
</script>

v-for的函数文本

回到顶部

解析模板的入口vue/src/compiler/index.js

astparse返回,所以先深入去parse是怎么生成v-forast!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
export const createCompiler = createCompilerCreator(function baseCompile (
template: string,
options: CompilerOptions
): CompiledResult {
// parse 生产 ast
log('ast:', ast);
const ast = parse(template.trim(), options)
if (options.optimize !== false) {
optimize(ast, options)
}
// generate 生产 渲染用的函数文本
const code = generate(ast, options)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
})

解析出ast

解析视图模板主要由parseHTML函数实现,而这个函数是比较长,parseHTMLv-for相关信息的解析,先说明用到的函数,以及对应的作用:

  1. const startTagMatch = parseStartTag(),parseStartTag是解析开始标签,主要是解析:a. 开始标签这段文本在整个html文本的开始和结束位置,b. 标签内的属性文本的位置,比如v-for="(name, idx) in names"的开始和结束位置。
  2. handleStartTag(startTagMatch),根据位置信息进一步接续出属性值,比如{ name: 'v-for', value: '(name, idx) in names' }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    /**
    * Convert HTML string to AST.
    */
    export function parse (
    template: string,
    options: CompilerOptions
    ): ASTElement | void {
    // ...
    parseHTML(template, {
    // ...
    start (tag, attrs, unary, start, end) {
    // ...
    let element: ASTElement = createASTElement(tag, attrs, currentParent)
    log('element:', element);
    // ...
    }
    // ...
    });

    // ...
    return root
    }

    export function parseHTML (html, options) {
    // ...
    while (html) {
    // ...
    // Start tag: 记录属性文本在争端元素字符串开始和结束的位置
    const startTagMatch = parseStartTag()
    if (startTagMatch) {
    // 根据 parseStartTag 解析出来的位置信息,进一步将文本解析成对象解构的属性
    handleStartTag(startTagMatch)
    // ...
    }
    }

    // advance(推进),更新html文本
    function advance (n) {
    index += n
    html = html.substring(n)
    }

    // 1. 找出开始标签的start-index和end-index,
    // 比如<span name="isaac"></span>中的开始标签就是<span name="isaac">
    // 2. 找出每个属性文本的始和终index
    function parseStartTag () {
    const start = html.match(startTagOpen)
    log('html.match(startTagOpen):', html);
    if (start) {
    const match = {
    tagName: start[1],
    attrs: [],
    start: index
    }
    advance(start[0].length)
    let end, attr
    while (
    !(end = html.match(startTagClose))
    && (
    attr = html.match(dynamicArgAttribute)
    || html.match(attribute)
    )
    ) {
    attr.start = index
    advance(attr[0].length)
    attr.end = index
    match.attrs.push(attr)
    }
    if (end) {
    match.unarySlash = end[1]
    advance(end[0].length)
    match.end = index
    return match
    }
    }
    }

    // 根据 parseStartTag, 得到的文职信息,以及属性的匹配信息
    // 将属性信息从文本解析成对象
    function handleStartTag (match) {
    // ...
    for (let i = 0; i < l; i++) {
    const args = match.attrs[i]
    // ...
    const attrItem = {
    name: args[1],
    value: decodeAttr(value, shouldDecodeNewlines)
    }
    log('attrItem:', attrItem)
    /**
    * output:
    * it-1: "attrItem:" {name: "v-for", value: "(name, idx) in names"}
    * it-2: "attrItem:" {name: ":key", value: "idx"}
    */
    attrs[i] = attrItem;
    // ...
    }

    // log('stack:', JSON.parse(JSON.stringify(stack)));
    if (options.start) {
    log('attrs:', JSON.parse(JSON.stringify(attrs)));
    options.start(tagName, attrs, unary, match.start, match.end)
    }
    }
    }

    const encodedAttr = /&(?:lt|gt|quot|amp|#39);/g
    const encodedAttrWithNewLines = /&(?:lt|gt|quot|amp|#39|#10|#9);/g
    function decodeAttr (value, shouldDecodeNewlines) {
    const re = shouldDecodeNewlines ? encodedAttrWithNewLines : encodedAttr
    return value.replace(re, match => decodingMap[match])
    }
  • log('attrs:', JSON.parse(JSON.stringify(attrs)));

  • log('element:', element)

  • log('ast:', ast)

根据ast解析出函数文本

path: vue/src/compiler/codegen/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
export function generate (
ast: ASTElement | void,
options: CompilerOptions
): CodegenResult {
const state = new CodegenState(options)
const code = ast ? genElement(ast, state) : '_c("div")'
return {
render: `with(this){return ${code}}`,
staticRenderFns: state.staticRenderFns
}
}

export function genElement (el: ASTElement, state: CodegenState): string {
// ...

if (el.staticRoot && !el.staticProcessed) {
// ...
} else if (el.for && !el.forProcessed) {
const forCode = genFor(el, state);
log('forCode', forCode);
// output: "forCode" "_l((names),function(name,idx){return _c('dl',{key:idx},[_c('dt',[_v("name:")]),_v(" "),_c('dd',[_v(_s(name))])])})"
return forCode;
} else {
// ...
}
}

path: vue/src/compiler/codegen/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
export function genFor (
el: any,
state: CodegenState,
altGen?: Function,
altHelper?: string
): string {
const log = (...rest) => console.log(Date.now(), `genFor-${rest.shift()}`, ...rest);
// exp这名变量名也提醒我们:被遍历的目标,可以直接是变量名,也可以是一段可执行的语句
const exp = el.for
// 成员临时别名
const alias = el.alias
// <div v-for="(value, name, index) in object">,中的 name
const iterator1 = el.iterator1 ? `,${el.iterator1}` : ''
// <div v-for="(value, name, index) in object">,中的 index
const iterator2 = el.iterator2 ? `,${el.iterator2}` : ''
// ..

el.forProcessed = true // avoid recursion
return `${altHelper || '_l'}((${exp}),` +
`function(${alias}${iterator1}${iterator2}){` +
// genElement递归生成
`return ${(altGen || genElement)(el, state)}` +
'})'
}
1
2
3
4
5
6
7
renderList((names), function(name,idx) {
return _c('dl', {key:idx}, [
_c('dt', [createTextVNode("name:")]),
createTextVNode(" "),
_c('dd', [createTextVNode(toString(name))])
])
})

renderList的实现

回到顶部

由上面知道最后v-forhtml段落最后被解析出来的函数文本:
解析v-for模板的函数文本

1
2
3
4
5
6
7
_l((names), function(name,idx) {
return _c('dl', {key:idx}, [
_c('dt', [_v("name:")]),
_v(" "),
_c('dd', [_v(_s(name))])
])
})

全局搜索一下_l就可以找到:

1
2
3
4
5
export function installRenderHelpers (target: any) {
// ...
target._l = renderList
// ...
}

renderList函数是vm._l的实现,它的功能是遍历v-for="item in list"中的list,list可以有多种不同的类型!注意遍历是这个函数功能,元素的渲染则是依赖renderList函数的第二个参数:ender: (val: any, keyOrIndex: string | number, index?: number) => VNode

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
import { isObject, isDef, hasSymbol } from 'core/util/index'

/**
* Runtime helper for rendering v-for lists.
*/
export function renderList (
// 遍历的目标
val: any,
// 渲染函数
render: (val: any, keyOrIndex: string | number, index?: number) => VNode
): ?Array<VNode> {
let ret: ?Array<VNode>, i, l, keys, key
// 1. 遍历一个数组
// 2. 遍历一个字符串
if (Array.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)
}
}
// 3. 循环 val 次
else if (typeof val === 'number') {
ret = new Array(val)
for (i = 0; i < val; i++) {
ret[i] = render(i + 1, i)
}
}
// 4. 遍历一个对象
else if (isObject(val)) {
// 4.1 遍历迭代器
/**
* 以下内置类型拥有默认的@@iterator方法:
* Array.prototype[@@iterator]()
* TypedArray.prototype[@@iterator]()
* String.prototype[@@iterator]()
* Map.prototype[@@iterator]()
* Set.prototype[@@iterator]()
*/
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()
}
}
// 4.2 遍历常规对象
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: any)._isVList = true
return ret
}

由上面的代码可以知道,v-for可以遍历以下几种类型

  1. 遍历数组
  2. 遍历类数组的字符串
  3. 循环指定次数
  4. 遍历迭代器
  5. 遍历常规对象

遍历迭代器可能用得比较少,下面有个不算很好的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<main id="app">
<ul>
<h3>myIterable: </h3>
<li v-for="(item, key) in myIterable">{{key}}: {{item}}</li>
</ul>
</main>
<script>
const vm = new Vue({
data: {
myIterable: (() => {
var myIterable = {}
myIterable[Symbol.iterator] = function* () {
yield 1;
yield 2;
yield 3;
};
return myIterable;
})()
}
}).$mount('#app');
</script>

迭代器的详细分析参考:什么是迭代器?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
parseHTML(template, {
// ...
start (tag, attrs, unary, start, end) {
// ...
let element: ASTElement = createASTElement(tag, attrs, currentParent)
// ...
else if (!element.processed) {
// structural directives
processFor(element)
// ..
}
}
// ...
});

总结

回到顶部

  • v-for视图解析到渲染成html文段的过程
  1. 使用parse方法解析视图模板,生成ast,其中主要的三个函数是: a. parseStartTag解析属性等主要信息的位置,b. handleStartTag解析属性,c. createASTElement根据解析出的属性等生成元素的ast;
  2. 使用generate将ast转化成函数文本,_lrenderlist)即是v-for视图的文本函数,其中主要函数是genElement,可递归生成后代元素的函数文本;
  3. 函数文本作为render-watcher.update方法主逻辑
  • renderlist中可以看出v-for可以遍历以下几种类型
  1. 遍历数组
  2. 遍历类数组的字符串
  3. 循环指定次数
  4. 遍历迭代器
  5. 遍历常规对象