前言
说起vue的v-if和v-show指令,我们多少有些了解,尤其是当讨论它两的区别时,我们可能会脱口而出它们的操作方式不同,v-if是通过操作元素节点的删除和添加来控制相关模块在视图中的显隐,v-show是通过控制style的display属性控制相关模块在视图中的显隐,v-if会导致重排开销较大,v-show不会导致重排,适合用于值变化频繁的场景。
但是当问起v-if、v-show的实现原理,以及如何自己写一个类似的指令,可能大家就比较陌生了。
因此本文通过分析一下v-if、v-show的源码,简单梳理一下v-if、v-show实现的原理,并基于相应的原理,自己手写一个简单的v-if、v-show。
在vue的源码中,对v-if、v-show的功能操作是基于已被编译后的虚拟dom。所谓的虚拟dom可以简单里理解就是一个json对象,通过对象的形式来表达元素标签,至于为什么采用虚拟dom有兴趣的同学可以先自行去学习一下。
而且对于v-if、v-show来说,其核心的部分,除了如何操作dom节点,或者display之外,还有就是如何响应式的响应操作,这个部分相对较为复杂,但其并不影响我们去理解v-if、v-show操作dom节点和样式,因此本文不做讨论。
源码分析
下面我们就v-if、v-show源码做如下分析:
v-if源码(vue2)
/* @flow */
import {
addIfCondition } from 'compiler/parser/index' // 添加if条件
import {
getAndRemoveAttr, addRawAttr } from 'compiler/helpers' // 获取、删除、添加元素属性attribute
// 判断元素标签是否含有v-if、v-else-if、v-else指令
function hasConditionDirective (el: ASTElement): boolean {
for (const attr in el.attrsMap) {
if (/^v\-if|v\-else|v\-else\-if$/.test(attr)) {
return true
}
}
return false
}
// 获取先前条件
function getPreviousConditions (el: ASTElement): Array<string> {
const conditions = []
if (el.parent && el.parent.children) {
for (let c = 0, n = el.parent.children.length; c < n; ++c) {
// $flow-disable-line
const ifConditions = el.parent.children[c].ifConditions
if (ifConditions) {
for (let i = 0, l = ifConditions.length; i < l; ++i) {
const condition = ifConditions[i]
if (condition && condition.exp) {
conditions.push(condition.exp)
}
}
}
}
}
return conditions
}
// 预转换v-if
export function preTransformVIf (el: ASTElement, options: WeexCompilerOptions) {
// 判断元素标签是否含有v-if、v-else-if、v-else指令
if (hasConditionDirective(el)) {
let exp
// 从attrsMap对象中删除v-if属性,并返回v-if属性表达式
const ifExp = getAndRemoveAttr(el, 'v-if', true /* remove from attrsMap */)
// 从attrsMap对象中删除v-else-if属性,并返回v-else-if属性表达式
const elseifExp = getAndRemoveAttr(el, 'v-else-if', true)
// don't need the value, but remove it to avoid being generated as a
// custom directive(不需要该值,但将其删除以避免作为自定义指令生成)
// 从attrsMap对象中删除v-else属性,并返回v-else属性表达式
getAndRemoveAttr(el, 'v-else', true)
if (ifExp) {
// 如果存在v-if属性表达式添加v-if条件
exp = ifExp
addIfCondition(el, {
exp: ifExp, block: el })
} else {
// 如果存在v-else-if属性表达式添加v-else-if条件
elseifExp && addIfCondition(el, {
exp: elseifExp, block: el })
// 获取先前条件(这里猜测是用于v-if语法判断进行错误提示)
const prevConditions = getPreviousConditions(el)
if (prevConditions.length) {
const prevMatch = prevConditions.join(' || ')
exp = elseifExp
? `!(${
prevMatch}) && (${
elseifExp})` // v-else-if
: `!(${
prevMatch})` // v-else
} else if (process.env.NODE_ENV !== 'production' && options.warn) {
options.warn(
`v-${
elseifExp ? ('else-if="' + elseifExp + '"') : 'else'} ` +
`used on element <${
el.tag}> without corresponding v-if.`
)
return
}
}
// 将处理后的v-if、v-else-if、v-else表达式重新添加到该元素属性中
addRawAttr(el, '[[match]]', exp)
}
}
通过上面代码可以了解到v-if的一个大概逻辑就是,先判断元素标签上是否含有v-if类型标签,如果有先把含有v-if的表达式的元素从当前的虚拟dom节点中删除并暂存起来,然后通过js代码if else 函数逻辑,将表达式值为真的元素标签重新添加到要渲染的虚拟dom节点中。
v-show的就相对简单些,重点就是对display的设置,如下:
el.style.display = value ? originalDisplay : 'none'
通过上述代码的分析,相比大家大概理解了v-if和v-show的实现逻辑,以及为什么v-if会导致重排而v-show不会。
原生实现
另外,基于上述原理,本人自己写了一个简单的v-if和v-show指令,不同于vue的虚拟dom,本文是通过js直接操作dom来实现的,其目的也是为了让大家能够有个清洗的认识,其次为了保证整理逻辑不太复杂方便理解,因此本文也没有对v-if、v-else-if、v-else,进行一个很深的逻辑判断,仅仅只是对v-if、v-else-if、v-show功能进行一个简单实现,如图:
具体代码如下:
<!DOCTYPE html>
<html>
<body>
<div v-if="1+1=== 2" htmll="123">我要显示</div>
<div v-if="1+1!==2" htmll="123">我不能显示</div>
<div v-else-if="1+2===3">
我也能显示
</div>
<div v-else-if="false">
我也不能显示
</div>
<div v-show="true">我是show显示</div>
<div v-show="false">我是show隐藏</div>
<script>
// 获取根元素
const el = document.querySelector('body')
// 获取子节点
const nodeList = el.childNodes
// 遍历子节点
for (let i = 0; i < nodeList.length; i++) {
const childNode = nodeList[i]
// 判断子元素是否含有v-if、v-else-if、v-show属性
const has = hasIf(childNode)
// 如果存在v-if、v-else-if、v-show属性,则执行响应操作
if (has) {
// 根据v-if、v-else-if、v-show的值操作相应元素
operateNode(childNode, has)
}
}
// 删除节点
function operateNode(el, attr) {
// 获取v-if、v-else-if、v-show的值
const attrValue = el.attributes.getNamedItem(attr) ? el.attributes.getNamedItem(attr).value : null
// 执行v-if、v-else-if、v-show表达式
const fun = new Function('', 'return (' + attrValue + ')')
const res = fun()
// v-if、v-else-if、v-show的值为真时正常显示
if (res) return
// 非v-show指令时采用删除子节点的方式进行操作
attr !== 'v-show' && el.parentNode && el.parentNode.removeChild(el)
// v-show指令时通过设置display的值控制显隐
attr === 'v-show' && (el.style.display = 'none')
}
// 判断是否含有v-if、v-else-if、v-show属性
function hasIf(el) {
// 子元素没有属性时直接返回
if (!el.attributes) return
const e = el.attributes
for (let i = 0; i < e.length; i++) {
// 通过正则匹配v-if、v-else-if、v-show指令
if (/^v\-if|v\-else\-if|v\-show$/.test(e[i].name)) {
return e[i].name
}
}
}
</script>
</body>
</html>
上述就是本文的内容,如果对你有帮助,请点赞加收藏,如果有什么建议,欢迎留言,或私聊。谢谢!