Skip to content

静态提升:hoistStatic 的实现

每次渲染都会执行 render 函数,创建新的 VNode 树。对于静态内容,每次创建的 VNode 完全相同——这是不必要的开销。

静态提升将这些内容提升到 render 函数外部,只创建一次。 这是一个简单但效果显著的优化。

问题场景

javascript
// 未优化:每次渲染都创建
function render() {
  return createVNode('div', null, [
    createVNode('h1', null, '欢迎'),      // 每次都创建
    createVNode('p', null, '这是介绍'),   // 每次都创建
    createVNode('span', null, ctx.msg)    // 必须每次创建
  ])
}

假设 render 执行 1000 次:

  • 创建 3000 个 VNode 对象
  • 其中 2000 个是完全相同的静态节点

静态提升后

javascript
// 提升到模块顶层
const _hoisted_1 = createVNode('h1', null, '欢迎', -1 /* HOISTED */)
const _hoisted_2 = createVNode('p', null, '这是介绍', -1 /* HOISTED */)

function render() {
  return createVNode('div', null, [
    _hoisted_1,  // 复用
    _hoisted_2,  // 复用
    createVNode('span', null, ctx.msg)
  ])
}

1000 次渲染只创建 1002 个 VNode(2 个静态 + 1000 个动态)。

常量类型分类

不是所有节点都能提升。Vue 3 使用 ConstantType 分类:

javascript
const ConstantTypes = {
  NOT_CONSTANT: 0,    // 不是常量,不能提升
  CAN_SKIP_PATCH: 1,  // 可以跳过 patch,但不能提升
  CAN_HOIST: 2,       // 可以提升
  CAN_STRINGIFY: 3    // 可以字符串化
}

判定规则:

javascript
function getConstantType(node, context) {
  switch (node.type) {
    case NodeTypes.ELEMENT:
      // 组件不能提升
      if (node.tagType !== ElementTypes.ELEMENT) {
        return ConstantTypes.NOT_CONSTANT
      }
      
      // 有动态绑定不能提升
      const flag = getPatchFlag(node)
      if (flag !== undefined && flag !== PatchFlags.HOISTED) {
        return ConstantTypes.NOT_CONSTANT
      }
      
      // 检查属性是否都是静态的
      for (const prop of node.props) {
        if (!isStaticProp(prop)) {
          return ConstantTypes.NOT_CONSTANT
        }
      }
      
      // 递归检查子节点
      let returnType = ConstantTypes.CAN_STRINGIFY
      for (const child of node.children) {
        const childType = getConstantType(child, context)
        if (childType === ConstantTypes.NOT_CONSTANT) {
          return ConstantTypes.NOT_CONSTANT
        }
        if (childType < returnType) {
          returnType = childType
        }
      }
      
      return returnType
      
    case NodeTypes.TEXT:
      return ConstantTypes.CAN_STRINGIFY
      
    case NodeTypes.INTERPOLATION:
      // 插值表达式是动态的
      return ConstantTypes.NOT_CONSTANT
      
    // ...
  }
}

hoistStatic 实现

javascript
function hoistStatic(root, context) {
  walk(root, context)
}

function walk(node, context) {
  const { children } = node
  
  for (let i = 0; i < children.length; i++) {
    const child = children[i]
    
    if (child.type === NodeTypes.ELEMENT) {
      const constantType = getConstantType(child, context)
      
      if (constantType >= ConstantTypes.CAN_HOIST) {
        // 可以提升
        // 标记 PatchFlag 为 HOISTED
        child.codegenNode.patchFlag = PatchFlags.HOISTED
        
        // 添加到提升列表
        child.codegenNode = context.hoist(child.codegenNode)
      } else if (constantType === ConstantTypes.CAN_SKIP_PATCH) {
        // 不能提升但可以跳过 patch
        child.codegenNode.patchFlag = PatchFlags.HOISTED
      }
    }
    
    // 递归处理子节点
    if (child.type === NodeTypes.ELEMENT && 
        child.codegenNode?.type === NodeTypes.VNODE_CALL) {
      walk(child, context)
    }
  }
}

context.hoist

javascript
function createTransformContext(root, options) {
  const hoists = []
  
  return {
    hoists,
    
    hoist(exp) {
      // 添加到提升列表
      hoists.push(exp)
      
      // 返回对提升变量的引用
      const identifier = createSimpleExpression(
        `_hoisted_${hoists.length}`,
        false,
        exp.loc,
        ConstantTypes.CAN_HOIST
      )
      identifier.hoisted = exp
      return identifier
    }
  }
}

代码生成

javascript
function generate(ast, context) {
  const { hoists } = ast
  
  // 生成提升变量
  if (hoists.length) {
    for (let i = 0; i < hoists.length; i++) {
      push(`const _hoisted_${i + 1} = `)
      genNode(hoists[i], context)
      push('\n')
    }
  }
  
  // 生成 render 函数
  push(`function render(_ctx) {\n`)
  // ...
}

静态属性提升

不仅整个节点可以提升,静态属性对象也可以:

html
<div class="container" id="main">
  {{ content }}
</div>
javascript
// 属性对象提升
const _hoisted_1 = { class: "container", id: "main" }

function render(_ctx) {
  return createElementVNode("div", _hoisted_1, 
    _toDisplayString(_ctx.content), 
    1 /* TEXT */
  )
}

不能提升的情况

组件

html
<MyComponent />

组件可能有副作用,不能提升。

动态属性

html
<div :class="cls">static</div>

有动态绑定,整个节点不能提升。

带 ref 的元素

html
<div ref="myDiv">static</div>

ref 需要在每次渲染时处理。

内存与 GC

静态提升有轻微的内存代价——提升的节点在模块生命周期内一直存在。

但收益远大于代价:

  • 减少了每次渲染的对象分配
  • 减少了 GC 压力
  • 提升的节点可以被多个组件实例共享

本章小结

本章分析了静态提升的实现:

  • ConstantType 分类:判断节点是否可提升
  • hoistStatic 转换:遍历 AST,标记可提升节点
  • context.hoist:收集提升节点,返回引用
  • 代码生成:在模块顶层生成提升变量

静态提升解决了"重复创建"的问题。下一章我们将分析预字符串化——将静态内容进一步优化为字符串。

静态提升:hoistStatic 的实现 has loaded