Skip to content
On this page

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
}

依照上述函数,依赖关系的绑定步骤如下:

  1. 当副作用函数自执行的时候,读取了响应式数据r1.c,触发了r1的代理对象get方法
  2. 尝试获取r1depsMap,为空,创建一个新的depsMap
  3. 尝试获取c下的effect集合,为空,创建一个新的dep
  4. activeEffect添加到dep

建立的关系图如下:r1-c

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)())
}

触发的操作就很简单了,步骤如下:

  1. 比较值是否发生改变,是的话执行trigger
  2. desMap中取出和target.key相关的effects并执行

WatchEffect

这样我们就实现了基本的getset拦截器,实现了简单的响应式对象。不知道你有没有忘记activeEffect这个变量,他其实就是当前活跃的副作用函数,那么如何把它绑定到activeEffect上呢?我们来实现一个简单的watchEffect

ts
function watchEffect(effect: any) {
    const run = () => {
        effect()
    }
    activeEffect = run
    run()
}

watchEffect接受一个effect函数,他的逻辑很简单,把effect绑定到activeEffect上并执行effect。这样在我们执行track之前activeEffect就是我们希望的副作用函数了,从而我们可以把它和响应式数据联系起来。

ITERATE

到此为止我们已经对响应式数据的getset行为进行了代理,实现了读取时的绑定和赋值时的触发执行,但是我们会发现下面的代码只会执行一次。

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就可以了。现在我们对settrigger进行改造:

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需要执行的额外动作,当typeTriggerOpTypes.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
    }
    ...
}

当我们删除的时候,抛出DELETEtype来通知triggertrigger就会在删除的时候执行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实现了迭代器循环的响应式。

Date: 2022/09/06

Authors: 徐安海

Tags: 教程、vue3、源码解析