vue v3.5版本
v-if 和 v-show 是 Vue.js 中两个常用的指令,用于控制元素的显示和隐藏。尽管它们的功能相似,但它们的实现原理和行为有所不同。以下是它们的源码原理对比:
1. v-if
- 表现:
v-if指令通过条件渲染决定元素是否在 DOM 中存在。如果条件为假,元素不会被渲染到 DOM 中;如果条件为真,元素会被创建和插入到 DOM 中。 - 生命周期:因为元素在条件为假时不会存在于 DOM 中,因此所有与该元素相关的生命周期钩子(如
mounted、updated)也不会被调用。 - 性能开销:由于频繁进出 DOM,会引起较大的性能开销,尤其是在包含复杂结构的情况下。适用于条件不频繁变化的场景。
- 源码实现:在源码层面,
v-if会根据条件的变化添加或删除 DOM 元素。每次条件改变时,Vue 的虚拟 DOM 会重新计算,决定是否需要插入或删除元素。
2. v-show
- 表现:
v-show指令控制元素的 CSSdisplay属性。即使条件为假,元素依然存在于 DOM 中,只是被隐藏。 - 生命周期:与
v-if不同,v-show被隐藏的元素仍然存在于 DOM 中,因此其生命周期钩子(如mounted、updated)会被调用。 - 性能开销:由于仅仅通过修改 CSS 属性来进行显示和隐藏的操作,
v-show在频繁切换的情况下性能表现更好,适用于那些需要频繁切换显示状态的场景。 - 源码实现:
v-show在实现时使用了 JavaScript 修改元素的style.display属性,基于这个属性的值来决定元素的可见性。
总结对比
| 特性 | v-if | v-show |
|---|---|---|
| 渲染行为 | 根据条件渲染(增删 DOM) | 仅改变 CSS 属性(不增删 DOM) |
| 生命周期影响 | 元素不在 DOM 中,生命周期钩子不调用 | 元素在 DOM 中,生命周期钩子调用 |
| 性能开销 | 较高(频繁条件变化时) | 较低(适合频繁切换) |
| 用例场景 | 条件很少变化的情况 | 条件频繁变化的情况 |
选择 v-if 还是 v-show 取决于具体场景:如果需要彻底控制 DOM 结构,则选择 v-if;如果只是简单地控制可见性并且希望保留性能,则选择 v-show。
v-Show源码相关
源码相关路径 packages\runtime-dom\src\directives\vShow.ts
export const vShow: ObjectDirective<VShowElement> & { name?: 'show' } = {
beforeMount(el, { value }, { transition }) {
el[vShowOriginalDisplay] =
el.style.display === 'none' ? '' : el.style.display
if (transition && value) {
transition.beforeEnter(el)
} else {
setDisplay(el, value)
}
},
mounted(el, { value }, { transition }) {
if (transition && value) {
transition.enter(el)
}
},
updated(el, { value, oldValue }, { transition }) {
if (!value === !oldValue) return
if (transition) {
if (value) {
transition.beforeEnter(el)
setDisplay(el, true)
transition.enter(el)
} else {
transition.leave(el, () => {
setDisplay(el, false)
})
}
} else {
setDisplay(el, value)
}
},
beforeUnmount(el, { value }) {
setDisplay(el, value)
},
}
v-if源码相关
在 Vue 3 中总共有四种指令:v-on、v-model、v-show 和 v-if。但是,实际上在源码中,只针对前面三者进行了特殊处理,这可以在 packages/runtime-dom/src/directives看出一下是截图
源码处理相关 packages\compiler-core\src\transforms\vIf.ts
import {
type NodeTransform,
type TransformContext,
createStructuralDirectiveTransform,
traverseNode,
} from '../transform'
import {
type AttributeNode,
type BlockCodegenNode,
type CacheExpression,
ConstantTypes,
type DirectiveNode,
type ElementNode,
ElementTypes,
type IfBranchNode,
type IfConditionalExpression,
type IfNode,
type MemoExpression,
NodeTypes,
type SimpleExpressionNode,
convertToBlock,
createCallExpression,
createConditionalExpression,
createObjectExpression,
createObjectProperty,
createSimpleExpression,
createVNodeCall,
locStub,
} from '../ast'
import { ErrorCodes, createCompilerError } from '../errors'
import { processExpression } from './transformExpression'
import { validateBrowserExpression } from '../validateExpression'
import { CREATE_COMMENT, FRAGMENT } from '../runtimeHelpers'
import { findDir, findProp, getMemoedVNodeCall, injectProp } from '../utils'
import { PatchFlagNames, PatchFlags } from '@vue/shared'
// 定义 `transformIf` 函数, 用于处理 v-if、v-else 和 v-else-if 指令
export const transformIf: NodeTransform = createStructuralDirectiveTransform(
/^(if|else|else-if)$/, // 正则匹配三种指令
(node, dir, context) => {
return processIf(node, dir, context, (ifNode, branch, isRoot) => {
// 动态递增 key 值,以处理相同深度的 v-if/else 连锁
const siblings = context.parent!.children
let i = siblings.indexOf(ifNode)
let key = 0
while (i-- >= 0) {
const sibling = siblings[i]
if (sibling && sibling.type === NodeTypes.IF) {
key += sibling.branches.length // 计算兄弟节点数量
}
}
// 回调函数,当所有子节点被转换后完成
return () => {
if (isRoot) {
ifNode.codegenNode = createCodegenNodeForBranch(
branch,
key,
context,
) as IfConditionalExpression
} else {
// 将当前分支的代码生成节点附加到 v-if 根节点上
const parentCondition = getParentCondition(ifNode.codegenNode!)
parentCondition.alternate = createCodegenNodeForBranch(
branch,
key + ifNode.branches.length - 1,
context,
)
}
}
})
},
)
// 处理 v-if 的核心逻辑
export function processIf(
node: ElementNode, // 当前节点
dir: DirectiveNode, // 指令节点
context: TransformContext, // 转换上下文
processCodegen?: (
node: IfNode, // IF 节点
branch: IfBranchNode, // 分支节点
isRoot: boolean, // 是否为根节点
) => (() => void) | undefined,
): (() => void) | undefined {
// 如果指令不是 'else' 且没有表达式,则报错
if (
dir.name !== 'else' &&
(!dir.exp || !(dir.exp as SimpleExpressionNode).content.trim())
) {
const loc = dir.exp ? dir.exp.loc : node.loc
context.onError(
createCompilerError(ErrorCodes.X_V_IF_NO_EXPRESSION, dir.loc),
)
dir.exp = createSimpleExpression(`true`, false, loc) // 默认设置为 true
}
// 浏览器环境下前缀标识符的处理
if (!__BROWSER__ && context.prefixIdentifiers && dir.exp) {
dir.exp = processExpression(dir.exp as SimpleExpressionNode, context)
}
// 开发环境下的表达式验证
if (__DEV__ && __BROWSER__ && dir.exp) {
validateBrowserExpression(dir.exp as SimpleExpressionNode, context)
}
// 处理 v-if 指令
if (dir.name === 'if') {
const branch = createIfBranch(node, dir) // 创建分支
const ifNode: IfNode = {
type: NodeTypes.IF, // 设置节点类型为 IF
loc: node.loc,
branches: [branch], // 将分支添加到 IF 节点
}
context.replaceNode(ifNode) // 替换当前节点
if (processCodegen) {
return processCodegen(ifNode, branch, true) // 处理代码生成
}
} else {
// 定位相邻的 v-if 节点
const siblings = context.parent!.children
const comments = []
let i = siblings.indexOf(node)
while (i-- >= -1) {
const sibling = siblings[i]
if (sibling && sibling.type === NodeTypes.COMMENT) {
context.removeNode(sibling) // 移除注释节点
__DEV__ && comments.unshift(sibling) // 开发环境下记录注释
continue
}
if (
sibling &&
sibling.type === NodeTypes.TEXT &&
!sibling.content.trim().length
) {
context.removeNode(sibling) // 移除空的文本节点
continue
}
if (sibling && sibling.type === NodeTypes.IF) {
// 检查 v-else 后面是否跟着 v-else-if
if (
dir.name === 'else-if' &&
sibling.branches[sibling.branches.length - 1].condition === undefined
) {
context.onError(
createCompilerError(ErrorCodes.X_V_ELSE_NO_ADJACENT_IF, node.loc),
)
}
// 将当前节点移动到相应的 if 节点的分支
context.removeNode()
const branch = createIfBranch(node, dir)
if (
__DEV__ &&
comments.length &&
// 忽略在 <transition> 直接子节点下的注释
!(
context.parent &&
context.parent.type === NodeTypes.ELEMENT &&
(context.parent.tag === 'transition' ||
context.parent.tag === 'Transition')
)
) {
branch.children = [...comments, ...branch.children] // 将注释添加到分支的子节点
}
// 检查用户是否在不同分支上强制使用相同的 key
if (__DEV__ || !__BROWSER__) {
const key = branch.userKey
if (key) {
sibling.branches.forEach(({ userKey }) => {
if (isSameKey(userKey, key)) {
context.onError(
createCompilerError(
ErrorCodes.X_V_IF_SAME_KEY,
branch.userKey!.loc,
),
)
}
})
}
}
sibling.branches.push(branch) // 将新创建的分支添加到兄弟节点的分支中
const onExit = processCodegen && processCodegen(sibling, branch, false)
// 因为分支已被移除,所以这里要确保遍历
traverseNode(branch, context)
// 调用退出回调
if (onExit) onExit()
// 遍历完成后重置当前节点
context.currentNode = null
} else {
context.onError(
createCompilerError(ErrorCodes.X_V_ELSE_NO_ADJACENT_IF, node.loc),
)
}
break
}
}
}
// 创建 if 分支
function createIfBranch(node: ElementNode, dir: DirectiveNode): IfBranchNode {
const isTemplateIf = node.tagType === ElementTypes.TEMPLATE // 判断是否为模板
return {
type: NodeTypes.IF_BRANCH, // 设置节点类型为 IF_BRANCH
loc: node.loc,
condition: dir.name === 'else' ? undefined : dir.exp, // 条件为条件表达式
children: isTemplateIf && !findDir(node, 'for') ? node.children : [node], // 如果是模板且没有 v-for,子节点为原节点的子节点
userKey: findProp(node, `key`), // 获取用户定义的 key
isTemplateIf,
}
}
// 为分支创建代码生成节点
function createCodegenNodeForBranch(
branch: IfBranchNode, // 分支节点
keyIndex: number, // key 索引
context: TransformContext, // 转换上下文
): IfConditionalExpression | BlockCodegenNode | MemoExpression {
if (branch.condition) {
return createConditionalExpression(
branch.condition, // 条件
createChildrenCodegenNode(branch, keyIndex, context), // 子节点的代码生成节点
// 生成注释节点以结束当前块
createCallExpression(context.helper(CREATE_COMMENT), [
__DEV__ ? '"v-if"' : '""',
'true', // 生成注释节点
]),
) as IfConditionalExpression
} else {
return createChildrenCodegenNode(branch, keyIndex, context) // 如果没有条件,直接返回子节点
}
}
// 创建分支的子节点代码生成节点
function createChildrenCodegenNode(
branch: IfBranchNode,
keyIndex: number,
context: TransformContext,
): BlockCodegenNode | MemoExpression {
const { helper } = context
const keyProperty = createObjectProperty(
`key`,
createSimpleExpression(
`${keyIndex}`, // 创建 key 属性
false,
locStub, // 使用空位置
ConstantTypes.CAN_CACHE, // 标记为可以缓存
),
)
const { children } = branch
const firstChild = children[0]
const needFragmentWrapper =
children.length !== 1 || firstChild.type !== NodeTypes.ELEMENT // 判断是否需要片段包装
if (needFragmentWrapper) {
if (children.length === 1 && firstChild.type === NodeTypes.FOR) {
// 当子节点是 ForNode 时,优化掉嵌套的片段
const vnodeCall = firstChild.codegenNode!
injectProp(vnodeCall, keyProperty, context) // 注入 key 属性
return vnodeCall
} else {
let patchFlag = PatchFlags.STABLE_FRAGMENT // 设置 patch 标志
let patchFlagText = PatchFlagNames[PatchFlags.STABLE_FRAGMENT]
// 检查片段是否只包含一个有效的子节点,其他为注释
if (
__DEV__ &&
!branch.isTemplateIf &&
children.filter(c => c.type !== NodeTypes.COMMENT).length === 1
) {
patchFlag |= PatchFlags.DEV_ROOT_FRAGMENT // 添加开发环境的根片段标志
patchFlagText += `, ${PatchFlagNames[PatchFlags.DEV_ROOT_FRAGMENT]}`
}
// 创建片段节点
return createVNodeCall(
context,
helper(FRAGMENT), // 使用 Fragment 作为调用帮助器
createObjectExpression([keyProperty]), // 设置 key 属性
children, // 子节点
patchFlag, // patch 标志
undefined,
undefined,
true, // 标记为片段
false,
false /* isComponent */,
branch.loc,
)
}
} else {
const ret = (firstChild as ElementNode).codegenNode as
| BlockCodegenNode
| MemoExpression
const vnodeCall = getMemoedVNodeCall(ret) // 获取记忆的 VNode 调用
// 将 createVNode 改为 createBlock
if (vnodeCall.type === NodeTypes.VNODE_CALL) {
convertToBlock(vnodeCall, context) // 转换为块
}
// 注入分支的 key
injectProp(vnodeCall, keyProperty, context)
return ret // 返回生成的节点
}
}
// 检查是否为相同的 key
function isSameKey(
a: AttributeNode | DirectiveNode | undefined,
b: AttributeNode | DirectiveNode,
): boolean {
if (!a || a.type !== b.type) {
return false
}
if (a.type === NodeTypes.ATTRIBUTE) {
if (a.value!.content !== (b as AttributeNode).value!.content) {
return false
}
} else {
// 指令
const exp = a.exp!
const branchExp = (b as DirectiveNode).exp!
if (exp.type !== branchExp.type) {
return false
}
if (
exp.type !== NodeTypes.SIMPLE_EXPRESSION ||
exp.isStatic !== (branchExp as SimpleExpressionNode).isStatic ||
exp.content !== (branchExp as SimpleExpressionNode).content
) {
return false
}
}
return true // 是相同的 key
}
// 获取父条件
function getParentCondition(
node: IfConditionalExpression | CacheExpression,
): IfConditionalExpression {
while (true) {
if (node.type === NodeTypes.JS_CONDITIONAL_EXPRESSION) {
if (node.alternate.type === NodeTypes.JS_CONDITIONAL_EXPRESSION) {
node = node.alternate // 如果是条件表达式,则继续查找
} else {
return node // 返回找到的条件
}
} else if (node.type === NodeTypes.JS_CACHE_EXPRESSION) {
node = node.value as IfConditionalExpression // 如果是缓存表达式,获取其值
}
}
}
除上面源码之外,v-if 和 v-show 都涉及到 Vue 的虚拟 DOM 和 diff 算法,但它们的具体处理方式不同。有兴趣的可以继续深入研究一下。。这里主要说明两者的原理区别;
本质
v-if的处理涉及到虚拟 DOM 的创建和销毁,并且使用 diff 算法来确定需要执行的 DOM 操作。v-show则较为简单,因为它只需要修改元素的显示状态,不涉及 DOM 的添加或移除,从而使得 diff 算法的应用相对较少。