条件渲染
之前的代码解读中,为了简化代码,同时更加抓住主要内容——VNode,我们忽略了属性、条件渲染(v-if)、列表渲染(v-for)等。对属性的处理,其实在之前的笔记中有所提及;下面首先简单记录一下条件渲染。
带着条件渲染看Vue渲染全过程
<div>
<div>Item 0</div>
<div v-if="a > 0">Item 1</div>
<div v-else-if="b > 0">Item 2</div>
<div v-else>Item 3</div>
<div>Item 4</div>
</div>
https://github.com/raphealguo/how-to-learn-vue2-blob/raw/master/figure/2.2.1/ifelse.png
从parse(template)看带条件渲染的AST树的生成
在AST树中, if 、else-if 、else的多个token节点会合成一个节点——if节点里边包含
[{
exp:'if判断条件',
block:<if的ast节点>
},
{
exp:'else-if判断条件',
block:<else-if的ast节点>,
},
{
exp: null,
block:<else的ast节点>
}]
而在parse(template)函数中,有一个processIf(element)函数:
processIf(element)
,它用于处理element上的v-if指令(包含在属性键值对中,有待分离);该函数体内部的getAndRemoveAttr()用于获取并移除指定的属性键值对。
function processIf (el) {
/**exp接收移除的v-if后面的值 */
const exp = getAndRemoveAttr(el, 'v-if')
if (exp) {
el.if = exp
// 为当前节点添加if条件
addIfCondition(el, {
exp: exp,
block: el
})
} else {
// 表示exp为空,处理v-else
if (getAndRemoveAttr(el, 'v-else') != null) {
el.else = true
}
// 继续处理v-else-if
const elseif = getAndRemoveAttr(el, 'v-else-if')
if (elseif) {
el.elseif = elseif
}
}
}
注意到,上述处理v-if条件渲染的代码中,还有一个addIfCondition()函数,它主要用于给element(即要生成的AST节点)添加ifConditions属性:
ifAstElm.ifConditions = [
{exp: 'a > 0', block: item1AstEl },
{exp: 'b > 0', block: item2AstEl },
{exp: null, block: item3AstEl },
]
这也再次印证,AST树中将多个v-if、v-else、v-else-if节点整合在了一起。
生成VNode render时对ifASTElement的处理
ifAstElm中的条件控制语句会变成连续的三元运算操作,运算结果产生一个VNode——v-if、v-else-if、v-else结构只会产生一个VNode。
我们知道,生成AST结构的下一步,是生成VNode-render(渲染VNode树的代码),因此我们把目光转移到compiler/codegen/index.js中,在生成VNode-render的次外层函数genElement(el)中,就有对ifAstElm的处理——genIf(el):
function genIf(el) {
el.ifProcessed = true // 标记已经处理过当前这个if节点了,避免递归死循环
// 将el.ifConditions的浅拷贝传递给函数
return genIfConditions(el.ifConditions.slice())
}
function genIfConditions(conditions) {
if (!conditions.length) {
return '_e()'
}
const condition = conditions.shift() // 因为我们并没有去真正删除 el.ifConditions 队列的元素,所以需要有el.ifProcessed = true来结束递归
if (condition.exp) {
return `(${condition.exp})?${genTernaryExp(condition.block)}:${genIfConditions(conditions)}`
} else {
return `${genTernaryExp(condition.block)}`
}
function genTernaryExp(el) {
return genElement(el)
}
}
代码段中,我们看到生成VNode-render的三元表达式:
if (condition.exp) {
return `(${condition.exp})?${genTernaryExp(condition.block)}:${genIfConditions(conditions)}`
} else {
return `${genTernaryExp(condition.block)}`
}
第一个分支,condition.exp是分支条件,condition.block是分支对应的AST节点(准确来说,condition对象来自前一步生成的AST树节点),采取递归方式,可以得到一个嵌套的三元表达式结构:
_c('div', {}, [ /* 留意children数组只有3个元素 */
_c('div', {}, [ _v("Item 0") ]),
/* 下边这个表达式只会产生一个 VNode 节点 */
(a > 0) ? // if
_c('div', {}, [ _v("Item 1") ]) :
(b > 0) ? // elseif
_c('div', {}, [ _v("Item 2") ]) :
// else
_c('div', {}, [ _v("Item 3") ]),
/* 上边这个表达式只会产生一个 VNode 节点 */
_c('div', {}, [ _v("Item 4") ])
])
第二个分支,表示当前condition.exp为null,即无条件了,对应v-else,为v-if系列中的最后一个分支。
后续步骤
上面已经得到了带有v-if的VNode-render表达式,后续就需要保证其运行正确,即可生成VNode树了。由VNode树到真实DOM树的过程与v-if无关了。
// 得到的VNode render代码示例:(renderCode)
var render = function () {
with (this) {
_c('div', {}, [
_c('div', {}, [ _v("Item 0") ]),
(a > 0) ? // if
_c('div', {}, [ _v("Item 1") ]) :
(b > 0) ? // elseif
_c('div', {}, [ _v("Item 2") ]) :
// else
_c('div', {}, [ _v("Item 3") ]),
_c('div', {}, [ _v("Item 4") ])
])
}
}
列表渲染
流程总览
以如下代码为例,简单看看整体流程示意图:
<ul>
<li v-for="(item, index) in items">{{ item }</li>
</ul>
https://github.com/raphealguo/how-to-learn-vue2-blob/raw/master/figure/2.2.2/for.png
HTML到AST的列表渲染处理
在生成AST节点时,需要提取出HTML模板中的v-for属性值。
function processFor (el) {
let exp
if ((exp = getAndRemoveAttr(el, 'v-for'))) {
// 此时,exp已经保存到了v-for的值,下面需要提取出指定的内容
const inMatch = exp.match(forAliasRE)
// v-for="item in list" => ["item in list", "item", "list"]
// v-for="(item, index) in list" => ["(item, index) in list", "(item, index)", "list"]
// v-for="(value, key, index) in object" => ["(value, key, index) in object", "(value, key, index)", "object"]
if (!inMatch) { // v-for语法有错误的时候,提示编译错误
warn(
`Invalid v-for expression: ${exp}`
)
return
}
// 注意,string.trim()将会从字符串两端删除空白字符
/**el.for:string,保存的是v-for命令值中,in后面的内容(可迭代对象) */
el.for = inMatch[2].trim()
const alias = inMatch[1].trim()
const iteratorMatch = alias.match(forIteratorRE)
if (iteratorMatch) { // v-for="(item, index) in list" 或者 // v-for="(value, key, index) in object"
el.alias = iteratorMatch[1].trim()
el.iterator1 = iteratorMatch[2].trim()
if (iteratorMatch[3]) {
el.iterator2 = iteratorMatch[3].trim()
}
} else {
el.alias = alias // alias = "item"
}
}
}
看懂这段代码的关键之一是明白JavaScript的正则表达式。比如,forAliasRE、forIteratorRE都是匹配模式串。以上面的html模板字符串为例,inMatch、iteratorMatch的内容如下:
// inMatch: Array
[
'(item, index) in list',
'(item, index)',
'list',
index: 0,
input: '(item, index) in list',
groups: undefined
]
// iteratorMatch: Array
[
'(item, index)',
'item',
' index',
undefined,
index: 0,
input: '(item, index)',
groups: undefined
]
由此,我们给出创建出来的AST节点el信息:
// v-for="(item, index) in list"
el.for = 'list' // inMatch[2]
const alias = '(item, index)' // inMatch[1]
el.alias = 'item' // iterator[1]
el.iterator1 = 'index' // iterator[2]
alias,别名,此处用以表示在v-for循环中给可迭代对象的当前遍历项,即它的别名。
从AST到Vnode render
在次外层函数genElement()中,genFor()用以生成列表渲染部分的renderCode:
function genFor(el) {
const exp = el.for
const alias = el.alias
const iterator1 = el.iterator1 ? `,${el.iterator1}` : ''
const iterator2 = el.iterator2 ? `,${el.iterator2}` : ''
if (!el.key) { // v-for 最好声明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 */
)
}
// v-for="(item, index) in list"
// alias = item, iterator1 = index
// v-for="(value, key, index) in object"
// alias = value, iterator1 = key, iterator2 = index
// _l(val, render)
// val = list
// render = function (alias, iterator1, iterator2) { return genElement(el) }
el.forProcessed = true // avoid recursion
return `_l((${exp}),` +
`function(${alias}${iterator1}${iterator2}){` +
`return ${genElement(el)}` +
'})'
}
生成的renderCode即为:
_l((items),function(item, index){
return _c('li', {}, [_v,( _s(item) )])
})
运行时的renderHelpersFunc中的渲染列表函数_l()
上面已经得到了带有列表渲染的renderCode,接下来就需要分析renderCode中的渲染函数_l()了——它是渲染出一个完整VNode列表的关键。
// core/instance/render-helpers/render-list.js
export function renderList (val, render) {
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') { // 支持 v-for="n in 10"
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
}
// core/instance/render.js
Vue.prototype._l = renderList
我们先看看最熟悉的renderCode情况:items是一个可迭代数组,那么函数内部将会创建一个ret数组,ret数组中的每一个元素都是可迭代数组中对应元素被渲染后的结果(单个VNode节点):
_l((items),function(item, index){
return _c('li', {}, [_v,( _s(item) )])
})
- items就是renderList(val, render)函数的第一个参数;
- function(item, index){…}是第二个参数render,注意它返回_c(…)的结果,而_c()用于创建VNode并返回之,因此此时的render返回值为单个VNode。
另外,除了可迭代数组,数字、字符串和对象均可以作为v-for的迭代对象。这里说明一下对象作为迭代对象时,首先取出对象的所有键组成的数组keys,建立与之对应的ret[],ret数组中的每一个元素都是keys中每一个键、对应的值的渲染(render)结果。