优化

2025/3/13 Vue3编译

# 1. 动态节点收集与补丁标志

# diff 算法的问题

<div id="app"> 
  <p>{{ msg }}</p>
</div>

无论哪种 diff 算法,都会一层一层的遍历。

  1. 对比 div 节点
  2. 对比 p 节点
  3. 对比 p 节点下的文本节点

vue3 中的编译器会讲编译时得到的信息放到 vnodedynamicChildren 中,这样在 diff 的时候,就可以直接跳过 p 节点,从而提高性能。

# patchFlags & block

<div>
  <div>foo</div>
  <p>{{ msg }}</p>
</div>

传统的虚拟 dom

const vnode = {
  tag: 'div',
  children: [
    { tag: 'div', children: 'foo' },
    { tag: 'p', children: ctx.msg }
  ]
}

编译优化之后的虚拟 dom, 区分了静态内容和动态内容,patchFlag 即补丁标志; 把所有的动态子节点提取出来。

block: 带有 dynamicChildren 属性的虚拟节点。

const vnode = {
  tag: 'div',
  children: [
    { tag: 'div', children: 'foo' },
    { tag: 'p', children: ctx.msg, patchFlag: PatchFlags.TEXT }
  ],
  // 会存储该节点下的所有动态子节点
  dynamicChildren: [
    { tag: 'p', children: ctx.msg, patchFlag: PatchFlags.TEXT }
  ]
}

PatchFlags 的值 点击查看 (opens new window)

当有了 Block 时,渲染器在更新的时候,只更新虚拟节点下的 dynamicChildren 中的节点,而不会更新静态节点。

什么时候虚拟节点才会变成 Block 节点呢?

所有模板的根节点,带有 v-for v-if 等指令的节点都需要作为 Block 节点。

# 动态节点的收集

<div id="app"> 
  <p class="msg">{{ msg }}</p>
</div>

它的渲染函数

const cmp = {
  render () {
    return createVnode('div', { id: 'app'}, [
      createVnode('p', { class: 'msg' }, text, PatchFlags.TEXT)
    ])
  }
}

createdVnode 函数的调用是层层嵌套的,且遵循内层先执行,外层后执行的原则。

const cmp = {
  render () {
    return (openBlock(), createBlock('div', { id: 'app'}, [
      createVnode('p', { class: 'msg' }, text, PatchFlags.TEXT)
    ]))
  }
}

function createVnode (type, props, children, patchFlag) {
  const vnode = {
    tag,
    props,
    children,
    patchFlag,
  }

  if(typeof  patchFlag !== 'undefined' && currentDynamicChildren){
    // 是动态节点就添加
    currentDynamicChildren.push(vnode)
  }

  return vnode
}

// 动态节点栈
const dynamicChildrenStack = []
// 当前动态节点集合
const currentDynamicChildren = null

function createBlock(type, props, children){
  // 创建 block
  const block = createVnode(type, props, children)
  block.dynamicChildren = currentDynamicChildren
  // 关闭当前动态节点集合
  closeBlock()
  return block
}

// 创建一个新的动态节点的集合
function openBlock(){
  dynamicChildrenStack.push(currentDynamicChildren = [])
}

// 从创建的栈中取出当前动态节点集合
function closeBlock(){
  currentDynamicChildren = dynamicChildrenStack.pop()
}
  1. 当创建 Block 时,内层所有的 createdVnode 已经执行完了,
  2. currentDynamicChildren 所存储的就是当前 Block 的所有动态子节点
  3. 创建完成之后,将动态节点结合从栈中弹出

createBlock (opens new window)

# 打补丁

function patchELement (n1, n2, container, anchor) {
  
  // 单个动态节点
  if(n2.patchFlag){
    if(n2.patchFlag === 1){
      // text
    }else if(n2.patchFlag === 2){
      // classs
    }
  //   ....
  }
  
  // 多个动态节点
  if(n2.dynamicChildren){
    patchBlockChild(n1, n2)
  }
}

function patchBlockChild (n1, n2) {
  // 只更新动态节点
  for (let i = 0; i < n2.dynamicChildren.length; i++) {
    patchELement(n1.dydynamicChildren[i], n2.dynamicChildren[i])
  }
}

