深入响应式系统
宇宙免责声明:本文不是知识梳理,旨在抛砖引玉!!!
前置
Proxy
想象你是一个高级小区的业主,你的公寓门口配备了诸多门卫,以监控和干预所有进出公寓的行为
Intro
一个 Proxy
对象(门卫)包装另一个对象并拦截诸如读取/写入属性和其他操作,可以选择自行处理它们,或者透明地允许该对象处理它们。
1
| let proxy = new Proxy(target, handler)
|
target
—— 是要包装的对象(公寓),可以是任何东西,包括函数。
handler
—— 带有捕捉器(即拦截操作的方法)的对象。比如 get
捕捉器用于读取 target
的属性,set
捕捉器用于写入 target
的属性,等等。
工作机理
proxy到底用来拦截什么?
对于对象的大多数操作,JavaScript 规范中有一个所谓的“内部方法”,它描述了最底层的工作方式。例如 [[Get]]
,用于读取属性的内部方法,[[Set]]
,用于写入属性的内部方法。
对每一个内部方法,都有一个对应的Proxy handler去拦截这些方法的调用:proxy规范
一个简单的举例
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 28
| const apartment = { temperature: 22, residents: ['Alice', 'Bob'] };
const apartmentProxy = new Proxy(apartment, { get(target, prop) { console.log(`有人想要查看${prop}的信息`); return target[prop]; }, set(target, prop, value) { console.log(`有人想要修改${prop}为${value}`); if (prop === 'temperature' && (value < 18 || value > 30)) { console.log('温度设置被拒绝:超出合理范围!'); return false; } target[prop] = value; return true; } });
|
Reflect
Intro
Reflect
是一个内建对象,前面所讲过的内部方法,例如 [[Get]]
和 [[Set]]
等,都只是规范性的,不能直接调用。Reflect
对象使调用这些内部方法成为了可能。对于每个可被 Proxy
捕获的内部方法,在 Reflect
中都有一个对应的方法,其名称和参数与 Proxy
捕捉器相同。
因此,我们可以通过 Reflect
将操作转发给原始对象。
Reflect有什么优势?
你可能会问,既然我们总是要调用内部方法的,为什么还要用Reflect在proxy中调这些方法呢?
这里以Reflect.get为例阐述其在处理继承关系的优势
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| let owner = { _name: "Eason", get name() { return this._name; } };
let ownerProxy = new Proxy(owner, { get(target, prop, receiver) { return target[prop]; } });
let guest = { __proto__: ownerProxy, _name: "Guest" };
console.log(guest.name);
|
我们希望读取 guest.name
返回 Guest
,而不是 Eason
,问题出在哪里呢?
分析:
- 读取
guest.name
,由于 guest
对象没有name属性,搜索转到其原型 ownerProxy
- 从代理读取name时,get handler触发,从owner对象返回
owner[prop]
- 此时的prop是一个getter,它将在
this=target
上下文中返回 _name
- 如何把上下文传递给 getter?这本质其实是一个this指向问题,对一般的数据属性我们可以用call/apply/bind处理,但getter是访问器属性,它不能“被调用”,只能被访问。
解决:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| let owner = { _name: "Eason", get name() { return this._name; } };
let ownerProxy = new Proxy(owner, { get(target, prop, receiver) { return Reflect.get(target, prop, receiver); } });
let guest = { __proto__: ownerProxy, _name: "Guest" };
console.log(guest.name);
|
我们需要用到get handler的第三个参数 receiver
,它保留了对正确this(Guest)的引用,通过 Reflect.get
传递给getter
Proxy局限性
Vue
为突出逻辑,以下部分代码在源码的基础上有适当改动
Vue2
在初始化阶段,Vue2会对配置对象中的不同属性做相关处理
data
和 props
中的每个属性变成响应式属性,每个属性内部持有一个Dep依赖收集器
- 对
computed
计算属性,内部创建computed watcher,每个computed watcher持有一个Dep依赖收集器
- 对
watch
,内部创建user watcher,即用户自定义的一些watch,data
or computed
中的属性Dep会存储与自己相关的watcher
侦测数据变化
Observer
在Vue2中,利用 Observer
类和 Object.defineProperty
,通过劫持对象属性,实现侦测数据变化
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 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| export class Observer { dep: Dep vmCount: number
constructor(public value: any, public shallow = false) { this.dep = new Dep() this.vmCount = 0 def(value, '__ob__', this) if (isArray(value)) { if (hasProto) { ;(value as any).__proto__ = arrayMethods } else { for (let i = 0, l = arrayKeys.length; i < l; i++) { const key = arrayKeys[i] def(value, key, arrayMethods[key]) } } if (!shallow) { this.observeArray(value) } } else { const keys = Object.keys(value) for (let i = 0; i < keys.length; i++) { const key = keys[i] defineReactive(value, key, NO_INITIAL_VALUE, undefined, shallow, mock) } } }
observeArray(value: any[]) { for (let i = 0, l = value.length; i < l; i++) { observe(value[i], false) } } }
|
defineReacitve
对象的属性定义响应式,Vue2使用 defineReactive
将一个对象转化成可观测对象
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 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
| export function defineReactive( obj: object, key: string, val?: any, shallow?: boolean, ) { const dep = new Dep()
const property = Object.getOwnPropertyDescriptor(obj, key) if (property && property.configurable === false) { return }
if (arguments.length === 2) { val = obj[key]; }
let childOb = shallow ? val && val.__ob__ : observe(val, false) Object.defineProperty(obj, key, { enumerable: true, configurable: true,
get: function reactiveGetter() { const value = getter ? getter.call(obj) : val
dep.depend()
if (childOb) { childOb.dep.depend() if (isArray(value)) { dependArray(value) } } return isRef(value) && !shallow ? value.value : value },
set: function reactiveSetter(newVal) { const value = getter ? getter.call(obj) : val if (!hasChanged(value, newVal)) { return } val = newVal childOb = shallow ? newVal && newVal.__ob__ : observe(newVal, false) dep.notify() } })
return dep }
|
分析:
- 通过以上方法仅只能观测Object数据的取值getter和修改更新已有数据setter,为解决这一点,Vue2增加了两个全局API,
Vue.set(vm.$set)
和 Vue.delete(vm.$delete)
Object.defineProperty
仅可以检测数组的下标变化(即通过下标获取某个元素和修改元素值),但无法检测数组长度变化,因此在 array.ts 对 push
、unshift
、splice
进行了特殊处理,也是为什么Observer需要特判
Object.defineProperty
需要遍历对象的每个属性,假如属性值也是对象,则需要递归地遍历,性能较低
依赖收集与更新
数据变的可观测后,我们可以知道数据什么时候发生了变化,那么现在的问题就是当某个数据变化后,我们该通知哪部分视图进行更新?
Dep
对象的每个属性都有一个Dep依赖管理器,其串联了属性和sub(或effect)
1 2 3 4 5 6 7 8 9 10
| Dep { subs: [Watcher1, Watcher2, ...] }
Watcher { deps: [Dep1, Dep2, ...], depIds: Set(1, 2, ...) }
|
当某个属性被访问,其通过depend收集访问它的watcher,当其数据变化,通过notify遍历更新所有依赖它的watcher
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 28 29 30 31 32 33 34
| export default class Dep { static target?: DepTarget | null subs: Array<DepTarget | null>
constructor() { this.subs = [] }
addSub(sub: DepTarget) { this.subs.push(sub) }
depend() { if (Dep.target) { Dep.target.addDep(this) } }
notify() { const subs = this.subs.filter(s => s) as DepTarget[] for (let i = 0, l = subs.length; i < l; i++) { const sub = subs[i] sub.update() } } }
|
Vue3
更新?
Vue2中,响应式系统主要是为对象设计的,假如我们需要处理一个基本类型(Number,String…)的数据,需要将其包装在对象中。因此Vue3在reactive的基础上推出了ref,它提供了一种更自然的方式来直接处理任意类型。
使用 Proxy
直接代理对象,直接监听整个对象
- 不需要重写array方法
- 可监听属性的新增和删除(不再使用set和delete)
相较于使用数组进行watcher的存储,采用 WeakMap<target, Map<key, Set<effect>>>
结构,可自动垃圾回收
数据代理
ref
ref函数本身很简单,就是直接调用 createRef
1 2 3
| export function ref(value?: unknown) { return createRef(value, false) }
|
createRef
1 2 3 4 5 6 7
| createRef`中对传入的rawValue进行判断,假如已经是响应式,直接返回,不是则作为参数传入`RefImpl function createRef(rawValue: unknown, shallow: boolean) { if (isRef(rawValue)) { return rawValue } return new RefImpl(rawValue, shallow) }
|
RefImpl
RefImpl
中维护一个 _value
,其值就是平时我们调用.value获取ref的值;维护一个 _rawValue
,用于和set的 newValue
比较,判断是否触发 trigger
进行响应式更新;维护一个Dep,记录依赖该属性的Subscriber(在Vue2中叫Watcher)
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 28 29 30 31 32 33 34 35
| class RefImpl<T = any> { _value: T private _rawValue: T
dep: Dep = new Dep()
public readonly [ReactiveFlags.IS_REF] = true public readonly [ReactiveFlags.IS_SHALLOW]: boolean = false
constructor(value: T, isShallow: boolean) { this._rawValue = isShallow ? value : toRaw(value) this._value = isShallow ? value : toReactive(value) this[ReactiveFlags.IS_SHALLOW] = isShallow }
get value() { this.dep.track() return this._value }
set value(newValue) { const oldValue = this._rawValue const useDirectValue = this[ReactiveFlags.IS_SHALLOW] || isShallow(newValue) || newValue = useDirectValue ? newValue : toRaw(newValue) if (hasChanged(newValue, oldValue)) { this._rawValue = newValue this._value = useDirectValue ? newValue : toReactive(newValue) this.dep.trigger() } } }
|
- 当newValue是Proxy对象,调用
toRaw
将其转成originalObj
- _value调用
toReactive
获得,由下面代码可看出其作用即根据value的type进行区别处理,若是Object,交给reactive处理,基础类型则直接返回,即处理对象之前加了一条分支判断
1 2
| export const toReactive = <T extends unknown>(value: T): T => isObject(value) ? reactive(value) : value
|
createReactiveObject
到这其实就进入在Vue3中使用reactive声明变量的流程(即 const var = reactive({count: 1})
),函数reactive在做完简单的可读性处理后直接调用 createReactiveObject
,其核心逻辑就是为target创建一个Proxy对象,并根据 targetType
传入不同的 handler
targetType:
- COMMON:Object、Array
- COLLECTION:Map、Set、WeakMap、WeakSet
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 28 29 30 31 32 33 34
| function createReactiveObject( target: Target, baseHandlers: ProxyHandler<any>, collectionHandlers: ProxyHandler<any>, proxyMap: WeakMap<Target, any>, ) { if (!isObject(target)) { return target } if (target instanceof Proxy) { return target } const existingProxy = proxyMap.get(target) if (existingProxy) { return existingProxy }
const targetType = getTargetType(target) if (targetType === TargetType.INVALID) { return target }
const proxy = new Proxy( target, targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers, ) proxyMap.set(target, proxy) return proxy }
|
依赖收集与更新
和Vue2一样,Vue3同样通过track进行依赖收集,trigger进行派发更新,只不过二者是在handler执行
Link
Vue2通过数组实现属性和watcher之间之间的双向依赖收集,而Vue3采用链表实现双向链接,进一步降低了依赖清理的时间复杂度(O(n)–>O(1)),提高了依赖管理的性能
1 2 3 4 5 6 7 8
| Link { sub: Effect, dep: Dep, nextDep: Link, prevDep: Link, nextSub: Link, prevSub: Link }
|
Effect
我们以下涉及多层 effect 嵌套例子为例,走一遍依赖收集与更新的流程:
1 2 3 4 5 6 7
| const count = ref(0) const double = computed(() => count.value * 2) const triple = computed(() => double.value * 1.5)
watchEffect(() => { console.log(`Triple value is: ${triple.value}`) })
|
effect是subscriber接口的具体实现,其中包含了批处理机制、依赖清理等逻辑,其核心 ReactiveEffect
的主要逻辑为 run
方法,其余与生命周期,自定义调度等相关的属性和方法这里不做进一步阐述
- 首先,watchEffect创建了一个
ReactiveEffect
,double和triple同理,当执行watchEffect时,调用其 run
方法:
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
| run(): T { cleanupEffect(this)
prepareDeps(this)
const prevEffect = activeSub const prevShouldTrack = shouldTrack
activeSub = this shouldTrack = true
try { return this.fn() } finally { cleanupDeps(this) activeSub = prevEffect shouldTrack = prevShouldTrack } }
|
- Vue3使用全局变量来跟踪当前正在执行的effect:
1 2
| export let activeSub: Subscriber | undefined export let shouldTrack = true
|
- 由run代码可以看出,当一个effect开始执行时,会将自己设置为activeSub(当前活跃的effect)
- 每次run的时候都需要清理之前的依赖,因为每次所需的依赖可能有区别
- 当访问triple.value时,其还未计算过值,触发triple的effect运行,double同理,给出effect嵌套执行依赖和执行栈变化:
- count -> double -> triple -> watchEffect
- 执行栈变化:
- 初始:
activeSub: null
,shouldTrack: true
- watchEffect:
activeSub: watchEffectInstance
,shouldTrack: true
- triple:
activeSub: tripleEffectInstance
,shouldTrack: true
- double:
activeSub: doubleEffectInstance
,shouldTrack: true
- 通过保存和恢复上下文(activeSub & shouldTrack),可以很好的处理嵌套effect
- 对于每一个正在执行的effect,由于将自己暴露为全局变量activeSub,其依赖可以在track的时候轻松获取
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 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
| export function track(target: object, key: unknown): void { if (shouldTrack && activeSub) { 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 Dep())) dep.map = depsMap dep.key = key } dep.track() } }
track(): Link | undefined { if (!activeSub || !shouldTrack || activeSub === this.computed) { return }
let link = this.activeLink if (link === undefined || link.sub !== activeSub) { link = this.activeLink = new Link(activeSub, this)
if (!activeSub.deps) { activeSub.deps = activeSub.depsTail = link } else { link.prevDep = activeSub.depsTail activeSub.depsTail!.nextDep = link activeSub.depsTail = link }
addSub(link) } else if (link.version === -1) { link.version = this.version
if (link.nextDep) { const next = link.nextDep next.prevDep = link.prevDep if (link.prevDep) { link.prevDep.nextDep = next }
link.prevDep = activeSub.depsTail link.nextDep = undefined activeSub.depsTail!.nextDep = link activeSub.depsTail = link
if (activeSub.deps === link) { activeSub.deps = next } } } return link }
|
- 假设此时更新了
count.value = 1
,此时触发trigger
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 28 29 30 31 32 33 34 35 36 37 38
| export function trigger( target: object, key?: unknown, ): void { const depsMap = targetMap.get(target) if (!depsMap) return startBatch() const dep = depsMap.get(key) if (dep) { dep.trigger() } endBatch() }
trigger(): void {
this.notify() }
notify(): void { startBatch() try { for (let link = this.subs; link; link = link.prevSub) { if (link.sub.notify()) { ;(link.sub as ComputedRefImpl).dep.notify() } } } finally { endBatch() } }
|
- 这里对dep的subs进行逆序遍历,即watchEffect->triple->double,主要为了配合批处理机制(batch processing):
- 假如我们正向遍历,更新double的时候会触发triple更新,triple又会触发watchEffect更新…如此往复需要(On^2)复杂度才可更新完嵌套执行的effect
- 而逆序遍历,先触发watchEffect更新,维护一个链表,采用头插法不断往其中加入effect,最后只需要On从头遍历一遍该链表即可完成嵌套effect的更新,且保证更新顺序按照subs原始的存储顺序更新
- 由以上代码可得出computed会被优先处理,再处理普通effect
- 至此,此时double被更新成2,triple更新成3,打印Triple value is: 3
参考资料
Vue2源码:https://github.com/vuejs/vue
Vue3源码:https://github.com/vuejs/core
Vue官方文档:https://cn.vuejs.org/guide/extras/reactivity-in-depth#how-reactivity-works-in-vue
特别鸣谢web某小动物:https://www.cheems.life/blog/65