「这是我参与2022首次更文挑战的第31天,活动详情查看:2022首次更文挑战」。
一、前情回顾 & 背景
本篇小作文讨论了 Vue 在编译时处理另一个常用指令 —— v-for 的渲染函数生成的方法 genFor,它的主要工作如下:
- 首先明确
genFor是genElement的一个分支流程,用于处理有v-for指令的ast节点; - 标记
el.processed为true,防止genFor递归genElement时进入死循环; - 根据前面
parse阶段所得到的el.for/el.alias/el.iterator1/el.iterator2信息拼接得到v-for的渲染函数代码结构,期间处理生成元素的具体逻辑还是有genElement方法实现的;
本篇小作文将聚焦于另一个 Vue 常用的指令 —— v-if 的处理方法 genFor,从源头上了解一下 v-if 是如何实现条件渲染的;其调用过程为 generate -> genElement -> genIf。
1.1 genElement 中的 genIf 调用:
export function genElement (....): string {
if (...) {
} else if (el.if && !el.ifProcessed) {
// 处理带有 v-if 指令的节点,得到一个三元表达式:condition ? render1 : render2
return genIf(el, state)
} else if (....) {
} else {
return code
}
}
二、genIf
方法位置:src/compiler/codegen/index.js -> function genIf
方法参数:
el,ast节点对象;state,CodegenState实例对象;altGen/altEmpty暂时忽略;
方法作用:
- 标记
el.ifProcessed属性为true防止后面递归调用genElement时进入死循序; - 调用
genIfConditions处理el.ifConditions,生成三元表达式
export function genIf (
el: any,
state: CodegenState,
altGen?: Function,
altEmpty?: string
): string {
// 标记当前节点的 v-if 指令已经被处理过了,避免死循环
el.ifProcessed = true // avoid recursion
// 得到三元表达式,condition ? render1 : render2
// 思考?这里为啥传入的是 el.ifConditions.slice() 这个复制品呢?
return genIfConditions(el.ifConditions.slice(), state, altGen, altEmpty)
}
2.1 el.ifConditions
el.ifConditions 是一个数组,数组项来自两处:
- 阶段的解析
html模板过程中,当parseHTML解析到元素的 开始标签时,会调用processIf方法,processIf会处理v-if,大致代码如下:
parseHTML(template, {
//....
start (tag, attrs, unary, start, end) {
if (...) {
} else if (!element.processed) {
// 给 v-if 的表达式添加到 el.ifConditons
// 初始化 el.elseif / el.else 属性
processIf(element)
}
if (!unary) {
closeElement(element)
}
},
end (...) {
closeEement(element)
})
// processIf 代码
function processIf (el) {
const exp = getAndRemoveAttr(el, 'v-if')
if (exp) {
// 初始化 el.if 然后条件语句即对应渲染的 ast 放到 el.ifConditions
el.if = exp
addIfCondition(el, {
exp: exp,
block: el
})
} else {
if (getAndRemoveAttr(el, 'v-else') != null) {
// 初始化 el.else
el.else = true
}
const elseif = getAndRemoveAttr(el, 'v-else-if')
if (elseif) {
// 初始化 el.elseif
el.elseif = elseif
}
}
}
- 当
parseHTML解析到自闭和标签的闭合/>或 非自闭和标签的闭合标签 如</div>就会调用closeElement方法,closeElement会处理v-else-if和v-else大致代码如下:
function closeElement (element) {
if (!stack.length && element !== root) {
// root v-if 暂时忽略
}
if (currentParent && !element.forbidden) {
if (element.elseif || element.else) {
// 处理 v-else-if / v-else
processIfConditions(element, currentParent)
} else {
}
}
}
// processIfConditions 方法处理 el.elseif / el.else
function processIfConditions (el, parent) {
const prev = findPrevElement(parent.children)
if (prev && prev.if) {
addIfCondition(prev, {
// 这里处理 el.else 时,exp 仍然取自 el.elseif ,
// 所以是个 undefined,而 v-else 本身也没有值,所以 undefined 是符合预期的
exp: el.elseif,
block: el
})
} else if (process.env.NODE_ENV !== 'production') {
warn(
`v-${el.elseif ? ('else-if="' + el.elseif + '"') : 'else'} ` +
`used on element <${el.tag}> without corresponding v-if.`,
el.rawAttrsMap[el.elseif ? 'v-else-if' : 'v-else']
)
}
}
如此一来,el.ifConditons 中包含了带有 v-if、v-else-if、v-else 各个条件下对应的渲染细节;el.ifConditions 中每一项都是一个包含 exp 和 block 的对象:{ exp, block },所谓 exp 就是条件表达式,block 就是满足 exp 条件时要渲染的 ast 节点;
- 以下面新增的三行条件渲染代码为例:
<div id="app">
<div class="staticR">
<article>hahahah</article>
</div>
<input :type="inputType" v-model="inputValue" />
<span v-for="item in someArr" :key="index">{{item}}</span>
+ <div v-if="inputType === 'checkbox'">inputType=checkBox</div>
+ <div v-else-if="inputType === 'radio'">inputType=radio</div>
+ <div v-else>inputType=something-else</div>
{{ msg }}
<some-com :some-key="forProp"></some-com>
<div>someComputed = {{someComputed}}</div>
<div class="static-div">静态节点</div>
</div>
下图即为处理后的 el.ifConditions 的细节部分,
2.2 genIfConditions
方法位置:src/compiler/codegen/index.js -> function genIfConditions
方法参数:
condtions,el.conditions的复制对象state,CodegenState实例对象altGen/altEmpty暂时忽略
方法作用:genIfConditions 的核心作用就是将条件变成三元表达式,形如:
if 条件 ? block1 : block2;
这个就是 v-if 和 v-else,那么有 v-else-if 怎么办?接着套娃就行,形如:
if 条件 : block1 : 满足 else-if 条件 ? block2 : block3;
具体工作如下:
- 处理
conditions 为空时,返回_e()` 这个渲染函数的帮助函数; - 从
conditions中拿出一个condition,判断是否有exp条件,如果有说明是el.if或者el.elseif,这个时候构造三元表达式并返回,此时构造三元表达式的否则部分是要递归调用genIfConditions的,只不过此时的conditions已经是被拿出来一个了,如此循环下去直到conditions被清空,就完成了所有的条件渲染; - 如果没有
exp说明是v-else,直接返回三元表达式;
function genIfConditions (
conditions: ASTIfConditions,
state: CodegenState,
altGen?: Function,
altEmpty?: string
): string {
// 长度为空,则用 _e() 生成一个空节点
if (!conditions.length) {
return altEmpty || '_e()'
}
// 从 conditions 数组中拿出一个条件对象 { exp, block }
// 从这里就可以理解为啥上面 genIf 调用 genIfConditions 是传入的是 el.ifConditions.slice() 这个复制品
// 这是因为不能直接操作 el.ifConditions 这个编译结果,所以就要复制一份出来操作
const condition = conditions.shift()
// 返回值:condition.exp ? 渲染函数1 : 渲染函数2
// 表示条件成立时 执行 渲染函数 1,否则就看看剩下的条件中是否有满足条件的,
// 即递归调用 genIfConditions 直接找到条件成立的元素返回一个三元表达式
if (condition.exp) {
return `(${condition.exp})?${
genTernaryExp(condition.block)
}:${
genIfConditions(conditions, state, altGen, altEmpty)
}`
} else {
// 说明没有 condition.exp 就是 v-else 了
return `${genTernaryExp(condition.block)}`
}
// 返回渲染函数表达式,核心还是递归调用 genElement
function genTernaryExp (el) {
return altGen
? altGen(el, state)
: el.once
? genOnce(el, state)
: genElement(el, state)
}
}
2.3 genIfConditions 返回结果
经过上面的一通操作后最终得到的返回值如下,其整个是个字符串,其中的注释是我加上去的,原本没有这些注释:
"(inputType === 'checkbox') // v-if 条件
? _c('div', [_v("inputType=checkBox")])
: (inputType === 'radio') // v-else-if 条件
? _c('div', [_v("inputType=radio")])
: _c('div', [_v("inputType=something-else")])" // v-else
三、总结
本篇小作文的笔墨放在了 v-if/v-else-if/v-else 这几个条件渲染指令了,看似神秘的条件渲染最终竟然是三元表达式,我说这话多少有点吹牛逼漏风😂😂。条件渲染的实现核心流程如下:
- 生成
ast的parse阶段parseHTML的过程中会将v-if、v-else-if、v-else解析成el.if/el.elseif/el.else,同时会条件和条件成立时渲染的元素组成对象:{ exp, block }push进el.ifConditions中; - 接着就是利用
ast生成render 函数的generate阶段,调用genElement,当判断el.if存在时调用genIf处理条件渲染并标记el.ifProcessed为true防止重复处理;