1. 首页
  2. Vuejs

Vue3响应式原理剖析

了解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
}

//JS中文网 – 前端进阶资源分享 www.javascriptc.com

测试代码

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]
//JS中文网 – 前端进阶资源分享 www.javascriptc.com

通过上面测试代码发现,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:编程

//JS中文网 – 前端进阶资源分享 www.javascriptc.com

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

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

依赖收集

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

// 用户修改关联数据会触发响应函数
const state = reactive({name:'张三'})
state.name = '李四'
// 设置响应函数,当state.name改变此函数会更新。
effect(() => console.log(state.foo))
//JS中文网 – 前端进阶资源分享 www.javascriptc.com

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

  • 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
}
//JS中文网 – 前端进阶资源分享 www.javascriptc.com

2、track、trigger方法实现

JS中文网 – 前端进阶资源教程 www.javascriptC.com
一个致力于帮助开发者用代码改变世界为使命的平台,每天都可以在这里找到技术世界的头条内容

// 映射关系表,结构大致如下:
// {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()
            })
        }
    }
}

//JS中文网 – 前端进阶资源分享 www.javascriptc.com

方法实现之后,我们只需要再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

看完两件小事

如果你觉得这篇文章对你挺有启发,我想请你帮我两个小忙:

  1. 关注我们的 GitHub 博客,让我们成为长期关系
  2. 把这篇文章分享给你的朋友 / 交流群,让更多的人看到,一起进步,一起成长!
  3. 关注公众号 「IT平头哥联盟」,公众号后台回复「资源」 免费领取我精心整理的前端进阶资源教程

JS中文网是中国领先的新一代开发者社区和专业的技术媒体,一个帮助开发者成长的社区,目前已经覆盖和服务了超过 300 万开发者,你每天都可以在这里找到技术世界的头条内容。欢迎热爱技术的你一起加入交流与学习,JS中文网的使命是帮助开发者用代码改变世界

本文著作权归作者所有,如若转载,请注明出处

转载请注明:文章转载自「 Js中文网 · 前端进阶资源教程 」https://www.javascriptc.com

标题:Vue3响应式原理剖析

链接:https://www.javascriptc.com/4087.html

« Vue3源码解读(二)-mount
2020玩转 webpack,使你的打包速度提升 90%»
Flutter 中文教程资源

相关推荐

QR code