# v-if 的浅析
与v-show不同,v-if并非在 vue 的运行时作用,而是在编译阶段进行了处理。下面是对这块实现的学习和总结
# 解析阶段
在 vue2 中,当调用 html 解析器 parseHtml 时有相关伪代码如下,对应源码位置是src/complier/parser/index.js
export function parse(template) {
// stack 当前节点ast的栈
const stack = [];
//...
parseHtml(template, {
// 表示处理标签开始节点 如 <div
start(tag, attrs, unary, start, end) {
//...
// element 表示当前 标签开始节点的 ast
let element: ASTElement = createASTElement(tag, attrs, currentParent);
//...
processIf(element);
//...
if (unary) {
closeElement(element);
}
},
end() {
const element = stack[stack.length - 1];
closeElement(element);
},
});
//...
}
function processIf(el) {
//获取v-if属性并将该属性从ast中移除
const exp = getAndRemoveAttr(el, 'v-if');
if (exp) {
el.if = exp;
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;
}
}
}
我们先看processIf的调用,从上面的代码我们可以看出,html 解析器在处理标签开始节点的时候,会同时生成标签开始节点的 ast,并调用processIf进行 ast 的 2 次处理。
processIf的主要作用就是依次判断该 ast 是否含有v-if v-else v-else-if这 3 个指令,同时对 ast 进行编辑修饰。
这里有个比较重要的逻辑是,调用了addIfCondition这个方法。这个方法比较简单,就是对 ast 的ifConditions属性进行编辑和修饰,将v-if可能的block添加到ifConditions这个属性中:
export function addIfCondition(el: ASTElement, condition: ASTIfCondition) {
if (!el.ifConditions) {
el.ifConditions = [];
}
el.ifConditions.push(condition);
}
我们再回头看parseHtml中closeElement的调用逻辑。这个方法会存在 2 种调用情况
- 解析标签开始节点,且这个标签是自闭合标签
- 解析标签结束节点
我们看下这个方法的实现
function closeElement() {
// 说明当前节点是一个和根节点同级别的节点
if (!stack.length && element !== root) {
//判断根节点是否有if 当前节点是否有 else elseif
if (root.if && (element.elseif || element.else)) {
addIfCondition(root, {
exp: element.elseif,
block: element,
});
}else{
//error
}
}
if (currentParent && !element.forbidden) {
// 当前节点存在 elseif else
if (element.elseif || element.else) {
processIfConditions(element, currentParent);
} else {
currentParent.children.push(element);
element.parent = currentParent;
}
}
}
上面对closeElement中对涉及v-if相关逻辑的一个提炼。可以看到,首先判断了当前处理的节点是否是一个和根节点同级别的节点。由于vue2中只允许存在一个根节点,所以必须要满足多个根节点的情况下只能渲染一个的条件。如果不满足条件则直接抛出错误。否则调用addIfCondition将当前节点作为block挂入到根节点的ifConditions属性下面。这里要注意哦,是根节点的ifConditions下面,这个很重要。
接着继续判断如果当前节点有elseorelseif则调用processIfConditions进行处理,同时传入当前父节点作为参数。注意是 当前父节点 不是 当前节点的父节点,其实看下面的条件就知道,else and elseif是不会作为子节点插入到ast树中的。
我们接着看processIfConditions的实现
function processIfConditions (el, parent) {
const prev = findPrevElement(parent.children)
if (prev && prev.if) {
addIfCondition(prev, {
exp: el.elseif,
block: el
})
} else {
//error
}
}
这里首先调用了findPrevElement获取到了当前节点的上一个节点,这个方法就是获取到parent.children的最后一个标签类型的节点,作为当前节点上一个节点prev。然后判断prev是否满足if条件,满足的话就将当前节点作为block添加到prev节点的ifConditions属性中,否则就抛出错误。现在是否理解为什么v-else v-else-if必须要接在v-if的标签后面了嘛!
其实在编译阶段最主要的就是,在处理标签开始节点时给v-if的元素打上标签,并将该元素添加到自身的ifConditions属性中。然后在处理标签结束节点时将v-else v-else-if的元素,添加到对应的v-if元素的ifConditions属性中。
# Code生成阶段
说完了编译阶段,我们来看下codegen过程中发生了什么。源码对应的位置是src/compiler/codegen/index.js codegen的核心函数genElement大概如下
function genElement(el,state){
if(/**/){
//...
}else if(/**/){
//...
}else if(el.if && !el.ifProcessed){
return genIf(el, state)
}else{
///
}
}
可以看到,genElement中一堆判断条件,其中有一条专门为v-if指令提供的,来看看genIf做了些什么
function genIf (el,state) {
el.ifProcessed = true // 打上标记
return genIfConditions(el.ifConditions.slice(), state)
}
genIf的核心就是给当前ast加上一个ifProcessed的标记,防止被重复处理。然后调用genIfConditions方法,并传入当前节点的ifConditions的复制。
我们再看下genIfConditions的代码
function genIfConditions (conditions,state){
if (!conditions.length) {
return '_e()' //空vnode
}
const condition = conditions.shift()
if (condition.exp) {
return `(${condition.exp})?${
//本质是调用genElement
genTernaryExp(condition.block)
}:${
genIfConditions(conditions, state)
}`
} else {
//本质是调用genElement
return `${genTernaryExp(condition.block)}`
}
}
首先判断了conditions是否为空,为空的话就返回一个空vnode的函数调用字符串。然后取出conditions数组中第一个元素condition。第一次调用这个方法时这个condition对应的v-if作用的元素。这里可以仔细思考下一下。
然后判断exp是否存在,这个其实就是v-if v-else-if对应的条件。如果条件存在,则返回一个3元表达式字符串,且调用了genElement生成一个函数字符串和递归调用genIfConditions方法,继续传入conditions。如果条件不存在(v-else对应的情况)则直接调用genElement返回一个函数字符串。
其实到这里v-if的逻辑就分析完了,v-if在经过编译后本质就是根据我们写的条件生成了一个符合条件的3元表达式而已。这样是不是就理解了为什么说v-if条件值为假,元素是不会渲染的了呢!
# 总结
- 在html解析阶段,在解析标签开始节点时处理节点的
v-if绑定,给ast添加标记,将节点push到自身的ifConditions属性中 - 在html解析阶段,在解析标签结束节点时,处理节点的
v-elsev-else-if绑定,给ast添加标记,将节点push到对应的v-if节点的ifConditions属性中 - 在代码生成阶段,判断ast是否有
v-if标记,然后递归的生成ifConditions中block对应的函数字符串,然后返回一个符合预期的3元表达式