所属分类:web前端开发
本篇文章带大家学习vue,聊聊vue如何处理响应式数据?希望对大家有所帮助!
关于vue的响应式数据,你可能有很多疑惑。
比如为什么要改用proxy?
比如为什么有reactive和ref两个api?
比如vue是如何实现响应式的?
其实这些在源码中,都能找到答案。
在第一篇vue3有哪些升级中,我也提了一下使用proxy的好处,以及Object.defineProperty的缺陷。但是今天,我想换个角度,让chatGPT来回答一下这个问题。【相关推荐:vuejs视频教程、web前端开发】
嗯,我感觉比我总结的好。
那继续下一个问题。
这个回答感觉就比较官方了,没有回答到我想要的答案。
理由其实很简单,因为proxy方法不能代理值类型,只能代理对象。所以需要一个额外的方法,来处理值类型的数据。
当然你也可以一个ref走天下,vue源码做了兼容会自动进行转换
有了这些理解之后,我们来看今天的重头戏,自己仿照vue3源码,实现一个响应式系统。
相关代码可以点击查看,根据文章的标题选择不同的分支即可。
在前文我们也介绍了,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]复制代码
登录后复制
在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,可以梳理一下执行顺序。
相关代码可以点击查看,选择lesson3分支即可。
文章相关代码,可在vue/examples中查看,仿vue实现版本可在packages/reactivity中查看。
(学习视频分享:vuejs入门教程、编程基础视频)
以上就是深入浅析vue处理响应式数据的方法的详细内容,更多请关注zzsucai.com其它相关文章!