了解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
看完两件小事
如果你觉得这篇文章对你挺有启发,我想请你帮我两个小忙:
- 把这篇文章分享给你的朋友 / 交流群,让更多的人看到,一起进步,一起成长!
- 关注公众号 「画漫画的程序员」,公众号后台回复「资源」 免费领取我精心整理的前端进阶资源教程