Vue 3.2 响应式原理——手写简单的响应式数据

91 阅读3分钟

响应式数据前瞻

不智能的数据

我们正常编写js代码的时候,变量值的改变不会触发相关视图依赖更新,如下面代码所示。

    const reactive = {
        a: 'hello'
    }
    function effect() {
        document.body.innerText = reactive.a
    }
    effect()
    reactive.a = 'edited' // 变量修改,不会触发effect函数执行,需要手动调用
    effect() // 手动调用,更新视图

Vue中智能的数据

在Vue编写的数据,当数据变化时,相关的视图也会实时更新,官方称之为响应式数据,实现响应式数据的核心就是数据劫持发布订阅。一句话概括就是通过劫持数据的获取(getter)操作来收集依赖,劫持数据的设置(setter)操作来触发依赖,其中收集依赖和触发依赖就是发布订阅模式的核心思想。

在Vue2中,数据劫持是通过Object.defineProperty来完成对象属性的获取(getter)和设置(setter)操作;Vue3则是通过Proxy + Reflect来完成对象属性相关劫持操作。下面可以使用Proxy来简单实现一个响应式数据。

// 当前被激活的依赖
let activeEffect = null
function effect(fn) {
    activeEffect = fn
    const result = fn()
    activeEffect = null
    return result
}

// 用于保存依赖的对应关系 target -> key -> effect
const targetMap = new WeakMap()
// 依赖收集
function track(target, key) {
    // 当前没有被激活的依赖,直接退出
    if (!activeEffect) return
    // 先获取 target 映射的依赖收集器
    let depsMap = targetMap.get(target)
    if (!depsMap) targetMap.set(target, (depsMap = new Map()))
    // 再获取 key 映射的依赖,里面用于存放依赖
    let deps = depsMap.get(key)
    if (!deps) depsMap.set(key, (deps = new Set()))
    if (!deps.has(activeEffect)) deps.add(activeEffect)
}
// 触发依赖
function trigger(target, key, newValue, oldValue) {
    // 值没变化直接退出
    if (newValue === oldValue) return
    const depsMap = targetMap.get(target)
    if (!depsMap) return
    const deps = depsMap.get(key)
    if (!deps) return
    const effects = [...deps]
    effects.forEach(effect => effect())
}
// 把普通对象变成响应式对象
function reactive(originData) {
    const reactiveData = new Proxy(originData, {
        get(target, key, receiver) {
            // 收集当前激活的依赖
            track(target, key)
            return Reflect.get(target, key, receiver)
        },
        set(target, key, value, receiver) {
            const oldValue = target[key]
            const result = Reflect.set(target, key, value, receiver)
            // 触发对应的依赖
            trigger(target, key, value, oldValue)
            return result
        }
    })
    return reactiveData
}

// 定义一个响应式对象
const originData = {
    name: 'f1ower1ang',
    age: 18
}
const reactiveData = reactive(originData)
// f1ower1ang-18
effect(() => {
    document.body.innerText = `${reactiveData.name}-${reactiveData.age}`
})
// edited-f1ower1ang-18
setTimeout(() => {
    reactiveData.name = 'edited-f1ower1ang'
}, 1000)

可以把上面的内容复制到浏览器控制台中运行一下,可以发现body元素起始内容为f1ower1ang-18,过了一秒后会自动更新为edited-f1ower1ang-18。这就是响应式数据的魅力所在,不需要我们修改完数据后手动操作去触发视图更新。

上述代码中,WeakMapProxy就是发布订阅模式的核心,其中WeakMap负责收集事件的订阅,Proxy中的getter和setter负责订阅事件和触发事件执行,这里的事件就是指传入effect中的函数参数,在Vue中则是和挂载组件以及组件更新相关的函数。

小结

本文从不具备响应式的数据作为切入点,到手写一个简单的响应式数据系统,为接下来的源码阅读打基础,下节则直接阅读源码,彻底掌握Vue3的响应式数据。