- 使用
parse解析模板生成ast,v-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
ast由parse返回,所以先深入去parse是怎么生成v-for的ast!
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 { log('ast:', ast); const ast = parse(template.trim(), options) if (options.optimize !== false) { optimize(ast, options) } const code = generate(ast, options) return { ast, render: code.render, staticRenderFns: code.staticRenderFns } })
|
解析出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
|
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) { const startTagMatch = parseStartTag() if (startTagMatch) { handleStartTag(startTagMatch) } }
function advance (n) { index += n html = html.substring(n) }
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 } } }
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)
attrs[i] = attrItem; }
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)));
TODO:补充打印图片
log('element:', element)
TODO:补充打印图片
log('ast:', ast)
TODO:补充打印图片
根据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); 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); const exp = el.for const alias = el.alias const iterator1 = el.iterator1 ? `,${el.iterator1}` : '' const iterator2 = el.iterator2 ? `,${el.iterator2}` : ''
el.forProcessed = true return `${altHelper || '_l'}((${exp}),` + `function(${alias}${iterator1}${iterator2}){` + `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'
export function renderList ( val: any, render: (val: any, keyOrIndex: string | number, index?: number) => VNode ): ?Array<VNode> { let ret: ?Array<VNode>, i, l, keys, key 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) } } else if (typeof val === 'number') { ret = new Array(val) for (i = 0; i < val; i++) { ret[i] = render(i + 1, i) } } else if (isObject(val)) {
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() } } 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 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) { processFor(element) } } });
|
总结
- 使用
parse方法解析视图模板,生成ast,其中主要的三个函数是: a. parseStartTag解析属性等主要信息的位置,b. handleStartTag解析属性,c. createASTElement根据解析出的属性等生成元素的ast; - 使用
generate将ast转化成函数文本,_l(renderlist)即是v-for视图的文本函数,其中主要函数是genElement,可递归生成后代元素的函数文本; - 函数文本作为render-watcher.update方法主逻辑
- 从
renderlist中可以看出v-for可以遍历以下几种类型
- 遍历数组
- 遍历类数组的字符串
- 循环指定次数
- 遍历迭代器
- 遍历常规对象