本篇文章,我们主要讲解v-for
指令的处理流程。v-for
是我们最常用的指令之一,我们从一个例子入手,详细的看一下Vue
中对它的处理流程。
<div id="app">
<p v-for="(value, key, index) in object">{{ index }}. {{ key }} : {{ value }}</p>
</div>
<script type="text/javascript">
var vm = new Vue({
el: '#app',
data: {
object: {
height: '178cm',
weight: '80kg',
gender: 'male',
address: 'BeiJing'
}
}
})
</script>
还是从src/compiler/parse/index.js
文件入手,在start
函数中,对于v-for
指令,我们通过processFor
方法来进行解析:
function processFor (el) {
let exp
if ((exp = getAndRemoveAttr(el, 'v-for'))) {
const inMatch = exp.match(forAliasRE)
if (!inMatch) {
process.env.NODE_ENV !== 'production' && warn(
`Invalid v-for expression: ${exp}`
)
return
}
el.for = inMatch[2].trim()
const alias = inMatch[1].trim()
const iteratorMatch = alias.match(forIteratorRE)
if (iteratorMatch) {
el.alias = iteratorMatch[1].trim()
el.iterator1 = iteratorMatch[2].trim()
if (iteratorMatch[3]) {
el.iterator2 = iteratorMatch[3].trim()
}
} else {
el.alias = alias
}
}
}
getAndRemoveAttr
从字面上我们就猜得到,它的功能是删除v-for
属性,并返回该属性对应的值。这里exp
的值为(value, key, index) in object
。之前我们提到过forAliasRE
:
export const forAliasRE = /(.*?)\s+(?:in|of)\s+(.*)/
从正则我们知道v-for
中,使用in
或者of
是完全一样的。匹配之后,inMatch
的值为["(value, key, index) in object", "(value, key, index)", "object", index: 0, input: "(value, key, index) in object"]
。
所以el.for
中保存的就是我们要遍历的对象或数组或数字或字符串。
再来看forIteratorRE
:
export const forIteratorRE = /\((\{[^}]*\}|[^,]*),([^,]*)(?:,([^,]*))?\)/
我们v-for
可以有如下形式:
v-for="item in items"
v-for="(item, index) in items"
v-for="(value, key, index) in object"
我们的例子中,是最全的一种,其中value
是属性值、key
是属性名、index
是索引值。
所以,最终处理完ast
中添加了如下属性:
el.alias = value
el.iterator1 = key
el.iterator2 = index
最终经过静态内容处理之后的p
标签对应ast
结构为:
{
alias: "value",
attrsList: [],
attrsMap: {v-for: "(value, key, index) in object"},
children: [{
expression: "_s(index)+". "+_s(key)+" : "+_s(value)",
text: "{{ index }}. {{ key }} : {{ value }}",
type: 2,
static: false
}],
for: "object",
iterator1: "key",
iterator2: "index",
plain: true,
tag: "p",
type: 1,
static: false,
staticRoot: false
}
接着,就是根据ast
结果,生成对应的render
字符串。
打开src/compiler/codegen/index.js
文件,这回我们会走到genFor
函数中,
function genFor (el: any): string {
const exp = el.for
const alias = el.alias
const iterator1 = el.iterator1 ? `,${el.iterator1}` : ''
const iterator2 = el.iterator2 ? `,${el.iterator2}` : ''
if (
process.env.NODE_ENV !== 'production' &&
maybeComponent(el) && el.tag !== 'slot' && el.tag !== 'template' && !el.key
) {
warn(
`<${el.tag} v-for="${alias} in ${exp}">: component lists rendered with ` +
`v-for should have explicit keys. ` +
`See https://vuejs.org/guide/list.html#key for more info.`,
true /* tip */
)
}
el.forProcessed = true // avoid recursion
return `_l((${exp}),` +
`function(${alias}${iterator1}${iterator2}){` +
`return ${genElement(el)}` +
'})'
}
这里if
块是开发环境做一些校验。如果是自定义元素且不是slot
和template
,则必须有el.key
。
最终返回的拼接后的字符串是一个_l
函数,其中第一个参数是el.for
即object
,第二个参数是一个函数,函数的参数是我们的三个变量value
、key
、index
。该函数返回值中再次调用genElement
生成p
元素的render
字符串。
最终生成的render
函数字符串为:
"_c('div',{attrs:{"id":"app"}},_l((object),function(value,key,index){return _c('p',[_v(_s(index)+". "+_s(key)+" : "+_s(value))])}))"
前面提到过,_c
是创建一个vnode
对象、_v
是创建一个vnode
文本结点,这些我们在vnode
中详细讲解,这里我们重点说一些_l
。从render.js
中,我们知道它对应的函数就是src/core/instance/render-helpers/render-list.js
中的renderList
方法。
export function renderList (
val: any,
render: () => VNode
): ?Array<VNode> {
let ret, 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)) {
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)
}
}
return ret
}
我们这里传入的val
就是object
,render
就是生成p
段落的render
函数。
代码中的三段if
判断,是因为我们v-for
可以遍历的不止数组和对象,还有数字和字符串。
最终返回的ret
是一个VNode
数组,每一个元素都是一个p
标签对应的VNode
。
从上面的分析中,我们也可以看出v-for
影响的范围只是在生成VNode
对象生成的个数,而对VNode
内部没有影响。