- Published on
Vue3响应式系统深度解析
- Authors
- Name
Vue 3的响应式系统可以概括为以下几个关键步骤:
- 创建响应式数据
- 依赖收集
- 设置副作用
- 触发更新
- 调度更新
- 组件更新
- 界面更新
让我们详细探讨每个步骤的实现。
1. 创建响应式数据
Vue 3使用reactive和ref两个函数来创建响应式数据。
function reactive(target) {
// 使用 Proxy 创建响应式对象
return new Proxy(target, {
get(target, key, receiver) {
const result = Reflect.get(target, key, receiver)
track(target, key) // 进行依赖收集
return result
},
set(target, key, value, receiver) {
const oldValue = target[key]
const result = Reflect.set(target, key, value, receiver)
if (oldValue !== value) {
trigger(target, key) // 触发更新
}
return result
}
})
}
function ref(value) {
// 创建一个包含 value 属性的对象
const refObject = {
get value() {
track(refObject, 'value') // 进行依赖收集
return value
},
set value(newValue) {
if (value !== newValue) {
value = newValue
trigger(refObject, 'value') // 触发更新
}
}
}
return refObject
}
2. 依赖收集
当访问响应式数据时,系统会自动收集依赖:
let activeEffect // 当前激活的副作用函数
const targetMap = new WeakMap() // 存储所有的依赖关系
function track(target, key) {
if (activeEffect) {
// 获取当前对象的依赖图
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 = new Set()))
}
// 将当前副作用函数添加到依赖集合中
dep.add(activeEffect)
}
}
3. 设置副作用
effect函数用于设置副作用,比如组件的渲染函数:
function effect(fn) {
const effectFn = () => {
activeEffect = effectFn // 设置当前活跃的副作用函数
fn() // 执行副作用函数,这将触发 getter,从而收集依赖
activeEffect = null // 清除当前活跃的副作用函数
}
effectFn() // 立即执行一次以收集初始依赖
return effectFn
}
副作用的设置和使用主要发生在以下四个场景:
组件渲染
- 相关 API:
setup(),<script setup> - 描述: 组件的渲染函数被自动包装在一个 effect 中
- 相关 API:
计算属性
- 相关 API:
computed() - 描述: 计算属性内部使用 effect 来追踪依赖并缓存结果
- 相关 API:
侦听器
- 相关 API:
watch(),watchEffect() - 描述: 用于观察响应式数据变化并执行回调
- 相关 API:
自定义副作用
- 相关 API: 直接使用
effect()(通常在底层库中使用) - 描述: 用于创建自定义的响应式行为
- 相关 API: 直接使用
当这些副作用函数执行时,它们会访问响应式数据,从而触发 track 函数进行依赖收集。这就建立了数据和副作用之间的联系。之后,当响应式数据变化时,相关的副作用函数就会被重新执行。
4. 触发更新
当响应式数据变化时,会触发相关的副作用:
function trigger(target, key) {
// 获取目标对象的依赖图
const depsMap = targetMap.get(target)
if (!depsMap) return
// 获取特定 key 的依赖集合
const dep = depsMap.get(key)
if (dep) {
// 遍历执行所有的副作用函数
dep.forEach(effectFn => {
if (effectFn.scheduler) {
// 如果有调度器,则使用调度器来执行
effectFn.scheduler()
} else {
// 否则直接执行
effectFn()
}
})
}
}
5. 调度更新
Vue 3使用异步队列来批量处理更新,提高性能:
const queue = new Set() // 使用 Set 来去重任务
let isFlushing = false
const p = Promise.resolve() // 创建一个 Promise 以使用微任务
function queueJob(job) {
queue.add(job)
if (!isFlushing) {
// 如果还没有刷新队列,则安排一次刷新
isFlushing = true
p.then(flushJobs)
}
}
function flushJobs() {
// 执行队列中的所有任务
for (const job of queue) {
job()
}
queue.clear() // 清空队列
isFlushing = false // 重置刷新标志
}
6. 组件更新
组件的更新函数被包装在effect中:
const componentUpdateFn = () => {
const vnode = render() // 生成新的虚拟 DOM
patch(prevVNode, vnode) // 对比新旧虚拟 DOM 并更新实际 DOM
}
effect(() => {
componentUpdateFn()
}, {
scheduler: queueJob // 使用 queueJob 作为调度器,实现异步更新
})
7. 界面更新
最后,通过虚拟DOM的diff算法,高效地更新实际DOM:
function patch(oldVNode, newVNode) {
if (oldVNode.tag !== newVNode.tag) {
// 如果节点类型变了,直接替换整个节点
oldVNode.el.parentNode.replaceChild(createElement(newVNode), oldVNode.el)
} else {
const el = newVNode.el = oldVNode.el
// 更新节点的属性
updateProperties(el, oldVNode.props, newVNode.props)
// 更新子节点
updateChildren(el, oldVNode.children, newVNode.children)
}
}
总结
Vue 3的响应式系统通过精心设计的流程,实现了高效的数据-视图同步:
- 使用Proxy或包装对象创建响应式数据。
- 通过依赖收集建立数据与副作用之间的关联。
- 利用effect函数包装副作用,实现自动依赖追踪。
- 在数据变化时触发相关副作用。
- 使用异步队列批量处理更新,优化性能。
- 组件更新函数被包装为响应式效果,自动响应数据变化。
- 通过虚拟DOM和diff算法高效更新实际DOM。