Vue响应式原理(1)-副作用函数和响应式数据

252 阅读3分钟

该系列文章主要参考了霍春阳老师的《Vue.js设计与实现》一书中Vue3响应式原理的相关章节。强烈推荐这本书,对于Vue3实现原理的解读清晰易懂,讲解深入浅出,堪称了解Vue框架实现原理一本宝典。

1.副作用函数和响应式数据

副作用函数指的是会产生副作用的函数。例如当函数执行时会设置body的文本内容,除了该副作用函数之外的任何函数都可以读取或设置body的文本内容。表明该副作用函数的执行会直接或间接影响其他函数的执行,这时我们说该函数产生了副作用。

假设在一个副作用函数中读取了某个对象的属性:

const obj = { text: 'hello world' }
function effect() {
    // effect 函数的执行会读取 obj.text
    document.body.innerText = obj.text
}

如上面的代码所示,副作用函数 effect 会设置 body 元素的innerText 属性,其值为 obj.text,当 obj.text 的值发生变化时,我们希望副作用函数 effect 会重新执行。如果能实现这个目标,那么对象 obj 就是响应式数据。

2.响应式数据实现

当副作用函数 effect 执行时,会触发字段 obj.text 的读取操作;当修改 obj.text 的值时,会触发字段 obj.text 的设置操作。所谓的响应式就是当我们对obj.text进行修改时,副作用函数会自动重新执行。自然的思路就是在读取obj.text时将effect存储起来,当设置obj.text时将effect取出并重新执行。

问题的关键变成了我们如何才能拦截一个对象属性的读取和设置操作。在 ES2015 之前,只能通过 Object.defineProperty 函数实现,这也是 Vue.js 2 所采用的方式。但是该方式存在着一些问题。因此在Vue.js 3中进行了改进,使用代理对象 Proxy 来实现响应式数据。

根据这个思路,我们可以实现一个简易版本的响应式数据:

// 存储副作用函数的桶
const bucket = new Set()
// 原始数据
const data = { text: 'hello world' }

// 对原始数据的代理
const obj = new Proxy(data, {
    // 拦截读取操作
    get(target, key) {
        // 将副作用函数 effect 添加到存储副作用函数的桶中
        bucket.add(effect)
        // 返回属性值
        return target[key]
    },

    // 拦截设置操作
    set(target, key, newVal) {
        // 设置属性值
        target[key] = newVal
        // 把副作用函数从桶里取出并执行
        bucket.forEach(fn => fn())
        // 返回 true 代表设置操作成功
        return true
    }
})

我们创建了一个用于存储副作用函数的桶 bucket,它是Set 类型。接着定义原始数据 data,obj 是原始数据的代理对象,我们分别设置了 get 和 set 拦截函数,用于拦截读取和设置操作。当读取属性时将副作用函数 effect 添加到桶里,即bucket.add(effect),然后返回属性值;当设置属性值时先更新原始数据,再将副作用函数从桶里取出并重新执行,这样我们就实现了响应式数据。

通过以下代码进行测试:

// 副作用函数
function effect() {
    document.body.innerText = obj.text
}
// 执行副作用函数,触发读取
effect()
// 1 秒后修改响应式数据
setTimeout(() => {
    obj.text = 'hello vue3'
}, 1000)

一秒后文字内容变成了'hello vue3'。

很明显当前的实现方式还存在很多问题,比如副作用函数直接通过变量名effect来获取,修改对象不同属性都会把所有存储的副作用函数执行一遍,后面将对这些问题进行优化。