Vue3:手写reactive(obj)和watchEffect(fn)

103 阅读2分钟

reactive(obj)和watchEffect(fn)是Vue的响应系统当中相当重要的两个函数,今天我们就来实现一下这两个函数,对我们理解Vue的响应式原理非常有帮助。 话不多说,我们开始。

第一步 我们想要做什么?

我们知道,reactive(obj)接收一个对象作为参数,并返回这个对象的一个代理,当我通过这个代理更改这个对象的属性的值的时候,这个对象的“副作用(Effect)”将会被触发,就像下面的代码展示的那样。

// 这是我们的状态对象。
const state = {
    count: 0
}
// 通过reactive()函数得到一个状态对象的一个响应式代理。
const reactivityState = reactive({
    count: 0
})
// 通过watchEffect()函数将一个函数注册为副作用。
watchEffect(() => {
    console.log(`目前count的值为:${reactivityState.count}`)
})

// 更改state的值会触发副作用。
state.count++

在vue中,调用上面的watchEffect(fn)函数时,watchEffect(fn)函数内部将执行一次我们所传入的函数,并把我们所传入的函数当作一个“副作用”注册到我们的函数内部所访问到的所有响应式属性当中去。这句话有点长有点拗口,不过我们慢慢来。

现在我们知道的是:

  1. reactive(obj)将返回给定对象的一个代理,当你通过这个代理访问它的属性时,这个代理将收集当前所访问的属性的“副作用”,当你通过这个代理更改它的属性值时,这个代理将触发之前收集到的该属性的“副作用”。
  2. watchEffect(fn)函数将会把函数fn作为“副作用”,注册到其内部所访问到的所有响应式属性中。

第二步 我们该怎么做?

通过ES6的Proxy代理对象,我们可以拦截对目标对象的访问和修改,示例代码如下:

const state = {
        count: 0
    }

const proxy = new Proxy(state, {
    get(target, prop, receiver) {
        console.log(`拦截到对目标对象属性值为${prop}的访问。`)
        return target[prop]
    },
    set(target, prop, value) {
        console.log(`拦截到对目标对象属性值为${prop}的修改,新的值为${value}。`)
    }
})

proxy.count     // ->拦截到对目标对象属性值为count的访问。
proxy.count++   // -> 拦截到对目标对象属性值为count的访问。(修改之前要先访问一下)
                // -> 拦截到对目标对象属性值为count的修改,新的值为1。

我们把上面的代码整理到reactive()函数里面:

function reactive(target) {
    return new Proxy(target, {
        get(target, prop, receiver) {
            console.log(`拦截到对目标对象属性值为${prop}的访问。`)
            return target[prop]
        },
        set(target, prop, value) {
            console.log(`拦截到对目标对象属性值为${prop}的修改,新的值为${value}。`)
        }
    })
}

const state = {
        count: 0
    }
const reactivityState = reactive(state)
reactivityState.count     // ->拦截到对目标对象属性值为count的访问。
reactivityState.count++   // -> 拦截到对目标对象属性值为count的访问。(修改之前要先访问一下)
                          // -> 拦截到对目标对象属性值为count的修改,新的值为1。

我们已经知道了如何拦截对象的访问了,那么我们要怎么收集依赖呢?

Vue是通过把“副作用”函数放到一个约定好的位置来收集的。代码如下:

// 当有“副作用”需要收集时,放到这里。
let currentEffect = null

收集到的依赖怎么存放呢?Vue把它放到了一个WeakMap中:

// 收集到的依赖放到这里
const targetEffectMap = new WeekMap()

第三步 开始实现

watchEffect(fn)函数很简单,就像这样:

let currentEffect = null
function watchEffect(fn) {
    currentEffect = fn  // 先把函数放到约定的地方
    fn()
}

先把fn放到约定好的地方,然后调用fn

接下来我们来完成reactive(obj)函数:

const targetEffectMap = new WeekMap()
let currentEffect = null

function reactive(target) {
    return new Proxy(target, {
        get(target, prop, receiver) {
            console.log(`拦截到对目标对象属性值为${prop}的访问,开始收集依赖。`)
            // 收集依赖
            if (currentEffect !== null) {
                let propEffectMap = targetEffectMap.get(target)
                if (!propEffectMap) {
                    // 这里还是一个Map,用来存放具体prop的effect数组
                    propEffectMap = new Map()
                    targetEffectMap.set(target, propEffectMap)
                }
                let propEffectArray = propEffectMap.get(prop)
                if (!propEffectArray) {
                    propEffectArray = []
                    propEffectMap.set(prop, propEffectArray)
                }
                propEffectArray.push(currentEffect)
                currentEffect = null
            }
            return target[prop]
        },
        set(target, prop, value) {
            console.log(`拦截到对目标对象属性值为${prop}的修改,新的值为${value},尝试触发副作用。`)
            target[prop] = value

            //寻找并触发全部的依赖
            let propEffectMap = targetEffectMap.get(target)
            if (!propEffectMap) {
                return
            }
            let propEffectArray = propEffectMap.get(prop)
            if (propEffectArray) {
                propEffectArray.forEach(effect => {
                    effect()
                })
            }
        }
    })
}

还是那句老话,在get时收集依赖,在set时触发依赖。

第四步 完成

这样一个简单的响应式功能就这样完成了,当然Vue的源码比这个复杂的多,但是基本的过程就是这样的。

以下是全部的代码,你可以把它复制下来运行一下看看:

let currentEffect = null
const targetEffectMap = new WeakMap()

function reactive(target) {
    return new Proxy(target, {
        get(target, prop, receiver) {
            console.log(`拦截到对目标对象属性值为${prop}的访问,开始收集依赖。`)
            // 收集依赖
            if (currentEffect !== null) {
                let propEffectMap = targetEffectMap.get(target)
                if (!propEffectMap) {
                    propEffectMap = new Map()
                    targetEffectMap.set(target, propEffectMap)
                }
                let propEffectArray = propEffectMap.get(prop)
                if (!propEffectArray) {
                    propEffectArray = []
                    propEffectMap.set(prop, propEffectArray)
                }
                propEffectArray.push(currentEffect)
                currentEffect = null
            }
            return target[prop]
        },
        set(target, prop, value) {
            console.log(`拦截到对目标对象属性值为${prop}的修改,新的值为${value},尝试触发副作用`)
            target[prop] = value

            //寻找并触发全部的依赖
            let propEffectMap = targetEffectMap.get(target)
            if (!propEffectMap) {
                return
            }
            let propEffectArray = propEffectMap.get(prop)
            if (propEffectArray) {
                propEffectArray.forEach(effect => {
                    effect()
                })
            }
        }
    })
}

function watchEffect(fn) {
    currentEffect = fn  // 先把函数放到约定的地方
    fn()
}

const state = {
    count: 0
}

const reactivityState = reactive(state)

watchEffect(() => {
    console.log(`这里是副作用函数,当前count的值是${state.count}。`)
})

reactivityState.count++   
                              
// -> 这里是副作用函数,当前count的值是0
// -> 拦截到对目标对象属性值为count的访问。
// -> 拦截到对目标对象属性值为count的访问。
// -> 拦截到对目标对象属性值为count的修改,新的值为1。
// -> reactive.html:105 这里是副作用函数,当前count的值是1