v-if和v-show的源码级别区别

299 阅读5分钟

vue v3.5版本

v-if 和 v-show 是 Vue.js 中两个常用的指令,用于控制元素的显示和隐藏。尽管它们的功能相似,但它们的实现原理和行为有所不同。以下是它们的源码原理对比:

1. v-if

  • 表现v-if 指令通过条件渲染决定元素是否在 DOM 中存在。如果条件为假,元素不会被渲染到 DOM 中;如果条件为真,元素会被创建和插入到 DOM 中。
  • 生命周期:因为元素在条件为假时不会存在于 DOM 中,因此所有与该元素相关的生命周期钩子(如 mountedupdated)也不会被调用。
  • 性能开销:由于频繁进出 DOM,会引起较大的性能开销,尤其是在包含复杂结构的情况下。适用于条件不频繁变化的场景。
  • 源码实现:在源码层面,v-if 会根据条件的变化添加或删除 DOM 元素。每次条件改变时,Vue 的虚拟 DOM 会重新计算,决定是否需要插入或删除元素。

2. v-show

  • 表现v-show 指令控制元素的 CSS display 属性。即使条件为假,元素依然存在于 DOM 中,只是被隐藏。
  • 生命周期:与 v-if 不同,v-show 被隐藏的元素仍然存在于 DOM 中,因此其生命周期钩子(如 mountedupdated)会被调用。
  • 性能开销:由于仅仅通过修改 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源码相关

render函数在线转换

在 Vue 3 中总共有四种指令:v-onv-modelv-show 和 v-if。但是,实际上在源码中,只针对前面三者进行了特殊处理,这可以在 packages/runtime-dom/src/directives看出一下是截图

image.png image.png

源码处理相关 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 算法的应用相对较少。