Appearance
vue3响应式基础
vue3的响应式贯穿整个开发周期,当我在愉快地使用框架提供的响应式方法时,试着更深入得了解它们的实现原理,所以本次分享会从reactive函数入手,带你揭开vue3响应式的底裤。本次分享基于vue-reactivity,如果你未接触过vue,可以先参考vue文档。
什么是响应式对象?
js
const r1 = reactive({})
watchEffect(()=>{
console.log(`get c => ${r1.c}`)
})
setTimeout(() => {
r1.c = 1
}, 0);
从上方的代码中可以看到我们定义了一个变量r1
,并在副作用函数中引用了该对象,在一段时间后将r1.c
赋值为1
。 通俗点来说,如果当r1
变化后,它相关联的副作用函数能重新执行,那么我们把r1
称为响应式对象。
响应式的收集和触发
当我们想在响应式对象发生变化的时候重新执行副作用函数,我们势必知道它们之间的关系。 它们的关系结构如图:
target
:响应式对象key
:对象的属性effect
:副作用函数
可以看到一个一个响应式对象下面关联着他的属性,属性才和副作用函数集建立关联。那么为什么不能直接把对象和函数进行关联呢? 这是为了精准执行只有相关属性所依赖的副作用函数,毕竟我们也不希望target.c
发生了变化,所有依赖target
的副作用函数全部重新执行。 我们可以看到相关的副作用函数是使用Set
来进行管理而不是数组,这是为了避免收集到重复的副作用,导致副作用重复执行。
实现reactive
想要监听一个对象,我们可以使用Proxy来帮助我们实现。
ts
function reactive(target: any) {
const proxy = new Proxy(target, {
get,
set,
})
return proxy
}
现在我们创建了一个代理对象函数,可以监听原始对象的基本操作(get代理了读取事件,set代理了赋值事件),首先我们来实现依赖的追踪。
Get
就像响应式对象的定义中说的,我们需要建立副作用函数和响应式对象的联系,其实就是将在副作用函数内读取过的响应式对象关联到它身上,下面我们来实现get
函数。
ts
type ActiveEffect = ((...params: any[]) => any)
let activeEffect: ActiveEffect | undefined
const targetMap = new WeakMap<any, KeyToDepMap>()
function get(target: any, key: string | symbol, receiver: object): boolean {
// 绑定依赖关系
track(target, key)
// 默认的返回行为
return Reflect.get(target, key, receiver)
}
// 为了更好的复用性,我们封装下track
function track(target: any, key: any) {
// 获取当前活动的副作用函数
if (!activeEffect) return
// 获取target相关的依赖Map
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
// 获取target[key]相关的依赖Set
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = new Set()))
}
dep.add(activeEffect)
activeEffect = undefined
}
依照上述函数,依赖关系的绑定步骤如下:
- 当副作用函数自执行的时候,读取了响应式数据
r1.c
,触发了r1的代理对象get方法 - 尝试获取
r1
的depsMap
,为空,创建一个新的depsMap
, - 尝试获取
c
下的effect集合,为空,创建一个新的dep
- 将
activeEffect
添加到dep
中
建立的关系图如下:
Set
当我们有了变量和函数的关系之后,下一步就是在合适的时间触发函数,那么什么是合适的时间呢?很简单,就是当该变量的值发生改变的时候。那么,如果监听变量的改变呢,我们只需要在set
中代理对象的赋值操作就可以了。下面我们来实现set
函数。activeEffect
我们将在下面介绍实现,你现在只需要知道他是当前的副作用函数就可以了。
ts
function set(target: any, key: string | symbol, value: unknown, receiver: object): boolean {
// 原先的值
const oldValue = target[key]
// 在触发前先设置值
const res = Reflect.set(target, key, value, receiver)
// 比较值是否发生改变
if (!Object.is(oldValue, value)) {
trigger(target, key)
}
return res
}
// 触发相关副作用函数
function trigger(target: any, key: any, type: TriggerOpTypes) {
const depsMap = targetMap.get(target)
if (!depsMap) {
return
}
const deps=depsMap.get(key)
// 遍历所有的依赖
deps?.forEach(effect => (effect as ActiveEffect)())
}
触发的操作就很简单了,步骤如下:
- 比较值是否发生改变,是的话执行
trigger
- 从
desMap
中取出和target.key
相关的effects
并执行
WatchEffect
这样我们就实现了基本的get
和set
拦截器,实现了简单的响应式对象。不知道你有没有忘记activeEffect
这个变量,他其实就是当前活跃的副作用函数,那么如何把它绑定到activeEffect
上呢?我们来实现一个简单的watchEffect
。
ts
function watchEffect(effect: any) {
const run = () => {
effect()
}
activeEffect = run
run()
}
watchEffect
接受一个effect
函数,他的逻辑很简单,把effect
绑定到activeEffect
上并执行effect
。这样在我们执行track
之前activeEffect
就是我们希望的副作用函数了,从而我们可以把它和响应式数据联系起来。
ITERATE
到此为止我们已经对响应式数据的get
和set
行为进行了代理,实现了读取时的绑定和赋值时的触发执行,但是我们会发现下面的代码只会执行一次。
ts
watchEffect(()=>{
let len=1
for(let k in r1){
len++
}
})
r1.a=1
这时因为迭代器不属于get
,所以我们要代理一个新的行为:ownKeys
。
ts
const ITERATE_KEY = Symbol()
function ownKeys(target: object): (string | symbol)[] {
track(target, ITERATE_KEY)
return Reflect.ownKeys(target)
}
因为迭代循环并没有指定的key,所以我们内部定义一个独立的ITERATE_KEY
来进行追踪和触发。
track
实现之后自然是找时机去触发trigger
,那么什么时候去执行迭代器
相关的副作用函数呢?因为迭代器
只关系key
,所以我们只需要在key
被删除或者新增的时候触发ITERATE_KEY
就可以了。现在我们对set
和trigger
进行改造:
ts
function set(target: any, key: string | symbol, value: unknown, receiver: object): boolean {
const oldValue = target[key]
// 添加TriggerOpType,告诉trigger触发的类型
const triggerType = hasOwn(target, key) ? TriggerOpTypes.SET : TriggerOpTypes.ADD
const res = Reflect.set(target, key, value, receiver)
if (!Object.is(oldValue, value)) {
trigger(target, key, triggerType)
}
return res
}
function trigger(target: any, key: any, type: TriggerOpTypes) {
const depsMap = targetMap.get(target)
if (!depsMap) {
return
}
const deps: (Dep | undefined)[] = [depsMap.get(key)]
switch (type) {
// 添加的时候触发ITERATE_KEY
case TriggerOpTypes.ADD:
deps.push(depsMap.get(ITERATE_KEY))
break
case TriggerOpTypes.SET:
break
}
if (deps.length) {
deps.forEach(dep => {
dep?.forEach(effect => (effect as ActiveEffect)())
})
}
}
在对象的key
新增时,我们需要触发ITERATE_KEY
,所以我们新增了TriggerOpTypes
来告诉trigger
需要执行的额外动作,当type
为TriggerOpTypes.ADD
时将ITERATE_KEY
相关的副作用函数添加到待执行的deps
中。
同样地,key
被删除的时候我们也需要触发ITERATE_KEY
,这时候我们需要补充一下删除的代理,使用deleteProperty
来实现。
ts
const hasOwn = (
val: object,
key: string | symbol
) => {
return Object.prototype.hasOwnProperty.call(val, key)
}
function deleteProperty(target: any, key: string | symbol): boolean {
const hadKey = hasOwn(target, key)
// 删除需要在触发前操作
const res = Reflect.deleteProperty(target, key)
if (res && hadKey) {
trigger(target, key, TriggerOpTypes.DELETE)
}
return res
}
export function trigger(target: any, key: any, type: TriggerOpTypes) {
...
switch (type) {
case TriggerOpTypes.ADD:
deps.push(depsMap.get(ITERATE_KEY))
break
// 删除时触发ITERATE_KEY
case TriggerOpTypes.DELETE:
deps.push(depsMap.get(ITERATE_KEY))
break
case TriggerOpTypes.SET:
break
}
...
}
当我们删除的时候,抛出DELETE
type来通知trigger
,trigger
就会在删除的时候执行ITERATE_KEY
。完整的reactive
实现如下:
ts
function reactive(target: any) {
const existingProxy = proxyMap.get(target)
if (existingProxy) return existingProxy
const proxy = new Proxy(target, {
get,
set,
deleteProperty,
ownKeys
})
proxyMap.set(target, proxy)
return proxy
}
至此,我们的for in
循环也能很好地执行了。
总结
在这节中我们了解了通过reactive
代理对象的基本实现,在get
时执行track
收集依赖,在set
时通过trigger
触发相关的副作用函数。
我们还实现了一个简单的WatchEffect
,进行副作用函数的绑定。
最后,我们使用自定义的ITERATE_KEY
实现了迭代器循环的响应式。