# 2. block

虽然根节点必须作为 Block 节点,但是如果只有根节点,那么他不会形成 Block 树。

dynamicChildren属性是会忽略虚拟 dom 层级的

# v-if

<div>
  <p v-if="show"><p>{{ msg }}</p></p>
  <div v-else><div><p>{{ msg }}</p></div><</div>
</div>

收集到的dynamicChildren[{tag: 'p', children: ctx.msg, patchFlag: PatchFlags.TEXT}]

但是把 v-if 等结构化的指令作为 Block 角色就可以了

const block = {
  tag: 'div',
  dynamicChildren: [
    { tag: 'p', children: ctx.msg, patchFlag: PatchFlags.TEXT, dynamicChildren: [] }
  ]
}

# v-for

不稳定结构,当 v-for 有顺序要求时,只通过传统的 diff 算法来进行更新。 如果循环的数据是个常量 就可以使用dynamicChildren

dynamicChildren 是没有层级的,无序的,无法满足有序的循环列表。

# 3. 静态提升

把静态节点提升到渲染函数之外

<div>
  <p>static</p>
  <p class="p" a="b">{{ title }}</p>
</div>

没有静态提升

function render(){
  const staticProps = { class: 'p', a: 'b' }
  
  return (openBlock(), createBlock('div', null, [
    createVnode('p', null, 'static'),
    createVnode('p', staticProps, ctx.title, PatchFlags.TEXT)
  ]))
}

静态提升之后,只保持对静态节点的引用,即使数据有变化,也不会对静态节点重新创建虚拟节点。

静态属性也会被提升

const staticNode = createVnode('p', null, 'static')
// 静态属性提升
const staticProps = { class: 'p', a: 'b' }

function render(){
  return (openBlock(), createBlock('div', null, [
    staticNode,
    createVnode('p', staticProps, ctx.title, PatchFlags.TEXT)
  ]))
}

静态提升是以树为单位的,整个 section 都会被提升,外层 div 作为根节点,默认是 Block

<div>
  <section>
    <div>
      <p>static</p>
    </div>
  </section>
</div>

# 4. 预字符串化

预字符串化是基于静态提升的一种优化策略

<div>
  <p>1</p>
  <p>2</p>
  <p>3</p>
<!--  20个  ... -->
  <p>20</p>
</div>

当包含了大量的纯静态的标签节点是,把静态节点序列化为字符串

function render(){
  const staticNode = createStaticVnode('<p></p><p></p> 20个...</p>')
  
  return (openBlock(), createBlock('div', null, [
    staticNode,
  ]))
}
  1. 大的静态内容可以通过 innerHtml 进行设置
  2. 减少虚拟节点的创建
  3. 减少内存的占用

# 5. 缓存内联处理函数

渲染函数的第二个参数是个 props , 每次重新渲染都会创建一个新的 props

const comp = `<Comp @click="a+b"/>`
function render(ctx){
  return h(comp, {
    onClick: () => ctx.a + ctx.b
  })
}

渲染函数的第二个参数是一个数组 cache,可以把内联事件添加到缓存中

const comp = `<Comp @click="a+b"/>`
function render(ctx, cache){
  return h(com, {
    onClick: cache[0] || (cache[0] = () => ctx.a + ctx.b)
  })
}

# 6. v-once

仅渲染元素和组件一次,并跳过之后的更新, 在随后的重新渲染,元素/组件及其所有子项将被当作静态内容并跳过渲染。这可以用来优化更新时的性能。

<div>
  <p v-once> {{ msg }} <div>
</div>
function render(ctx, cache){
  return (openBlock(), createBlock('div', null, [
    cache[1] || (
      // 阻止 vnode 被 block 收集
      setBlockTracking(-1),
      cache[1] = createVnode('p', null, ctx.msg, PatchFlags.TEXT),
      // 恢复收集
      setBlockTracking(1),
      cache[1]
    )
  ]))
}
  1. 避免组件更新,虚拟 dom 的重新创建
  2. 避免无用的 diff 算法
Last Updated: 2026/06/09/ 06:29:26