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

前言

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

如果有条件的话非常推荐看这本书的原版,真的写得通俗易懂。

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

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

  • Proxy
  • Set
  • Map
  • WeakMap

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

副作用函数

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

javascript 复制代码
function effect() {
    document.body.innerText = 'hello vue3'
}

响应式数据

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

javascript 复制代码
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) 时再执行被收集的函数,那么就基本实现了响应式数据,如下:

javascript 复制代码
// 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)

目前实现的效果如下:

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

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

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

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

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

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

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

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

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

修改代理规则如下:

javascript 复制代码
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
    }
})

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

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

现在整个过程为:

  1. 执行watchEffectFn,具体的函数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,具体值是对应的副作用函数集合
javascript 复制代码
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),
    ...
  }
}

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

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

javascript 复制代码
// 所有对象的副作用集合
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()
    })
}

小结

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

相关推荐
崔庆才丨静觅9 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby606110 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了10 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅10 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅11 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅11 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment11 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅11 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊11 小时前
jwt介绍
前端
爱敲代码的小鱼11 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax