Vue3响应式原理剖析__Vue.js
发布于 4 年前 作者 banyungong 1697 次浏览 来自 分享
粉丝福利 : 关注VUE中文社区公众号,回复视频领取粉丝福利

了解vue3响应式原理之前,先回顾一下vue2的响应式原理的一些弊端:

  • 响应化过程需要递归遍历,消耗较大
  • 新加或删除属性无法监听
  • 数组响应化需要额外实现
  • Map、Set、Class等无法响应式 修改语法有限制

而vue3.0使用ES6的Proxy特性来解决上面这些问题,下面我们通过Proxy实现一个响应式函数:

function reactive(obj) {
    // Proxy只能接受一个对象
    if (typeof obj !== 'object' && obj != null) {
        return obj
    }
    // Proxy相当于在对象外层加拦截
    const observed = new Proxy(obj, {
        get(target, key, receiver) {
            // Reflect用于执行对象默认操作,更规范、更友好
            // Proxy和Object的方法Reflect都有对应
            const res = Reflect.get(target, key, receiver)
            console.log(`获取${key}:${res}`)
            return res
        },
        set(target, key, value, receiver) {
            const res = Reflect.set(target, key, value, receiver)
            console.log(`设置${key}:${value}`)
            return res
        },
        deleteProperty(target, key) {
            const res = Reflect.deleteProperty(target, key)
            console.log(`删除${key}:${res}`)
            return res
        }
    })
    return observed
}

测试代码

const state = reactive({
    name: '张三',
    hobbies: { book: '编程' }
})
state.name  // 获取name:张三
state.name = '李四' // 设置name:李四
state.age = 29 // 设置age:29
delete state.age // 删除age:true
state.hobbies.book // 获取hobbies:[object Object]

通过上面测试代码发现,reactive方法中的对象中如果还嵌套其它对象就不能正确get取值了,下面我们来解决这问题。

嵌套对象响应式

// 定义一个工具方法,在get取值的时候用于判断该值是否是一个对象。
const isObject = val => val !== null && typeof val === 'object'

function reactive(obj) {
    // Proxy只能接受一个对象
    if (!isObject(obj)) {
		return obj 
	}
    // Proxy相当于在对象外层加拦截
    const observed = new Proxy(obj, {
        get(target, key, receiver) {
            // Reflect用于执行对象默认操作,更规范、更友好
            // Proxy和Object的方法Reflect都有对应
            const res = Reflect.get(target, key, receiver)
             console.log(`获取${key}:${res}`)
            // 在get取值的是否判断该值是否是一个对象,如果是则递归
            return isObject(res) ? reactive(res) : res
        },
        set(target, key, value, receiver) {
            const res = Reflect.set(target, key, value, receiver)
            console.log(`设置${key}:${value}`)
            return res
        },
        deleteProperty(target, key) {
            const res = Reflect.deleteProperty(target, key)
            console.log(`删除${key}:${res}`)
            return res
        }
    })
    return observed
}

// 测试代码
const state = reactive({
    name: '张三',
    hobbies: { book: '编程' }
})

state.hobbies.book  // 获取book:编程

注:vue3.0中reactive函数要复杂的多,它里面有对于重复代理、新增或修改,新旧值等问题的处理,这里篇幅有限就不一一展开了。

通过完善,对象嵌套嵌套对象不能触发get的问题就解决了,下面来建立响应数据和更新函数之间的对应关系。

依赖收集

建立响应数据key和更新函数之间的对应关系,用法如下:

// 用户修改关联数据会触发响应函数 
const state = reactive({name:'张三'})
state.name = '李四'
// 设置响应函数,当state.name改变此函数会更新。
effect(() => console.log(state.foo))

要实现上面的功能,我们先来实现三个函数:

  • effect:将回调函数保存起来备用,立即执行一次回调函数触发它里面一些响应数据的getter
  • track:getter中调用track,把前面存储的回调函数和当前target,key之间建立映射关系
  • trigger:setter中调用trigger,把target,key对应的响应函数都执行一遍

** 1、创建effect函数 **

// 保存当前活动响应函数作为getter和effect之间桥梁 
const effectStack = []
// effect任务:执行fn并将其入栈 
function effect(fn) {
    const rxEffect = function () { // 1.捕获可能的异常
        try {
            // 2.入栈,用于后续依赖收集 
            effectStack.push(rxEffect)
            // 3.运行fn,触发依赖收集 
            return fn()
        } finally {
            // 4.执行结束,出栈
            effectStack.pop()
        }
    }
    // 默认执行一次响应函数 
    rxEffect()
    // 返回响应函数
    return rxEffect
}

