Vue3 设计响应式系统(一),了解响应式系统!

133 阅读3分钟

前言

本文为记录笔者学习《Vue.js设计与实现》一书所写,同时也希望可以给正在学习Vue.js 的朋友一些参考。
受限于篇幅,笔者将会发布三到四篇文章做成一个系列。

读此系列文章你可以收获什么

从了解Vue3 响应式原理,到设计一个完善的响应式系统。
注:笔者将尽可能保证所写代码都可运行。

让我们开始吧

副作用函数

在我们学习响应式之前,让我们来看看副作用函数

function effect() {
document.body.innerHTML = 'Hello World'
}

当我们执行effect 函数的时候,它会设置body 文本内容,但是除了effect 函数外,其他任何函数可以对body 内容进行读取和修改。这时我们就说effect 函数产生了副作用。

理解了副作用函数后,我们再来看响应式

const obj = { text: 'Hello Vue3' }
function effect() {
//effect 函数执行的时候会读取obj.text
document.body.innerHTML = obj.text
}

我们希望在obj.text 值发生改动的时候,body 的文本内容也跟着变化(这个需求听起来似乎很合理)。

obj.text = 'Hello Aganivi'

例如这里将obj.text 的值修改了,如果我们能将effect 函数再执行一次,body 文本将会被重新赋值。

那我们应该怎么做呢?
我们可以使用一个桶,将effect 函数装进去,然后去监听对象obj 的变化,当obj.text 值发生改变的时候,将桶里的effect 函数取出来执行。听起来似乎解决了我们的问题,那么我们应该用什么监听对象obj呢?

使用Proxy 代理

关于Proxy 这里我们只需要知道,它提供了监听功能,让我们可以在对Proxy 对象进行读取和设置操作的时候进行拦截,如下面代码

// 全局变量
let result
// 副作用函数
function effect() {
  result = obj.text
}
// 创建‘桶’用于存放effect 函数
const bucket = new Set()
// 原始数据
const data = { text: 'Hello Vue3' }
// 原始数据被代理后的对象
const obj = new Proxy(data, {
  //拦截读取操作
  get(target, key) {
    // 将effect 函数存入‘桶’中
    bucket.add(effect)
    // 返回属性值
    return target[key]
  },
  // 拦截设置操作
  set(target, key, newVal) {
    target[key] = newVal
    // 将effect 函数重新执行
    bucket.forEach((effect) => effect())
    // 返回true 代表操作成功
    return true
  },
})

// 下面代码用于检验响应式是否成功
// 执行副作用函数
effect()
// 打印全局变量
console.log(result)
setTimeout(() => {
  // 重新赋值
  obj.text = 'Hello Aganivi'
  // 二次打印全局变量
  console.log(result)
}, 1000)

为了方便运行代码观察结果,我们将effect 函数稍微做了一些修改,如果你想观察effect 函数执行情况,可以在effect 函数中增添一些打印结果。

这里我们新建了bucket 来存放effect 函数,在对对象obj 进行读取操作的时候(即执行effect 函数的时候)我们将effect 函数加入桶中。然后在对obj.text 赋值的时候,将bucket 中的函数取出来执行。

这样一个简易的响应式系统便实现了。但是目前的实现还有许多缺陷,例如我们直接通过名字(effect)来获取副作用函数,这种硬编码方式极其不灵活,我们完全将副作用函数随意名字,甚至使用匿名函数。下篇文章我们再来讲讲如何进行优化。