Vue 3 Proxy 响应式与性能优化

Vue 3 响应式原理、静态标记和按需更新机制

问题

Vue 3 相比 Vue 2 在响应式系统和渲染性能上做了哪些改进?

解答

1. Proxy 响应式

Vue 2 使用 Object.defineProperty,Vue 3 改用 Proxy

// Vue 2 方式:Object.defineProperty
function defineReactive(obj, key, val) {
  Object.defineProperty(obj, key, {
    get() {
      console.log('get', key)
      return val
    },
    set(newVal) {
      console.log('set', key, newVal)
      val = newVal
    }
  })
}

// 缺点:无法检测属性新增/删除,数组索引修改
const obj = { a: 1 }
defineReactive(obj, 'a', obj.a)
obj.b = 2 // 无法响应
// Vue 3 方式:Proxy
function reactive(target) {
  return new Proxy(target, {
    get(target, key, receiver) {
      console.log('get', key)
      track(target, key) // 收集依赖
      return Reflect.get(target, key, receiver)
    },
    set(target, key, value, receiver) {
      console.log('set', key, value)
      const result = Reflect.set(target, key, value, receiver)
      trigger(target, key) // 触发更新
      return result
    },
    deleteProperty(target, key) {
      console.log('delete', key)
      const result = Reflect.deleteProperty(target, key)
      trigger(target, key)
      return result
    }
  })
}

// 优点:可以检测属性新增、删除、数组索引修改
const state = reactive({ a: 1 })
state.b = 2 // 可以响应
delete state.a // 可以响应

2. 静态标记(PatchFlags)

编译时给动态节点打标记,运行时只对比有标记的节点:

<template>
  <div>
    <p>静态文本</p>
    <p>{{ message }}</p>
    <p :class="cls">动态 class</p>
  </div>
</template>

编译后:

import { createVNode, toDisplayString, openBlock, createBlock } from 'vue'

// PatchFlags 枚举
// 1 = TEXT        动态文本
// 2 = CLASS       动态 class
// 4 = STYLE       动态 style
// 8 = PROPS       动态属性

export function render() {
  return (openBlock(), createBlock('div', null, [
    createVNode('p', null, '静态文本'),  // 无标记,diff 时跳过
    createVNode('p', null, toDisplayString(message), 1 /* TEXT */),
    createVNode('p', { class: cls }, '动态 class', 2 /* CLASS */)
  ]))
}

3. 按需更新(Block Tree)

通过 openBlock 收集动态节点,更新时只遍历动态节点:

// 简化的 Block 实现
let currentBlock = null

function openBlock() {
  currentBlock = []
}

function createBlock(type, props, children) {
  const vnode = createVNode(type, props, children)
  vnode.dynamicChildren = currentBlock // 只存动态子节点
  currentBlock = null
  return vnode
}

function createVNode(type, props, children, patchFlag) {
  const vnode = { type, props, children, patchFlag }
  
  // 有 patchFlag 的节点加入 block
  if (patchFlag && currentBlock) {
    currentBlock.push(vnode)
  }
  
  return vnode
}

// patch 时只对比 dynamicChildren
function patchBlock(n1, n2) {
  const dynamicChildren = n2.dynamicChildren
  for (let i = 0; i < dynamicChildren.length; i++) {
    patchElement(n1.dynamicChildren[i], dynamicChildren[i])
  }
}

4. 静态提升(hoistStatic)

静态节点只创建一次,复用 VNode:

// 编译前
<template>
  <div>
    <p>静态内容</p>
    <p>{{ dynamic }}</p>
  </div>
</template>

// 编译后(开启静态提升)
const _hoisted_1 = createVNode('p', null, '静态内容')

export function render() {
  return (openBlock(), createBlock('div', null, [
    _hoisted_1,  // 复用,不重新创建
    createVNode('p', null, toDisplayString(dynamic), 1)
  ]))
}

关键点

  • Proxy 优势:可拦截属性新增、删除、数组索引修改,无需递归初始化
  • 静态标记:编译时标记动态内容类型,运行时按类型定向更新
  • Block Tree:收集动态节点到扁平数组,跳过静态节点的 diff
  • 静态提升:静态 VNode 提升到 render 函数外,避免重复创建
  • 缓存事件:内联事件处理函数被缓存,避免子组件无效更新