** 2、track、trigger方法实现 **

// 映射关系表,结构大致如下:
// {target: {key: [fn1,fn2]}} 
let targetMap = new WeakMap()
function track(target, key) {
    // 从栈中取出响应函数
    const effect = effectStack[effectStack.length - 1]
    if (effect) {
        // 获取target对应依赖表
        let depsMap = targetMap.get(target)
        if (!depsMap) {
            depsMap = new Map()
            targetMap.set(target, depsMap)
        }
        // 获取key对应的响应函数集
        let deps = depsMap.get(key)
        if (!deps) {
            deps = new Set()
            depsMap.set(key, deps)
        }
        if (!deps.has(effect)) {
            deps.add(effect)
        }
    }
}

// 触发target.key对应响应函数 
function trigger(target, key) {
    // 获取依赖表
    const depsMap = targetMap.get(target)
    if (depsMap) {
        // 获取响应函数集合
        const deps = depsMap.get(key)
        if (deps) {
            // 执行所有响应函数 
            deps.forEach(effect => {
                effect()
            })
        }
    }
}

方法实现之后,我们只需要再Proxy构造函数中的get和set中进行依赖收集即可,下面是完整的代码:

const isObject = val => val !== null && typeof val === 'object'

function reactive(obj) {
    // Proxy只能接受一个对象
    if (!isObject(obj)) {
        return obj
    }
    // Proxy相当于在对象外层加拦截
    const observed = new Proxy(obj, {
        get(target, key, receiver) {
            // Reflect用于执行对象默认操作,更规范、更友好
            // Proxy和Object的方法Reflect都有对应
            const res = Reflect.get(target, key, receiver)
            track(target, key)
            console.log(`获取${key}:${res}`)
            // 在get取值的是否判断该值是否是一个对象,如果是则递归
            return isObject(res) ? reactive(res) : res
        },
        set(target, key, value, receiver) {
            const res = Reflect.set(target, key, value, receiver)
            trigger(target, key)
            console.log(`设置${key}:${value}`)
            return res
        },
        deleteProperty(target, key) {
            const res = Reflect.deleteProperty(target, key)
            console.log(`删除${key}:${res}`)
            return res
        }
    })
    return observed
}
// 保存当前活动响应函数作为getter和effect之间桥梁 
const effectStack = []
// effect任务:执行fn并将其入栈 
function effect(fn) {
    const rxEffect = function () { // 1.捕获可能的异常
        try {
            // 2.入栈,用于后续依赖收集 
            effectStack.push(rxEffect)
            // 3.运行fn,触发依赖收集 
            return fn()
        } finally {
            // 4.执行结束,出栈
            effectStack.pop()
        }
    }
    // 默认执行一次响应函数 
    rxEffect()
    // 返回响应函数
    return rxEffect
}

// 映射关系表,结构大致如下:
// {target: {key: [fn1,fn2]}} 
let targetMap = new WeakMap()
function track(target, key) {
    // 从栈中取出响应函数
    const effect = effectStack[effectStack.length - 1]
    if (effect) {
        // 获取target对应依赖表
        let depsMap = targetMap.get(target)
        if (!depsMap) {
            depsMap = new Map()
            targetMap.set(target, depsMap)
        }
        // 获取key对应的响应函数集
        let deps = depsMap.get(key)
        if (!deps) {
            deps = new Set()
            depsMap.set(key, deps)
        }
        if (!deps.has(effect)) {
            deps.add(effect)
        }
    }
}

// 触发target.key对应响应函数 
function trigger(target, key) {
    // 获取依赖表
    const depsMap = targetMap.get(target)
    if (depsMap) {
        // 获取响应函数集合
        const deps = depsMap.get(key)
        if (deps) {
            // 执行所有响应函数 
            deps.forEach(effect => {
                effect()
            })
        }
    }
}


// 测试代码
  const state = reactive({ name: '张三' })
  // 第一次取值打印出张三,当state.name修改之后,就打印出李四了
  effect(() => console.log(state.name)) 
  state.name = '李四'

版权声明:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 作者: crayons32242 原文链接:https://juejin.im/post/6864396298394189832

回到顶部