Appearance
Vue 3 响应式渲染原理与实现
1. 响应式系统原理
1.1 核心概念
Vue 3 的响应式系统基于 ES6 的 Proxy 和 Reflect API,相比 Vue 2 的 Object.defineProperty 有以下优势:
- 能够监听对象的新增属性
- 能够监听数组的索引和长度变化
- 支持 Map、Set、WeakMap、WeakSet 等集合类型
- 性能更好,特别是在大型对象上
1.2 实现原理
Vue 3 的响应式系统由以下核心部分组成:
- Proxy 代理:创建对象的代理,拦截各种操作
- ReactiveEffect:响应式副作用,用于追踪依赖
- WeakMap:存储原始对象到代理对象的映射
- Map:存储对象属性到依赖集合的映射
1.2.1 核心实现代码
javascript
// 创建响应式对象
function reactive(target) {
return createReactiveObject(
target,
false,
mutableHandlers,
mutableCollectionHandlers
)
}
// 创建代理对象
function createReactiveObject(
target,
isReadonly,
baseHandlers,
collectionHandlers
) {
// 目标不是对象,直接返回
if (!isObject(target)) {
return target
}
// 目标已经是代理对象,直接返回
if (target.__v_isReactive && !isReadonly) {
return target
}
// 目标已经是只读代理,直接返回
if (target.__v_isReadonly && isReadonly) {
return target
}
// 检查是否有缓存的代理对象
const proxyMap = isReadonly ? readonlyMap : reactiveMap
const existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy
}
// 创建新的代理对象
const proxy = new Proxy(
target,
isCollectionType(target) ? collectionHandlers : baseHandlers
)
// 缓存代理对象
proxyMap.set(target, proxy)
return proxy
}1.3 依赖追踪与触发
1.3.1 依赖追踪
当访问响应式对象的属性时,会触发 get 拦截器,此时会收集当前正在执行的副作用函数作为依赖:
javascript
function createGetter(isReadonly = false, shallow = false) {
return function get(target, key, receiver) {
const res = Reflect.get(target, key, receiver)
// 非只读时进行依赖收集
if (!isReadonly) {
track(target, TrackOpTypes.GET, key)
}
// 处理嵌套对象
if (isObject(res) && !shallow) {
return isReadonly ? readonly(res) : reactive(res)
}
return res
}
}
// 依赖收集
function track(target, type, key) {
if (!isTracking()) {
return
}
// 获取 target 对应的依赖映射
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
// 获取 key 对应的依赖集合
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = createDep()))
}
// 将当前副作用添加到依赖集合
trackEffects(dep)
}1.3.2 依赖触发
当修改响应式对象的属性时,会触发 set 拦截器,此时会通知所有依赖该属性的副作用函数重新执行:
javascript
function createSetter(shallow = false) {
return function set(target, key, value, receiver) {
const oldValue = target[key]
const hadKey =
isArray(target) && isIntegerKey(key)
? Number(key) < target.length
: hasOwn(target, key)
const result = Reflect.set(target, key, value, receiver)
// 如果目标是原型链上的属性,不触发更新
if (target === toRaw(receiver)) {
if (!hadKey) {
// 新增属性
trigger(target, TriggerOpTypes.ADD, key, value)
} else if (
!shallow &&
oldValue !== value &&
(oldValue === oldValue || value === value)
) {
// 修改属性
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
}
return result
}
}
// 依赖触发
function trigger(target, type, key, newValue, oldValue) {
const depsMap = targetMap.get(target)
if (!depsMap) {
return
}
// 收集所有需要触发的依赖
const effects = new Set()
const add = effectsToAdd => {
if (effectsToAdd) {
effectsToAdd.forEach(effect => effects.add(effect))
}
}
// 处理不同类型的操作
if (type === TriggerOpTypes.CLEAR) {
// 清除操作,触发所有依赖
depsMap.forEach(add)
} else if (type === TriggerOpTypes.RESET) {
// 重置操作,触发所有依赖
depsMap.forEach(add)
} else if (type === TriggerOpTypes.ADD || type === TriggerOpTypes.DELETE) {
// 新增或删除属性,触发对应 key 的依赖以及长度相关的依赖(数组)
add(depsMap.get(key))
if (isArray(target)) {
if (key === 'length') {
// 长度变化,触发所有索引大于等于新长度的依赖
depsMap.forEach((dep, key) => {
if (key >= newValue) {
add(dep)
}
})
} else {
// 索引变化,触发长度的依赖
add(depsMap.get('length'))
}
}
} else if (type === TriggerOpTypes.SET) {
// 修改属性,触发对应 key 的依赖
add(depsMap.get(key))
// 数组特殊处理
if (isArray(target) && isIntegerKey(key)) {
add(depsMap.get('length'))
}
}
// 触发所有依赖
effects.forEach(effect => {
if (effect !== activeEffect || effect.allowRecurse) {
if (effect.scheduler) {
effect.scheduler()
} else {
effect.run()
}
}
})
}2. 虚拟 DOM 原理
2.1 核心概念
虚拟 DOM(Virtual DOM)是对真实 DOM 的抽象表示,是一个普通的 JavaScript 对象,包含了描述真实 DOM 所需的信息。
Vue 3 的虚拟 DOM 相比 Vue 2 有以下改进:
- 更轻量:移除了一些不必要的属性
- 更高效:优化了创建和更新的流程
- 更好的 TypeScript 支持
2.2 VNode 结构
Vue 3 的 VNode 结构如下:
typescript
interface VNode {
__v_isVNode: true
type: VNodeTypes // 元素类型、组件、Fragment 等
props: VNodeProps | null // 属性
key: string | number | null // 唯一标识,用于 diff 算法
ref: VNodeRef | null // 引用
children: VNodeNormalizedChildren // 子节点
el: HostNode | null // 对应的真实 DOM 节点
// 其他内部属性
}2.3 创建 VNode
javascript
function _createVNode(
type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT,
props: (Data & VNodeProps) | null = null,
children: unknown = null,
patchFlag: number = 0,
dynamicProps: string[] | null = null,
isBlockNode = false
): VNode {
// 标准化 props
props = props || null
// 创建 VNode 对象
const vnode = {
__v_isVNode: true,
type,
props,
key: props && normalizeKey(props),
ref: props && normalizeRef(props),
children: null,
el: null,
patchFlag,
dynamicProps,
appContext: null
}
// 标准化子节点
normalizeChildren(vnode, children)
// 设置 appContext
if (currentApp) {
vnode.appContext = currentApp._context
}
return vnode
}3. Diff 算法原理与实现
3.1 核心概念
Diff 算法是虚拟 DOM 的核心,用于比较新旧 VNode 树的差异,并最小化 DOM 操作。Vue 3 的 Diff 算法相比 Vue 2 有以下改进:
- 静态标记:通过
patchFlag标记动态内容,减少比较次数 - 区块化:将 VNode 树划分为区块,只比较动态区块
- 快速路径:针对常见场景优化,如数组尾部添加元素
3.2 实现原理
Vue 3 的 Diff 算法主要分为以下几个步骤:
- 同类型节点比较:如果新旧 VNode 类型相同,进行精细化比较
- 不同类型节点替换:如果新旧 VNode 类型不同,直接替换整个节点
- 子节点比较:对于有子节点的节点,使用双端比较算法
3.3 核心实现代码
3.3.1 节点比较
javascript
function processComponent(n1: VNode | null, n2: VNode, container: RendererElement, anchor: RendererNode | null, parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, isSVG: boolean, optimized: boolean) {
if (n1 == null) {
// 首次渲染
mountComponent(n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
} else {
// 更新组件
updateComponent(n1, n2, optimized)
}
}
function processElement(n1: VNode | null, n2: VNode, container: RendererElement, anchor: RendererNode | null, parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, isSVG: boolean, optimized: boolean) {
if (n1 == null) {
// 首次渲染
mountElement(n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
} else {
// 更新元素
patchElement(n1, n2, parentComponent, parentSuspense, isSVG, optimized)
}
}3.3.2 子节点比较
javascript
function patchChildren(n1: VNode, n2: VNode, container: RendererElement, anchor: RendererNode | null, parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, isSVG: boolean, optimized: boolean) {
const c1 = n1 && n1.children
const c2 = n2.children
const prevShapeFlag = n1 ? n1.shapeFlag : 0
const nextShapeFlag = n2.shapeFlag
// 处理文本节点
if (nextShapeFlag & ShapeFlags.TEXT_CHILDREN) {
if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// 旧节点是数组,新节点是文本,清空旧节点
unmountChildren(c1 as VNode[], parentComponent, parentSuspense)
}
if (c2 !== c1) {
// 文本内容不同,更新文本
hostSetElementText(container, c2 as string)
}
} else {
// 处理数组子节点
if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
if (nextShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// 新旧节点都是数组,使用 diff 算法
patchKeyedChildren(c1 as VNode[], c2 as VNode[], container, anchor, parentComponent, parentSuspense, isSVG, optimized)
} else {
// 旧节点是数组,新节点不是,清空旧节点
unmountChildren(c1 as VNode[], parentComponent, parentSuspense)
}
} else {
if (nextShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// 旧节点不是数组,新节点是数组,挂载新节点
mountChildren(c2 as VNode[], container, anchor, parentComponent, parentSuspense, isSVG, optimized)
}
// 新旧节点都不是数组,无需处理
}
}
}3.3.3 双端比较算法
javascript
function patchKeyedChildren(c1: VNode[], c2: VNode[], container: RendererElement, anchor: RendererNode | null, parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, isSVG: boolean, optimized: boolean) {
let i = 0
const l2 = c2.length
let e1 = c1.length - 1
let e2 = l2 - 1
// 1. 从头部开始比较
while (i <= e1 && i <= e2) {
const n1 = c1[i]
const n2 = c2[i]
if (isSameVNodeType(n1, n2)) {
// 类型相同,递归比较
patch(n1, n2, container, null, parentComponent, parentSuspense, isSVG, optimized)
} else {
break
}
i++
}
// 2. 从尾部开始比较
while (i <= e1 && i <= e2) {
const n1 = c1[e1]
const n2 = c2[e2]
if (isSameVNodeType(n1, n2)) {
// 类型相同,递归比较
patch(n1, n2, container, null, parentComponent, parentSuspense, isSVG, optimized)
} else {
break
}
e1--
e2--
}
// 3. 新节点比旧节点多,挂载新节点
if (i > e1) {
if (i <= e2) {
const nextAnchor = e2 + 1 < l2 ? c2[e2 + 1].el : anchor
while (i <= e2) {
patch(null, c2[i], container, nextAnchor, parentComponent, parentSuspense, isSVG, optimized)
i++
}
}
}
// 4. 旧节点比新节点多,卸载旧节点
else if (i > e2) {
while (i <= e1) {
unmount(c1[i], parentComponent, parentSuspense, true)
i++
}
}
// 5. 中间部分,使用 key 进行比较
else {
const s1 = i
const s2 = i
// 构建新节点的 key 到索引的映射
const keyToNewIndexMap = new Map()
for (let i = s2; i <= e2; i++) {
const nextChild = c2[i]
keyToNewIndexMap.set(nextChild.key, i)
}
const toBePatched = e2 - s2 + 1
const newIndexToOldIndexMap = new Array(toBePatched).fill(0)
// 遍历旧节点,进行更新或卸载
for (let i = s1; i <= e1; i++) {
const prevChild = c1[i]
let newIndex = keyToNewIndexMap.get(prevChild.key)
if (newIndex === undefined) {
// 旧节点在新节点中不存在,卸载
unmount(prevChild, parentComponent, parentSuspense, true)
} else {
// 旧节点在新节点中存在,更新
newIndexToOldIndexMap[newIndex - s2] = i + 1
patch(prevChild, c2[newIndex], container, null, parentComponent, parentSuspense, isSVG, optimized)
}
}
// 遍历新节点,挂载或移动
const increasingNewIndexSequence = getSequence(newIndexToOldIndexMap)
let j = increasingNewIndexSequence.length - 1
for (let i = toBePatched - 1; i >= 0; i--) {
const nextIndex = s2 + i
const nextChild = c2[nextIndex]
const anchor = nextIndex + 1 < l2 ? c2[nextIndex + 1].el : anchor
if (newIndexToOldIndexMap[i] === 0) {
// 新节点在旧节点中不存在,挂载
patch(null, nextChild, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
} else {
// 新节点在旧节点中存在,检查是否需要移动
if (i !== increasingNewIndexSequence[j]) {
// 需要移动
hostInsert(nextChild.el!, container, anchor)
} else {
// 不需要移动,更新 j
j--
}
}
}
}
}3.3.4 最长递增子序列算法
为了最小化 DOM 移动操作,Vue 3 使用了最长递增子序列算法来找出不需要移动的节点:
javascript
function getSequence(arr: number[]): number[] {
const p = arr.slice()
const result = [0]
let i, j, u, v, c
const len = arr.length
for (i = 0; i < len; i++) {
const arrI = arr[i]
if (arrI !== 0) {
j = result[result.length - 1]
if (arr[j] < arrI) {
p[i] = j
result.push(i)
continue
}
u = 0
v = result.length - 1
while (u < v) {
c = (u + v) >> 1
if (arr[result[c]] < arrI) {
u = c + 1
} else {
v = c
}
}
if (arrI < arr[result[u]]) {
if (u > 0) {
p[i] = result[u - 1]
}
result[u] = i
}
}
}
u = result.length
v = result[u - 1]
while (u-- > 0) {
result[u] = v
v = p[v]
}
return result
}4. 响应式渲染流程
4.1 整体流程
Vue 3 的响应式渲染流程如下:
- 初始化:创建组件实例,初始化响应式数据
- 渲染:执行 render 函数,生成虚拟 DOM
- 挂载:将虚拟 DOM 转换为真实 DOM
- 更新:当响应式数据变化时,触发依赖更新,重新渲染
- 卸载:组件销毁时,清理依赖和 DOM
4.2 渲染过程
javascript
function setupRenderEffect(instance: ComponentInternalInstance, initialVNode: VNode, container: RendererElement, anchor: RendererNode | null, parentSuspense: SuspenseBoundary | null) {
// 创建响应式副作用
const componentUpdateFn = () => {
if (!instance.isMounted) {
// 首次渲染
const subTree = (instance.subTree = renderComponentRoot(instance))
patch(null, subTree, container, anchor, instance, parentSuspense, isSVG(instance.vnode.type))
initialVNode.el = subTree.el
instance.isMounted = true
} else {
// 更新渲染
let { next, vnode } = instance
if (next) {
vnode = next
next = null
}
const prevTree = instance.subTree
const nextTree = renderComponentRoot(instance)
instance.subTree = nextTree
patch(prevTree, nextTree, container, anchor, instance, parentSuspense, isSVG(vnode.type))
}
}
// 创建副作用
const effect = (instance.effect = new ReactiveEffect(
componentUpdateFn,
() => queueJob(update),
instance.scope
))
// 执行副作用
const update = (instance.update = () => effect.run())
update()
}5. 性能优化
5.1 编译优化
Vue 3 编译器会对模板进行静态分析,生成优化后的代码:
- 静态提升:将静态节点提升到渲染函数外部
- Patch Flag:为动态节点添加标记,减少比较次数
- 区块化:将 VNode 树划分为区块,只比较动态区块
5.2 运行时优化
- 缓存事件处理函数:避免每次渲染都创建新的函数
- 合理使用 key:帮助 Diff 算法快速定位节点
- 避免不必要的响应式:对于不需要响应式的数据,使用
shallowRef或markRaw - 使用
v-memo:缓存计算结果,避免重复计算
6. 总结
Vue 3 的响应式系统和虚拟 DOM 实现相比 Vue 2 有了显著的改进:
- 响应式系统:基于 Proxy,支持更多类型,性能更好
- 虚拟 DOM:更轻量,更高效
- Diff 算法:优化了比较策略,减少了 DOM 操作
- 编译优化:生成更高效的渲染代码
这些改进使得 Vue 3 在大型应用中表现更加出色,同时保持了 Vue 一贯的易用性和灵活性。