vue3响应式原理

简单回顾vue2
vue2响应式的一些问题:- 响应化过程需要遍历
data,props等,消耗较大 - 不支持
Set/Map、Class、数组等类型 - 新加的或者删除属性无法监听
- 数组响应化需要额外实现
- 对应的修改语法有限制
- 响应化过程需要遍历
vue3响应式处理
vue3响应式原理:使用ES6的proxy来解决这些问题
vue3的响应式流程并没有多大变化:依然是vue2的那一套(数据劫持+收集依赖+派发更新),只不过vue3更换了api,做了一些优化
effect
effect是vue3响应式系统的核心,而响应式又是vue3的核心,所以从effect开始讲。
export function effect<T = any>(
fn: () => T,
options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
if (isEffect(fn)) {
fn = fn.raw
}
const effect = createReactiveEffect(fn, options)
if (!options.lazy) {
effect()
}
return effect
}
参数:
fn: 传入的回调函数
options: 可选配置参数,说明如下表
| 参数 | 含义 |
|---|---|
| lazy | 是否延迟触发 effect |
| computed | 是否为计算属性 |
| scheduler | 调度函数 |
| onTrack | 追踪时触发 |
| onTrigger | 触发回调时触发 |
| onStop | 停止监听时触发 |
export interface ReactiveEffectOptions {
lazy?: boolean
computed?: boolean
scheduler?: (job: ReactiveEffect) => void
onTrack?: (event: DebuggerEvent) => void
onTrigger?: (event: DebuggerEvent) => void
onStop?: () => void
}
介绍完可配置参数options后,接着讲回调函数。当传入的回调函数是effect时,取出effect中的原始值然后调用createReactiveEffect创建新的effect;如果传入的options的lazy不为true,则立即调用我们刚刚创建的 effect, 最后返回刚刚创建的 effect。
createReactiveEffect是怎么创建effect的
function createReactiveEffect<T = any>(
fn: (...args: any[]) => T,
options: ReactiveEffectOptions
): ReactiveEffect<T> {
const effect = function reactiveEffect(...args: unknown[]): unknown {
if (!effect.active) {
return options.scheduler ? undefined : fn(...args)
}
if (!effectStack.includes(effect)) {
// 清空之前的依赖
cleanup(effect)
try {
// 开启跟踪
enableTracking()
effectStack.push(effect)
activeEffect = effect
return fn(...args)
} finally {
effectStack.pop()
resetTracking()
activeEffect = effectStack[effectStack.length - 1]
}
}
} as ReactiveEffect
effect.id = uid++
effect._isEffect = true
effect.active = true
effect.raw = fn
effect.deps = []
effect.options = options
return effect
}
reactiveEffect 如果 effect 不是激活状态,这种情况发生在我们调用了 effect 中的 stop 方法之后,那么先前没有传入调用 scheduler 函数的话,直接调用原始方法fn,否则直接返回。首先判断是否当前 effect 是否在 effectStack 当中,如果在,则不进行调用,这个主要是为了避免死循环。
effect挂载属性
| effect 挂载属性 | 含义 |
|---|---|
| id | 自增id, 唯一标识effect |
| _isEffect | 用于标识方法是否是effect |
| active | effect 是否激活 |
| raw | 创建effect是传入的fn |
| deps | 持有当前 effect 的dep 数组 |
| options | 创建effect是传入的options |
接着是清除依赖,每次 effect 运行都会重新收集依赖, deps 是持有 effect 的依赖数组,其中里面的每个 dep 是对应对象某个 key 的 全部依赖,我们在这里需要做的就是首先把 effect 从 dep 中删除,最后把 deps 数组清空。
function cleanup(effect: ReactiveEffect) {
const {
deps } = effect
if (deps.length) {
for (let i = 0; i < deps.length; i++) {
deps[i].delete(effect)
}
deps.length = 0
}
}
清除完依赖,就开始重新收集依赖。首先开启依赖收集,把当前 effect 放入 effectStack 中,然后将 activeEffect 设置为当前的 effect,activeEffect 主要为了在收集依赖的时候使用(在下面会很快讲到),然后调用 fn 并且返回值,当这一切完成的时候,finally 阶段,会把当前 effect 弹出,恢复原来的收集依赖的状态,还有恢复原来的 activeEffect。
try {
enableTracking()
effectStack.push(effect)
activeEffect = effect
return fn(...args)
} finally {
effectStack.pop()
resetTracking()
activeEffect = effectStack[effectStack.length - 1]
}
那 effect 是怎么收集依赖的呢?vue3 利用 proxy 劫持对象,在上面运行 effect 中读取对象的时候,当前对象的 key 的依赖 set集合 会把 effect 收集进去。
export function track(target: object, type: TrackOpTypes, key: unknown) {
...
}
vue3 在 reactive 中触发 track 函数,reactive 会在单独的章节讲。触发 track 的参数中,object 表示触发 track 的对象, type 代表触发 track 类型,而 key 则是 触发 track 的 object 的 key。在下面可以看到三种类型的读取对象会触发 track,分别是 get、 has、 iterate。
export const enum TrackOpTypes {
GET = 'get',
HAS = 'has',
ITERATE = 'iterate'
}
回到 track 内部,如果 shouldTrack 为 false 或者 activeEffect 为空,则不进行依赖收集。接着 targetMap 里面有没有该对象,没有新建 map,然后再看这个 map 有没有这个对象的对应 key 的 依赖 set 集合,没有则新建一个。 如果对象对应的 key 的 依赖 set 集合也没有当前 activeEffect, 则把 activeEffect 加到 set 里面,同时把 当前 set 塞到 activeEffect 的 deps 数组。最后如果是开发环境而且传入了 onTrack 函数,则触发 onTrack。 所以 deps 就是 effect 中所依赖的 key 对应的 set 集合数组, 毕竟一般来说,effect 中不止依赖一个对象或者不止依赖一个对象的一个key,而且 一个对象可以能不止被一个 effect 使用,所以是 set 集合数组。
if (!shouldTrack || activeEffect === undefined) {
return
}
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = new Set()))
}
if (!dep.has(activeEffect)) {
dep.add(activeEffect)
activeEffect.deps.push(dep)
if (__DEV__ && activeEffect.options.onTrack) {
activeEffect.options.onTrack({
effect: activeEffect,
target,
type,
key
})
}
}
依赖都收集完毕了,接下来就是触发依赖。如果 targetMap 为空,说明这个对象没有被追踪,直接return。
export function trigger(
target: object,
type: TriggerOpTypes,
key?: unknown,
newValue?: unknown,
oldValue?: unknown,
oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
const depsMap = targetMap.get(target)
if (!depsMap) {
// never been tracked
return
}
...
}
其中触发的 type, 包括了 set、add、delete 和 clear。
export const enum TriggerOpTypes {
SET = 'set',
ADD = 'add',
DELETE = 'delete',
CLEAR = 'clear'
}
接下来对 key 收集的依赖进行分组,computedRunners 具有更高的优先级,会触发下游的 effects 重新收集依赖。
const effects = new Set<ReactiveEffect>()
const computedRunners = new Set<ReactiveEffect>()
add 方法是将 effect 添加进不同分组的函数,其中 effect !== activeEffect 这个是为了避免死循环,在下面的注释也写的很清楚,避免出现 foo.value++ 这种情况。至于为什么是 set 呢,要避免 effect 多次运行。就好像循环中,set 触发了 trigger ,那么 ITERATE 和 当前 key 可能都属于同个 effect,这样就可以避免多次运行了。
const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
if (effectsToAdd) {
effectsToAdd.forEach(effect => {
if (effect !== activeEffect || !shouldTrack) {
if (effect.options.computed) {
computedRunners.add(effect)
} else {
effects.add(effect)
}
} else {
// the effect mutated its own dependency during its execution.
// this can be caused by operations like foo.value++
// do not trigger or we end in an infinite loop
}
})
}
}
下面根据触发 key 类型的不同进行 effect 的处理。如果是 clear 类型,则触发这个对象所有的 effect。如果 key 是 length , 而且 target 是数组,则会触发 key 为 length 的 effects ,以及 key 大于等于新 length的 effects, 因为这些此时数组长度变化了。
if (type === TriggerOpTypes.CLEAR) {
// collection being cleared
// trigger all effects for target
depsMap.forEach(add)
} else if (key === 'length' && isArray(target)) {
depsMap.forEach((dep, key) => {
if (key === 'length' || key >= (newValue as number)) {
add(dep)
}
})
}
下面则是对正常的新增、修改、删除进行 effect 的分组, isAddOrDelete 表示新增 或者不是数组的删除,这为了对迭代 key的 effect 进行触发,如果 isAddOrDelete 为 true 或者是 map 对象的设值,则触发 isArray(target) ? 'length' : ITERATE_KEY 的 effect ,如果 isAddOrDelete 为 true 且 对象为 map, 则触发 MAP_KEY_ITERATE_KEY 的 effect
else {
// schedule runs for SET | ADD | DELETE
if (key !== void 0) {
add(depsMap.get(key))
}
// also run for iteration key on ADD | DELETE | Map.SET
const isAddOrDelete =
type === TriggerOpTypes.ADD ||
(type === TriggerOpTypes.DELETE && !isArray(target))
if (
isAddOrDelete ||
(type === TriggerOpTypes.SET && target instanceof Map)
) {
add(depsMap.get(isArray(target) ? 'length' : ITERATE_KEY))
}
if (isAddOrDelete && target instanceof Map) {
add(depsMap.get(MAP_KEY_ITERATE_KEY))
}
}
最后是运行 effect, 像上面所说的,computed effects 会优先运行,因为 computed effects 在运行过程中,第一次会触发上游把cumputed effect收集进去,再把下游 effect 收集起来。
还有一点,就是 effect.options.scheduler,如果传入了调度函数,则通过 scheduler 函数去运行 effect, 但是 scheduler 里面可能不一定使用了 effect,例如 computed 里面,因为 computed 是延迟运行 effect, 这个会在讲 computed 的时候再讲。
const run =

本文详细介绍了Vue3响应式处理的改进,包括effect、reactive、baseHandlers和collectionHandlers等核心概念。通过对比Vue2,阐述了Vue3如何使用Proxy解决响应式问题,深入探讨了effect的运行机制、依赖收集和派发更新的过程。此外,还介绍了ref和computed的实现,揭示了Vue3计算属性的延迟与缓存特性。
最低0.47元/天 解锁文章
5029

被折叠的 条评论
为什么被折叠?



