我的开源库:
- fly-barrage 前端弹幕库,项目官网:https://fly-barrage.netlify.app/,可实现类似于 B 站的弹幕效果,并提供了完整的 DEMO,Gitee 推荐项目;
- fly-gesture-unlock 手势解锁库,项目官网:https://fly-gesture-unlock.netlify.app/,在线体验:https://fly-gesture-unlock-online.netlify.app/,可高度自定义锚点的数量、样式以及尺寸;
前面的 17 篇文章对 Vue 底层的核心运行机制进行了解析,接下来,开始对 Vue 中的众多特性进行针对化的讲解。
首先讲讲大家在工作中经常用到的 v-if、v-else、v-else-if 指令,这些指令是在模板编译阶段实现的。
1,v-if
v-if 的用法如下所示:
<template>
<div id="app">
<h1 v-if="isShow">Hello Vue h1</h1>
</div>
</template>
<script>
export default {
name: 'App',
data(){
return {
isShow: true
}
},
methods: {}
}
</script>
如果 v-if 后面的表达式为 true 的话,h1 标签就会被渲染;
如果 v-if 后面的表达式为 false 的话,h1 标签就不会被渲染;
v-if 的用法大家都很熟悉了,接下来开始研究 v-if 的底层实现原理。
1-1,模板字符串 --> 抽象语法树
首先看看上面例子中的模板字符串生成的抽象语法树是什么样子的。
{
attrs: [{name: "id", value: "\"app\""}],
attrsList: [{name: "id", value: "app"}],
attrsMap: {id: "app"},
children: [{
attrsList: [],
attrsMap: {v-if: "isShow"},
children: [{type: 3, text: "Hello Vue h1", static: true}],
if: "isShow",
ifConditions: [{
exp: "isShow",
block: {type: 1, tag: "h1", attrsList: Array(0), attrsMap: {…}, parent: {…}, …},
}],
ifProcessed: undefined,
parent: {type: 1, tag: "div", attrsList: Array(1), attrsMap: {…}, parent: undefined, …},
plain: true,
static: false,
staticRoot: false,
tag: "h1",
type: 1
}],
parent: undefined,
plain: false,
static: false,
staticRoot: false,
tag: "div",
type: 1
}
上面生成的抽象语法树中,与 v-if 特性有关联的属性有三个,分别是:if、ifConditions、ifProcessed,他们的作用如下所示:
- if:用于标识当前的 AST 节点有没有使用 v-if 特性。
- ifProcessed:用于标识当前 AST 节点的 v-if 特性有没有被处理,防止出现重复处理 v-if 特性的情况。
- ifConditions:该属性是一个对象,里面有两个属性,分别是:exp 和 block,exp 是 v-if 指令后面的表达式,block 是当 exp 表达式为 true 时,应该生成对应代码字符串的 AST 节点。
这三个属性将用于 v-if 相关代码字符串的生成。
1-2,抽象语法树 --> 代码字符串
抽象语法树生成代码字符串是编译器中的代码生成器的内容,与 v-if 相关的代码生成器源码如下所示。
function genElement (el, state) {
// 首次执行到这里,!el.ifProcessed 为 true
// 下次执行到这里,!el.ifProcessed 为 false,因为在 genIf 函数中,el.ifProcessed 被设为了 true
if (el.if && !el.ifProcessed) {
// 用于处理 v-if
return genIf(el, state)
} else {
// component or element
let code
if (el.component) {
code = genComponent(el.component, el, state)
} else {
// 代码会执行到这个分支,生成 _c('h1',[_v(\"Hello Vue h1\")]) 代码字符串
const data = el.plain ? undefined : genData(el, state)
const children = el.inlineTemplate ? null : genChildren(el, state, true)
code = `_c('${el.tag}'${
data ? `,${data}` : '' // data
}${
children ? `,${children}` : '' // children
})`
}
return code
}
}
function genIf (
el,
state,
altGen,
altEmpty
) {
// ifProcessed 属性用于判断 el 的 v-if 有没有被处理过,避免重复处理。
el.ifProcessed = true; // avoid recursion
// 调用 genIfConditions 进行代码字符串的生成
return genIfConditions(el.ifConditions.slice(), state, altGen, altEmpty)
}
function genIfConditions (
conditions,
state,
altGen,
altEmpty
) {
// 在当前的例子中,conditions 的值是
// [{
// exp: "isShow",
// block: {type: 1, tag: "h1", attrsList: Array(0), attrsMap: {…}, parent: {…}, …},
// }]
// 数组中只有一个对象
// 如果 conditions 数组为空的话,则返回 _e()
if (!conditions.length) {
return altEmpty || '_e()'
}
// 从数组中的头部取出数据,在当前的例子中,这一次取出数据之后,conditions 数组就变成了空数组了
var condition = conditions.shift();
// 如果 condition 对象中有 exp 属性的话,则进入此逻辑。
// 注意:v-else 对应的 condition 对象没有 exp 属性
if (condition.exp) {
// 拼接并返回代码字符串:(isShow)?_c('h1',[_v(\"Hello Vue h1\")]):_e()
// _c('h1',[_v(\"Hello Vue h1\")]) 代码字符串由 genTernaryExp 函数生成。
// _e() 代码字符串会再次调用 genIfConditions 函数生成,因为在此例子中,conditions 已经变成了空数组,所以这次调用的 genIfConditions 函数会返回 _e(),对应的逻辑看上面的代码。
// 如果 v-if 和 v-else-if 搭配使用的话,在这里再次调用 genIfConditions 函数的时候 conditions 数组就不是空数组,最终生成的代码字符串是 嵌套的三元表达式 结构
return ("(" + (condition.exp) + ")?" + (genTernaryExp(condition.block)) + ":" + (genIfConditions(conditions, state, altGen, altEmpty)))
} else {
// 该分支逻辑用于生成 v-else AST 节点的代码字符串
return ("" + (genTernaryExp(condition.block)))
}
// v-if with v-once should generate code like (a)?_m(0):_m(1)
// 调用 genTernaryExp 函数生成 _c('h1',[_v(\"Hello Vue h1\")]) 代码字符串,其内部调用 genElement(el, state) 生成该代码字符串
function genTernaryExp (el) {
return altGen
? altGen(el, state)
: el.once
? genOnce(el, state)
: genElement(el, state)
}
}
源码的详细解释都在注释中,大家看注释即可理解,生成的代码字符串如下所示:
with(this){
return _c(
'div',
{attrs:{\"id\":\"app\"}},
[(isShow)?
_c('h1',[_v(\"Hello Vue h1\")])
:_e()
]
)
}
可以发现,v-if 特性生成的代码字符串是一个三元表达式,如果 exp 也就是 isShow 变量为 true 的话,则 render 函数生成的 vnode 将会包含 h1 vnode 节点。如果 isShow 变量为 false 的话,render 函数生成的 vnode 将不会包含 h1 vnode 节点,而是一个注释 vnode 节点作为占位。
1-3,isShow 不同状态下,render 函数生成的 vnode
如果 isShow 为 true 的话,生成的 vnode 节点如下图所示:
app div vnode 对象的 children 属性内有一个 h1 节点的 vnode,该 vnode 树 patch 到页面上,自然会在页面上渲染出 h1 元素。
如果 isShow 为 false 的话,生成的 vnode 节点如下图所示:
可以发现,如果 isShow 的值为 false 生成的 vnode,children 属性中的 vnode 是一个占位用的注释 vnode,这种节点在页面上显示任何内容。
2,v-else
如果上面的 v-if 理解了的话,v-else 也就很简单了,我们以下面的例子开始讨论。
<template>
<div id="app">
<h1 v-if="isShow">Hello Vue h1</h1>
<h2 v-else>Hello Vue h2</h2>
</div>
</template>
<script>
export default {
name: 'App',
data(){
return {
isShow: false
}
},
methods: {}
}
</script>
如果 isShow 为 true 的话,页面渲染 h1 节点,如果 isShow 为 false 的话,页面渲染 h2 节点。
2-1,模板字符串 --> 抽象语法树
生成的抽象语法树如下所示:
与上面生成的抽象语法树唯一的不同点是 ifConditions 数组中有两个对象,第一个 condition 对象对应 v-if="isShow",第二个 condition 对象对应 v-else。
{
attrs: [{name: "id", value: "\"app\""}],
attrsList: [{name: "id", value: "app"}],
attrsMap: {id: "app"},
children: [{
attrsList: [],
attrsMap: {v-if: "isShow"},
children: [{type: 3, text: "Hello Vue h1", static: true}],
if: "isShow",
ifConditions: [{
exp: "isShow",
block: {type: 1, tag: "h1", attrsList: Array(0), attrsMap: {…}, parent: {…}, …},
},{
exp: undefined,
block: {type: 1, tag: "h2", attrsList: Array(0), attrsMap: {…}, parent: {…}, …}
}],
ifProcessed: undefined,
parent: {type: 1, tag: "div", attrsList: Array(1), attrsMap: {…}, parent: undefined, …},
plain: true,
static: false,
staticRoot: false,
tag: "h1",
type: 1
}],
parent: undefined,
plain: false,
static: false,
staticRoot: false,
tag: "div",
type: 1
}
2-2,抽象语法树 --> 代码字符串
生成的代码字符串如下所示:
with(this){
return _c(
'div',
{attrs:{\"id\":\"app\"}},
[(isShow)?
_c('h1',[_v(\"Hello Vue h1\")]):
_c('h2',[_v(\"Hello Vue h2\")])
]
)
}
可以看到 v-if 和 v-else 的组合生成的代码字符串是一个三元表达式,如果 isShow 为 true 的话,生成 h1 的 vnode,如果 isShow 为 false 的话,生成 h2 的 vnode。
2-3,isShow 不同状态下,render 函数生成的 vnode
如果 isShow 为 true 的话,生成的 vnode 节点如下图所示:
app div vnode 对象的 children 属性内有一个 h1 节点的 vnode,该 vnode 树 patch 到页面上,自然会在页面上渲染出 h1 元素。
如果 isShow 为 false 的话,生成的 vnode 节点如下图所示:
app div vnode 对象的 children 属性内有一个 h2 节点的 vnode,该 vnode 树 patch 到页面上,自然会在页面上渲染出 h2 元素。
3,v-else-if
v-else-if 可以搭配 v-if 使用,其底层的本质是生成多层嵌套的三元表达式,我们直接看例子。
<template>
<div id="app">
<h1 v-if="score >= 80">优秀</h1>
<h2 v-else-if="score >= 60">及格</h2>
<h3 v-else>不及格</h3>
</div>
</template>
<script>
export default {
name: 'App',
data(){
return {
score: 60
}
},
methods: {}
}
</script>
如果 score >= 80 的话,页面渲染 h1 标签;
如果 80 > score >= 60 的话,页面渲染 h2 标签;
如果 score < 60 的话,页面渲染 h3 标签;
3-1,模板字符串 --> 抽象语法树
生成的抽象语法树如下所示:
该例子生成的抽象语法树的特点是 ifConditions 数组中有三个对象。第一个 condition 对象对应 v-if="score >= 80",第二个 condition 对象对应 v-else-if="score >= 60",第三个 condition 对象对应 v-else。
{
attrs: [{name: "id", value: "\"app\""}],
attrsList: [{name: "id", value: "app"}],
attrsMap: {id: "app"},
children: [{
attrsList: [],
attrsMap: {v-if: "isShow"},
children: [{type: 3, text: "Hello Vue h1", static: true}],
if: "isShow",
ifConditions: [{
exp: "score >= 80",
block: {type: 1, tag: "h1", attrsList: Array(0), attrsMap: {…}, parent: {…}, …}
},{
exp: "score >= 60",
block: {type: 1, tag: "h2", attrsList: Array(0), attrsMap: {…}, parent: {…}, …}
},{
exp: undefined,
block: {type: 1, tag: "h3", attrsList: Array(0), attrsMap: {…}, parent: {…}, …}
}],
ifProcessed: undefined,
parent: {type: 1, tag: "div", attrsList: Array(1), attrsMap: {…}, parent: undefined, …},
plain: true,
static: false,
staticRoot: false,
tag: "h1",
type: 1
}],
parent: undefined,
plain: false,
static: false,
staticRoot: false,
tag: "div",
type: 1
}
3-2,抽象语法树 --> 代码字符串
生成的代码字符串如下所示:
with(this){
return _c(
'div',
{attrs:{\"id\":\"app\"}},
[(score >= 80)?
_c('h1',[_v(\"优秀\")]):
(score >= 60)?
_c('h2',[_v(\"及格\")]):
_c('h3',[_v(\"不及格\")])])
}
v-if 和 v-else-if 搭配生成的代码字符串是嵌套的三元表达式的结构。如果 score >= 80 的话,生成 h1 的 vnode,如果 80 > score >= 60 的话,生成 h2 的 vnode,如果 score < 60 的话,生成 h3 的 vnode。
3-3,score 不同数值,render 函数生成的 vnode
如果 score >= 80 的话,生成的 vnode 节点如下图所示:
app div vnode 对象的 children 属性内有一个 h1 节点的 vnode,该 vnode 树 patch 到页面上,自然会在页面上渲染出 h1 元素。
如果 80 > score >= 60 的话,生成的 vnode 节点如下图所示:
app div vnode 对象的 children 属性内有一个 h2 节点的 vnode,该 vnode 树 patch 到页面上,自然会在页面上渲染出 h2 元素。
如果 score < 60 的话,生成的 vnode 节点如下图所示:
app div vnode 对象的 children 属性内有一个 h3 节点的 vnode,该 vnode 树 patch 到页面上,自然会在页面上渲染出 h3 元素。