需要解决什么问题(同vue2.0 老三样)
第一步 数据劫持
vue3.0做数据代理就是通过 es6 Proxy这个api来完成的
Vue3.0 对于数据的响应式挟持,统一使用 composition API 来实现,对最典型的 reactive来讲解
铺垫
依赖缓存的数据结构 (一对多对多的结构模式)

reactive
1 | export function reactive<T extends object>(target: T): UnwrapNestedRefs<T> |
createReactiveObject
前置函数或者依赖
这个先不看 createReactiveObject用到的时候再来看下
1 | export const reactiveMap = new WeakMap<Target, any>() |
createReactiveObject入口函数
1 | function createReactiveObject( |
mutableHandlers(Object/Array 代理处理器)
注意看这里
有依赖收集track触发逻辑,在get、has、ownKeys拦截里面
有惰性响应式的实现,在get拦截里面
有派发更新trigger触发逻辑,在set、deleteProperty拦截里面
为了方便阅读,忽略了一些在 reactive 情境下的常量值判断,比如 readOnly,shadow
1 | export const mutableHandlers: ProxyHandler<object> = { |
collectionHandlers(Map/Set/WeakMap/WeakSet 代理处理器)
由于上述四个类型修改值都是通过函数修改的,所以代理函数只拦截 get 方法,用于拦截响应对象调用了哪个操作函数,通过key判断再进行具体的依赖收集或者派发更新
注意看这里,通过key作为代理函数
有依赖收集track触发逻辑,在get、size、has、ownKeys、forEach拦截里面
有派发更新trigger触发逻辑,在add、set、deleteEntry、clear拦截里面
1 |
|
第二步 依赖收集
在刚刚数据劫持的逻辑里面可以看到很多 调用track,这个就是定义在effect模块里面,作为依赖收集的功能函数。
如何知道 响应对象 依赖了哪些 数据,这个问题进一步就是 响应对象用了哪些数据。
Vue 的大体思路是这样的,比如我一个函数 fnA,里使用了 data 里的 B 和 C。
想要知道 fnA 使用了 B 和 C,那我们干脆就直接运行一下 fnA,在 B 和 C 里面等待 fnA 的获取,然后建立两者的依赖。
在看完2.0源码分析 回顾Vue2.0 里有一些概念 Watcher,Dep,target。
- Watcher 就是指 fnA
- Dep 则是存在 B 的 setter 里面的一个对象,用于存放 Watcher 集合
- target 则是现在正在进行依赖收集的 Watcher
简单粗暴来讲
new Watcher(fnA)
=> target 等于当前 Watcher并调用 fnA
=> fnA 获取 B 的值,B 会将 target 放到自己的 Dep 上
=> B 更新了,通知自己 Dep 上的 Watcher 重新执行 fnA
Vue3.0 思路差不多,但是实现上大有不同,因为 Vue3.0 不再随意对数据进行侵入式修改或者挟持,所以 Vue3.0 单独拎出来了一个静态变量存储依赖关系,这个变量叫做 targetMap
同时引进了一个新概念 effect,它与 Vue2.0 的 Watcher 差不多,但是概念有些转换,从 监听者 变成了 副作用,指的是值 (对应依赖) 改变后会发生的副作用
targetMap数据类型
1 | type Dep = Set<ReactiveEffect> |
targetMap 的 key 指向的是 A 和 B 所在的对象 DatatargetMap 的 value 指向的是KeyToDepMap
KeyToDepMap的 key 指向的是被响应式数据的键值
KeyToDepMap的 value 存放 effect 里面的 Watcher 集合

effect
effect 作用相当于 Vue2.0 里面的 Watcher,做的事情相对而言化繁为简
注意看这里
computed为何是惰性的答案在这里
第13行 立即执行副作用函数以触发依赖的getter 从而依赖收集1
2const effectStack: ReactiveEffect[] = [] // 全局 effect 栈
let activeEffect: ReactiveEffect | undefined // 当前激活的 effect
1 | export function effect<T = any>( |
createReactiveEffect
1 | function createReactiveEffect<T = any>( |
reactiveEffect
18行关键点 真正触发副作用函数的地方
前置变量或者函数let shouldTrack = true // 是否允许依赖收集1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28function reactiveEffect(): unknown {
// 如果副作用已经被暂停,则优先执行其调度器,再运行函数本体
if (!effect.active) {
return options.scheduler ? undefined : fn()
}
// 如果当前副作用未在运行的时候才进入
if (!effectStack.includes(effect)) {
// 先清除旧的依赖关系
cleanup(effect)
try {
// 开启全局 shouldTrack,允许依赖收集
enableTracking()
// 压栈 加入运行中的副作用堆栈
effectStack.push(effect)
// 确认当前副作用,Vue2.0 里的 target
activeEffect = effect
// 执行函数 调用fn()时可能会触发get方法,此时会触发上面get中调用的track函数
return fn()
} finally {
// 出栈
effectStack.pop()
// 恢复 shouldTrack 开启之前的状态
resetTracking()
// 将当前副作用转交给上一个或者置空
activeEffect = effectStack[effectStack.length - 1]
}
}
}
track
会在数据挟持的 get / has / ownKeys 中调用,这里前面已经敲重点了。
前置依赖1
2
3
4
5
6
7export const enum TrackOpTypes {
GET = 'get',
HAS = 'has',
ITERATE = 'iterate'
}
const effectStack: ReactiveEffect[] = [] // 全局 effect 栈
let activeEffect: ReactiveEffect | undefined // 当前激活的 effect
1 | export function track(target: object, type: TrackOpTypes, key: unknown) { |
第三步 派发更新
只需要在值发生变动的时候,从依赖关系中取出对应的副作用集合,触发effect副作用函数即可。
我们可以在上面数据挟持中的 set / deleteProperty发现派发更新的函数 trigger 的调用。
前置依赖
1 | export const enum TriggerOpTypes { |
trigger
1 | // 根据target为key值找到对应的存储集合中的effect依次执行,直接看到最后一句 |
到这里 是不是vue3.0响应式原理的面纱就揭开了~
了解了这些,再去看ref 和 computed 实现就简单了,实际上还是对reactive和effect的调用。去看看~