v-for的原理分析
大纲
前言
- 使用
parse
解析模板生成ast
,v-for
相关的属性; - 使用
generate
,结合ast
生成函数文本(code
),包含v-for
的函数文本是_l(/* ... */)
; - 结合
code
构造render_watcher.update()
,从而渲染v-for
元素。
接下来使用一下例子结合源码进行学习:
1 | <main id="app"> |
v-for的函数文本
解析模板的入口:vue/src/compiler/index.js
ast
由parse
返回,所以先深入去parse是怎么生成v-for
的ast
!
1 | export const createCompiler = createCompilerCreator(function baseCompile ( |
解析出ast
解析视图模板主要由parseHTML
函数实现,而这个函数是比较长,parseHTML
对v-for
相关信息的解析,先说明用到的函数,以及对应的作用:
const startTagMatch = parseStartTag()
,parseStartTag是解析开始标签,主要是解析:a. 开始标签这段文本在整个html文本的开始和结束位置,b. 标签内的属性文本的位置,比如v-for="(name, idx) in names"
的开始和结束位置。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 | export function generate ( |
path: vue/src/compiler/codegen/index.js
1 | export function genFor ( |
1 | renderList((names), function(name,idx) { |
renderList的实现
由上面知道最后v-for
html段落最后被解析出来的函数文本:
解析v-for模板的函数文本
1 | _l((names), function(name,idx) { |
全局搜索一下_l
就可以找到:
1 | export function installRenderHelpers (target: any) { |
renderList函数是vm._l
的实现,它的功能是遍历v-for="item in list"
中的list,list可以有多种不同的类型!注意遍历是这个函数功能,元素的渲染则是依赖renderList函数的第二个参数:ender: (val: any, keyOrIndex: string | number, index?: number) => VNode
。
1 | import { isObject, isDef, hasSymbol } from 'core/util/index' |
由上面的代码可以知道,v-for
可以遍历以下几种类型
- 遍历数组
- 遍历类数组的字符串
- 循环指定次数
- 遍历迭代器
- 遍历常规对象
遍历迭代器可能用得比较少,下面有个不算很好的例子:
1 | <main id="app"> |
迭代器的详细分析参考:什么是迭代器?
1 | parseHTML(template, { |
总结
- v-for视图解析到渲染成html文段的过程
- 使用
parse
方法解析视图模板,生成ast,其中主要的三个函数是: a. parseStartTag解析属性等主要信息的位置,b. handleStartTag解析属性,c. createASTElement根据解析出的属性等生成元素的ast; - 使用
generate
将ast转化成函数文本,_l
(renderlist
)即是v-for
视图的文本函数,其中主要函数是genElement
,可递归生成后代元素的函数文本; - 函数文本作为render-watcher.update方法主逻辑
- 从
renderlist
中可以看出v-for可以遍历以下几种类型
- 遍历数组
- 遍历类数组的字符串
- 循环指定次数
- 遍历迭代器
- 遍历常规对象