vue3响应式系统(1)

32 阅读4分钟

前言

为了实现响应式系统,我们先明确2个名词的概念,响应式数据副作用函数

  • 响应式数据:当数据发生改变时,使用该数据的函数会重新执行。
  • 副作用函数:直接或间接地影响其他函数的执行,比如在函数里面读取或修改了其他函数也可以访问的变量,如全局变量。

我们先看以下代码:

    // 01
    const obj = { name: 'vue', version: 3 }

    const effect = () => {
       document.body.innerHTML = obj.name
    }

    effect()

    obj.name = 'new vue'

effect执行时,读取了obj的属性,我们希望obj发生改变时,会重新执行effect函数,但显然obj只是一个普通的对象,根本无法实现这个目标,于是我们利用ES6的新特性Proxy,把obj变成响应式数据。

响应式数据的基本实现

上面的代码涉及到数据的读取和修改:执行时effect里面读取了数据(obj.name),然后修改obj.name,因此我们对obj的读取和修改分别做拦截:

  • 当读取时,把effect放进一个“桶”里
  • 当设置时,把“桶”里面的effect拿出来执行

因此,我们按照上面的思路实现以下代码:

// 02

const bucket = new Set()
const obj = { name: 'vue', version: 3 }

const data = new Proxy(obj, {
    get(target, key, receiver) {
        bucket.add(effect)
        return Reflect.get(target, key, receiver)
    },
    set(target, key, value, receiver) {
        Reflect.set(target, key, value, receiver)
        for (const effect of bucket) {
            effect()
        }
        return true
    }
})

const effect = () => {
   document.body.innerHTML = data.name
}
effect()

setTimeout(() => {
    data.name = 'new vue'
}, 1000);

发现1s后修改了dataeffect重新执行了,到这里,我们就实现了一个基本的响应式系统,但是上面的代码仍然有很多待改善的地方,接下来我们就一步一步去完善它。

在那之前,我们大脑里要把一句话深深印在脑海里:(重要的事情说3遍)

副作用函数执行时建立与数据之间的联系

副作用函数执行时建立与数据之间的联系!!

副作用函数执行时建立与数据之间的联系!!!

好了,我们可以开始后面的内容了。

完善的响应式系统

1. 解决副作用函数硬编码的问题

上面的代码我们是使用编码的形式来把副作用函数effect放进“桶”里面的,因此我们要首先解决这个问题,于是通过提供注册副作用函数机制来解决,我们改写effect代码如下:

// 03
let activeEffect = null
const effect = (fn) => {
    // 将当前的副作用函数赋给activeEffect
    activeEffect = fn
    // 执行副作用函数
    activeEffect()
}

我们通过向effect注册一个函数,然后把当前执行的副作用函数赋给全局变量activeEffect,一是为了解决硬编码的问题,二则是在收集对象的副作用函数时直接读取全局变量activeEffect。 于是我们需要修改data的写法如下:

const data = new Proxy(obj, {
    get(target, key, receiver) {
        bucket.add(activeEffect) // 修改为添加activeEffect
        return Reflect.get(target, key, receiver)
    },
    set(target, key, value, receiver) {
        Reflect.set(target, key, value, receiver)
         for (const effect of bucket) {
             effect()
         }
        return true
    }
})

测试:

// 向effect注册一个匿名函数
effect(()=> {
    document.body.innerHTML = data.name
})

setTimeout(() => {
    data.name = 'new vue'
}, 1000);

这样就可以了。

2. 不同对象的不同属性之间的分别关联的副作用函数

大家可能注意到了,就是当我们修改version属性的时候,同样会执行匿名副作用函数,而该函数里面只读取了对象的name属性,我们希望副作用函数的执行只跟与它们关联的对象属性绑定,于是我们很容易得到以下关系:这里可能有n个对象(target),一个对象有n个属性(key),一个属性关联n个副作用函数(deps)。据上关系设计如下数据格式:

const bucket = new WeakMap() // 键值为target,值为depsMap
const depsMap = new Map() // 键值为key, 值为deps
const deps = new Set() // 存储副作用函数,我们也叫依赖集合

因此我们根据上面的数据结构和逻辑重新修改响应式数据:

// 04
const data = new Proxy(obj, {
    get(target, key, receiver) {
        if (!activeEffect) return Reflect.get(target, key, receiver)
        // 获取该对象的所有依赖映射
        let depsMap = bucket.get(target)
        if (!depsMap) {
            bucket.set(target, (depsMap = new Map()))
        }
        // 获取对应key的依赖集合
        let deps = depsMap.get(key)
        if (!deps) {
            depsMap.set(key, (deps = new Set()))
        }
        deps.add(activeEffect)
        return Reflect.get(target, key, receiver)
    },
    set(target, key, value, receiver) {
        Reflect.set(target, key, value, receiver)
        const deps = bucket.get(target)?.get?.(key)
        deps && deps.forEach(fn => fn())
        return true
    }
})

到这里,我们就已经基本实现了响应式系统,下一篇我们将继续改造。

完整代码查看