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-forhtml段落最后被解析出来的函数文本:
解析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可以遍历以下几种类型
- 遍历数组
- 遍历类数组的字符串
- 循环指定次数
- 遍历迭代器
- 遍历常规对象