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 函数外,避免重复创建
- 缓存事件:内联事件处理函数被缓存,避免子组件无效更新
目录