Vue源码解析17-v-if的处理

parse

processIf用于处理v-if

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function processIf(el) {
const exp = getAndRemoveAttr(el, 'v-if'); // 获取节点上的v-if属性值
if (exp) {
el.if = exp; // 'condition'
addIfCondition(el, {
exp: exp,
block: el,
});
} else {
if (getAndRemoveAttr(el, 'v-else') != null) {
el.else = true;
}
const elseif = getAndRemoveAttr(el, 'v-else-if');
if (elseif) {
el.elseif = elseif; // "condition2"
}
}
}

我们照常用一个示范html帮助理解:

1
2
3
4
5
<div id="app">
<div v-if="condition">this is v-if</div>
<div v-else-if="condition2">this is v-else-if</div>
<div v-else>this is v-else</div>
</div>

上面这段html会调用processIf3 次,每次的差别在于attrsMap里的属性值:

1
2
3
4
5
6
7
8
9
{
attrsList:[],
attrsMap: {
'v-if': 'condition'
// 或者 'v-else-if': "condition2"
// 或者 'v-else': ""
},
tag: 'div',
}

都处理完后,el上会多出 3 个属性值:

1
2
3
4
5
6
{
if: 'condition',
elseif: 'condition2',
else: true,

}

addIfCondition用于给el.ifConditions数组塞入新元素:

1
2
3
4
5
6
export function addIfCondition(el: ASTElement, condition: ASTIfCondition) {
if (!el.ifConditions) {
el.ifConditions = [];
}
el.ifConditions.push(condition);
}

细心的人可能发现了只有if分支才改变了el.ifConditions,另外两个分支应该也会有类似的操作,在什么地方做的呢?

在调用完processIf后,还有一个地方调用了processIfConditions,它也会操作el.ifConditions

1
2
3
4
5
6
7
8
9
10
11
12
if (currentParent && !element.forbidden) {
// 在处理到v-else-if and v-else节点时,需要将其和v-if节点结合起来,
// 通过在v-if节点上的ifConditions数组,来最终决定渲染哪个节点
if (element.elseif || element.else) {
processIfConditions(element, currentParent);
} else if (element.slotScope) {
// 作用域插槽处理略去。。。
} else {
currentParent.children.push(element);
element.parent = currentParent;
}
}

很明显我们在处理到v-else-ifv-else时,processIfConditions都会被调用。另外注意这种情况下是不会把当前元素计入currentParent.children的,只有v-if节点才被计入了。看看processIfConditions

1
2
3
4
5
6
7
8
9
10
function processIfConditions(el, parent) {
const prev = findPrevElement(parent.children); // 找到v-else-if或v-else前面的v-if节点
if (prev && prev.if) {
// 添加prev.ifConditions数组元素
addIfCondition(prev, {
exp: el.elseif,
block: el,
});
}
}

findPrevElement用来在前面找到紧挨着v-else-ifv-else的那个节点:

1
2
3
4
5
6
7
8
9
10
11
// 找到children中第一个element节点
function findPrevElement(children: Array<any>): ASTElement | void {
let i = children.length;
while (i--) {
if (children[i].type === 1) {
return children[i];
} else {
children.pop();
}
}
}

最终经过parse处理,我们的AST节点上就有了所有分支的相关属性:

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
{
type: 1,
tag: 'div',
plain: false,
children: [{
type: 1,
tag: 'p',
children: [{
text: 'this is v-if',
type: 3
}]
if: 'condition',
ifConditions: [{
exp: "condition",
block: {
type: 1,
tag: 'p',
children: [{
text: 'this is v-if',
type: 3
}],
if: 'condition',
ifConditions: [],
plain: true
}
}, {
exp: "condition2",
block: {
type: 1,
tag: 'p',
children: [{
text: 'this is v-else-if',
type: 3
}],
elseif: 'condition2',
plain: true
}
}, {
exp: undefined,
block: {
type: 1,
tag: 'p',
children: [{
text: 'this is v-else',
type: 3
}],
else: true,
plain: true
}
}]
}]
}

注意根节点的children只有一个元素,这与我们上面讲的一致,只有v-if才进入了父元素的children中。Vue template的一个细节就是有这个处理得出的,后面会说。

generate

处理v-if的是genIf函数:

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
export function genIf(el: any, state: CodegenState, altGen?: Function, altEmpty?: string): string {
el.ifProcessed = true; // avoid recursion
return genIfConditions(el.ifConditions.slice(), state, altGen, altEmpty);
}

// 循环处理ifConditions里面的每一个元素,直到找到exp返回true的元素。
function genIfConditions(conditions: ASTIfConditions, state: CodegenState, altGen?: Function, altEmpty?: string): string {
if (!conditions.length) {
return altEmpty || '_e()'; // _e: createEmptyVNode
}

const condition = conditions.shift();
if (condition.exp) {
// exp确实设置了,如"value === 1" 而不是undefined或空串,
return `(${condition.exp})?${genTernaryExp(condition.block)}:${genIfConditions(conditions, state, altGen, altEmpty)}`;
} else {
return `${genTernaryExp(condition.block)}`;
}

// v-if with v-once should generate code like (a)?_m(0):_m(1)
function genTernaryExp(el) {
// 在genOnce中也有类似的设置:若v-once上同时存在v-if,则优先调用genIf
return altGen ? altGen(el, state) : el.once ? genOnce(el, state) : genElement(el, state);
}
}

注释很详细,我们直接看最终生成的render字符串:

1
2
3
4
5
6
7
`
with (this) {
return _c('div', { attrs: { id: 'app' } }, [
condition ? _c('div', [_v('this is v-if')]) : condition2 ? _c('div', [_v('this is v-else-if')]) : _c('div', [_v('this is v-else')]),
]);
}
`;

没有涉及新的帮助函数,可以很清楚的看到核心是一个嵌套的三目运算,最终只有条件成立的那个分支才会生成对应的vnode

template 小细节

通常情况下,我们的Vue组件只能有一个根元素,但是使用v-if可以有多个根元素分支:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<div id="app">
<my-comp></my-comp>
</div>
<script type="text/javascript">
var vm = new Vue( {
el: '#app',
components: {
myComp: {
data()
{
return {
condition: true,
condition2: false
}
},
template: `
<div v-if="condition">this is v-if</div>
<div v-else-if="condition2">this is v-else-if</div>
<div v-else>this is v-else</div>
`,
}
}
} )
</script>

正是因为parse阶段处理v-if时的特殊代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
if (!root) {
root = element;
} else if (!stack.length) {
if (root.if && (element.elseif || element.else)) {
addIfCondition(root, {
exp: element.elseif,
block: element,
});
}
}

if (currentParent && !element.forbidden) {
if (element.elseif || element.else) {
processIfConditions(element, currentParent);
} else {
currentParent.children.push(element);
element.parent = currentParent;
}
}

currentParentchildren中只会有v-if分支的节点,v-else-ifv-else节点都会放到el.ifConditions中,只在条件成立时才渲染出来。