《Vue.js设计与实现》——响应式系统的基本实现

361 阅读6分钟

前言

网上冲浪时常被推荐霍春阳《Vue.js设计与实现》,Vue.js也是目前自己能吃上一口饭的主要工具,所以前段时间就开始阅读这本书,到目前为止也阅读了差不多三分之二的内容(👽宇宙安全声明:电子书是方便阅读,我确实是买了正版书的)
image.png
如果有条件的话非常推荐看这本书的原版,真的写得通俗易懂。

写下文章巩固看过的知识点(其实也是后面的章节有点看不下去了,等会后面不看了前面的又忘掉了,双输!😭),第一篇文章记录非常核心的——响应式系统的基本实现。

可能需要预先了解的概念:

  • Proxy
  • Set
  • Map
  • WeakMap

响应式数据和副作用函数概念

副作用函数

什么是副作用函数?例如document.body.innerText是浏览器中全局能够访问到的变量,如果在某个函数中修改了这个值,毫无疑问这个行为会影响到其他使用了这个值的地方,那么这个函数就可以被称为一个副作用函数。当然,对于自己声明的全局变量也是同样的道理。

function effect() {
    document.body.innerText = 'hello vue3'
}

响应式数据

什么是响应式数据?当数据发生变化时, 与其有联系的副作用函数可以被执行,那么这个数据就可以称为响应式数据。如下面的例子, effectFn中使用到了obj.text,那么如果obj.text发生改变后, effectFn也能够被执行,那么这个数据就是响应式数据。

let obj = {
    text: 'hello responsive data'
}

function effectFn() {
    document.body.innerText = obj.text
}

effectFn()
obj.text = 'hello vue3'

响应式数据的基本实现

在上面的例子中,effectFn执行过程中obj.text会被读取(get),如果我们可以能实现obj.text被读取时把effectFn收集起来,在obj.text设置(set) 时再执行被收集的函数,那么就基本实现了响应式数据,如下:

// bucket是用于收集副作用函数的变量
let bucket = new Set()
let obj = { text: 'hello responsive data' }

const proxyObj = new Proxy(obj, {
    get: function (target, key) {
        // 变量读取时将副作用函数收集下来,目前这里是写死了函数effectFn
        bucket.add(effectFn)
        return target[key]
    },
    set: function (target, key, val) {
        target[key] = val
        // 变量赋值时依次执行收集的副作用函数
        for (const item of bucket) {
            item()
        }
        // set中需要返回true
        return true
    }
})

function effectFn() {
    // 这边需要注意,要操作的是代理后的对象proxyObj
    document.body.innerText = proxyObj.text
}

// 需要执行一次effectFn,否则你怎么敢说你和proxyObj有关系的,蚝跌油
effectFn()

// 2s后修改数据
setTimeout(() => {
    proxyObj.text = 'hello vue3'
}, 2000)

目前实现的效果如下:

动画.gif

上面的实现中存在的不足有:

  1. 收集副作用函数时是硬编码将effectFn收集起来的,改变了函数名称的话get中的具体实现也需要做更改
  2. 目前是整个代理对象和副作用函数建立了联系,具体到某一个字段,例如obj的text字段和副作用函数并没有建立联系。试想一下我的obj中有另外的字段例如content,如果我更改了content的值,set的过程很明显也会执行一遍,那么收集到的副作用函数也会执行一遍,但副作用函数中其实没有使用到这个字段,这和我们的期望并不符合。

下面我们尝试解决这两个问题。

实现一个较为完善的响应式系统

响应式系统工作的基本流程:

  • 读取变量时,将副作用函数收集
  • 设置变量时,执行收集的副作用函数

函数收集时是硬编码的不足

先解决上一节中收集副作用函数是硬编码的不足,为此我们添加一个变量用于记录当前正在执行的函数,并将副作用函数用effect包裹起来,用该函数来执行副作用函数,如下:

// 全局声明一个变量用于记录当前正在执行的副作用函数
let activeEffectFn = null
function effect(fn) {
    activeEffectFn = fn
    fn()
    activeEffectFn = null
}

修改代理规则如下:

const proxyObj = new Proxy(obj, {
    get: function (target, key) {
        if (activeEffectFn) {
            // 当前正在执行的fn被收集
            bucket.add(activeEffectFn)
        }
        return target[key]
    },
    set: function (target, key, val) {
        target[key] = val
        for (const item of bucket) {
            item()
        }
        return true
    }
})

effect(() => {
  document.body.innerText = proxyObj.text
})

setTimeout(() => {
    proxyObj.text = 'hello vue3'
}, 2000)

现在整个过程为:

  1. 执行effect,具体的函数fn被传递过来,activeEffectFn被赋值
  2. 执行传递具体的函数fn,此时触发get,fn被收集
  3. get执行完毕,fn执行完毕,activeEffectFn置空
  4. 2s后修改值,set被触发,依次执行收集到的函数

key与函数之间无联系

然后存储函数的时候需要在具体的key和副作用函数之间建立联系,为此我们需要建立新的存储副作用函数的数据结构,达到收集时按key收集,触发时按key寻找对应的副作用函数集合的效果。

重新设计后的存储结构如下:

  1. weakMap中的每一项的key是需要代理的对象(target-1,target-2...)
  2. key-1,key-2...是对象中的具体的key,具体值是对应的副作用函数集合
weakMap: {
  target-1: {
    key-1: Set(effect-1, effect-2, ..., effect-n),
    key-2: Set(effect-1, effect-2, ..., effect-n),
    ...
  },
  target-2: {
    key-1: Set(effect-1, effect-2, ..., effect-n),
    key-2: Set(effect-1, effect-2, ..., effect-n),
    ...
  }
}

image.png

图源《Vue.js设计与实现》

新增track函数用来收集副作用函数,新增trigger用来触发副作用函数

// 所有对象的副作用集合
const targetBucket = new WeakMap()

const proxyObj = new Proxy(obj, {
    get: function (target, key, receiver) {
        // 需要把副作用函数按照key放进set里
        track(target, key, receiver)
        return target[key]
    },
    set: function (target, key, newVal, receiver) {
        target[key] = newVal
        // 获取key的对应的副作用函数,并执行
        trigger(target, key, newVal, receiver)
        return true
    }
})

/**
 * 副作用函数收集
 */
function track(target, key, receiver) {
    // 没有正在执行的副作用函数直接返回, 无需收集
    if (!activeEffectFn) return
    let depsMap = targetBucket.get(target)
    
    if (!depsMap) {
        // 以每一个需要代理的对象为key,没有的话就新建
        targetBucket.set(target, (depsMap = new Map()))
    }

    let deps = depsMap.get(key)
    if (!deps) {
      	// 以对象的key为key,存储该key对应的所有副作用函数
        depsMap.set(key, (deps = new Set()))
    }

    deps.add(activeEffectFn)
}

/**
 * 副作用函数触发
 */
function trigger(target, key, newVal, receiver) {
    const depsMap = targetBucket.get(target)
    if (!depsMap) return
    const effects = depsMap.get(key)
    effects && effects.forEach(fn => {
        fn()
    })
}

小结

由此我们就实现了一个最基本的响应式系统,当时这个系统自然还有其他的问题,后面我们再继续完善这个系统。有什么写的不对的地方或者什么疑问可以留言,希望文章有帮助到你~