深入浅析vue处理响应式数据的方法
本篇文章带大家学习vue,聊聊vue如何处理响应式数据?希望对大家有所帮助!
关于vue的响应式数据,你可能有很多疑惑。
比如为什么要改用proxy?
比如为什么有reactive和ref两个api?
比如vue是如何实现响应式的?
其实这些在源码中,都能找到答案。
在第一篇vue3有哪些升级中,我也提了一下使用proxy的好处,以及Object.defineProperty的缺陷。但是今天,我想换个角度,让chatGPT来回答一下这个问题。【相关推荐:vuejs视频教程、web前端开发】
嗯,我感觉比我总结的好。
那继续下一个问题。
这个回答感觉就比较官方了,没有回答到我想要的答案。
理由其实很简单,因为proxy方法不能代理值类型,只能代理对象。所以需要一个额外的方法,来处理值类型的数据。
当然你也可以一个ref走天下,vue源码做了兼容会自动进行转换
有了这些理解之后,我们来看今天的重头戏,自己仿照vue3源码,实现一个响应式系统。
相关代码可以点击查看,根据文章的标题选择不同的分支即可。
实现reactive
在前文我们也介绍了,reactive其实用的是proxy代理对象。
我们可以使用proxy实现一个简单的代理函数reactive。
function reactive(target) { const isObject = (val) => val !== null && typeof val === 'object' if (!isObject(target)) { console.warn(`数据必须是对象: ${String(target)}`) return target } const proxy = new Proxy(target, { get: (target, key, receiver) => { console.log('get', key) const res = Reflect.get(target, key, receiver) // track(target, key) // 这句很关键,不然嵌套数据,里面的不会响应 if (isObject(res)) { return reactive(res) } return res }, set: (target, key, value, receiver) => { console.log('set', key, value) const result = Reflect.set(target, key, value, receiver) // trigger(target, key, value) return result }, deleteProperty: () => { const result = Reflect.deleteProperty(target, key) return result } }) return proxy } const person = reactive({ name: '', age: 18, like: ['apple'] }) person.name = 'vue test'复制代码
注意:Reflect
Reflect.get(target, key)跟直接访问target[key]是有一点差别的。
当代理对象中,有get,set等this指向的时候,可以重定向this。
举个例子:
const person = new Proxy({ name: "vue test", age: 18, get info() { return 'name is :' + this.name + ' age is:' + this.age } }, { get: (target, key, receiver) => { console.log('get', key) // return target[key] return Reflect.get(target, key, receiver) } }) console.log(person.info)复制代码
使用Reflect,我们在访问name, age的时候均可以触发。
改成target之后,则只会在info触发一次。
return target[key]复制代码
实现ref
在vue3中,ref是通过get和set来实现的。
跟上面类似,也还是先声明一个函数,然后通过get,set来访问数据。
function ref(value) { return new RefImpl(value) } class RefImpl { constructor(value) { // 如果值是对象,则用reactive处理 this._value = isObject(value) ? reactive(value) : value // 记录一下初始值 this._rawValue = value } get value() { // trackRefValue(this) return this._value } set value(newVal) { if (!Object.is(newVal, this._rawValue)) { // 更新原始数据 this._rawValue = newVal // 更新 .value 的值 this._value = isObject(newVal) ? reactive(newVal) : value // triggerRefValue(this) } } }复制代码
源码也很直观的解释了,为什么ref的使用一定要用.value的方式使用,因为get/set就是通过value来设置的。
添加依赖收集与触发
数据代理我们已经完成了,但是数据更改之后,怎么通知组件实现双向绑定呢?
答案就是依赖收集与触发,也就是当触发get的时候,我把触发get的这个条件【函数】给保存起来,当触发set的时候,重新执行一下触发一下这个条件【函数】不就行了吗。
还是来看代码,我加一个track收集的方法,加一个target触发的方法。(也就是上面代码片段注释掉的两行代码)
除此之外,还需要一个effect函数来管理触发函数。
let activeEffect = null const targetMap = new WeakMap() // 依赖收集/触发 function track(target, key) { let depsMap = targetMap.get(target) if (!depsMap) { targetMap.set(target, (depsMap = new Map())) } let dep = depsMap.get(key) if (!dep) { dep = new Set() } dep.add(activeEffect) depsMap.set(key, dep) } function trigger(target, key, value) { const depsMap = targetMap.get(target) if (!depsMap) { return } const deps = depsMap.get(key) if (!deps) { return } deps.forEach(effectFn => { if (effectFn.scheduler) { effectFn.scheduler() } else { effectFn() } }) } function effect(fn,options = {}) { const effectFn = () => { try { activeEffect = effectFn return fn() } catch (error) { activeEffect = null } } if (!options.lazy) { effectFn() } effectFn.scheduler = options.scheduler return effectFn } const person = reactive({ name: "hello world" }) effect(() => { console.log('effect person', person.name) }) setTimeout(() => { person.name = 'setTimeout world' }, 2000)复制代码
activeEffect用来存触发的条件函数。
targetMap用来存放依赖字典,格式如下
{ target: { key: [] } }复制代码
输出结果为hello world,2秒之后,重新执行依赖性函数,输出setTimeout world
总结
数据的代理并不复杂,就是在proxy的基础上,加上一些边界处理。而要实现响应式则需要加上依赖收集,依赖触发的实现。
effect是一个很重要的函数,很多api是基于这个函数开发的,比如useEffect, watch。组件的更新也是基于effect函数,这个后续还会提到。
如果不太理解effect,可以梳理一下执行顺序。
- 1.调用effect函数,并传入参数fn
- 2.申明一个effectFn函数,并执行,将函数保存为activeEffect
- 3.执行fn,读取person.name
- 4.走proxy的get代理
- 5.收集依赖,将activeEffect保存的函数存到全局map中
- 6.此时,effect函数执行完毕,等待数据更新
- 7.2s后,走proxy的set代理
- 8.执行全局map中保存的函数,重新执行effect函数,再次回到步骤1
相关代码可以点击查看,选择lesson3分支即可。
文章相关代码,可在vue/examples中查看,仿vue实现版本可在packages/reactivity中查看。
(学习视频分享:vuejs入门教程、编程基础视频)
以上就是深入浅析vue处理响应式数据的方法的详细内容,更多请关注其它相关文章!