本文已参与「新人创作礼」活动,一起开启掘金创作之路,符合活动条件。
小白今天约了一个面试
情景对话
面试官:工作中有用到watch监听数据吗?
小白:有的,经常会监听某个属性的状态,然后去处理业务逻辑
面试官:那你知道watch底层的原理吗?
小白:就是监听响应式数据,数据改变了,就保存下之前的数据,拿到更新的数据,把这两个数据传递给回调函数就行了呗...(暗自窃喜 😏)
面试官:watch内部是怎么做的,你知道吗
小白:这。。。。再见👋 👋
watch的用法
在vue3中,watch可以这样使用,这里贴上官网用法:
今天我们主要聊聊watch,如何监听一个响应式对象和一个getter函数
实现
监听getter函数
假如我们有一个代理对象newObj,调用watch函数,去实现监听,示例如下
const obj={
name:"张三",
flag:true,
count:1,
}
//代理对象newObj,相当于 const newObj=reactive({ ...})
const newObj=new Proxy(obj,{
get(){
...
},
set(){
...
}
})
watch(()=>newObj.name,(newValue,oldValue)=>{
console.log("旧值是"+oldValue);
console.log("新值是"+newValue);
})
newObj.name="李四"
接下来,我们思考下,我么要如何实现一个watch呢?
1.思路
1.读取newObj.name的值,使用track收集依赖
2.每当数据改变的时候,不能立马通过trigger触发依赖,而是要在合适的时机去触发依赖,这里我们就想到了调度函数
粗略的实现一下
function watch(getter,cb){
let oldValue,newValue
const effectFn= effect(getter,{
lazy:true,
schedule(fn){
newValue=fn()
cb(newValue,oldValue)
oldValue=newValue
}
})
oldValue= effectFn()
}
上述代码逻辑:
1.lazy懒加载执行effectFn,因为watch是惰性的,将name值首先赋值给oldValue,此时track收集及依赖;
2.当我们修改name属性的值时,会触发trigger函数,然后发现有调度器,走调度器的逻辑,这个时候,通过执行fn,将name最新的值赋值给newValue,
3.然后再执行用户传进来的回调函数cb,此时把newValue和oldValue作为参数传入
4.最后一步,重新给oldValue赋值,这样可以保证下次,name更新的时候,oldValue拿的是最新的值
测试一下,没问题!yeah
监听一个响应式对象
1.思路
有了上面的getter的实现,我们想监听一个对象,似乎并不难 思路大概是: 1.拿到一个对象,遍历读取一下,循环执行track函数读取
2.在修改对象的某一个属性的时候,拿到修改后的对象给到newValue,保存修改前的对象给到oldValue
2.实现
//调用
watch(newObj,()=>{
console.log("旧值是");
console.log(oldValue);
console.log("新值是");
console.log(newValue);
})
newObj.name="李四"
在之前的watch函数上进行了修改
function traverse (value,seen=new Set()){
if(typeof value !=='object'||value==null||seen.has(value)) return
seen.add(value)
for (const key in value) {
traverse(value[key],seen)
}
return value
}
function watch(source,cb){
let getter
if(typeof source =='function'){
getter=source
}else {
getter=()=>traverse(source)
}
let oldValue,newValue
const effectFn=effect(getter,{
lazy:true,
schedule(fn){
newValue=fn()
cb(newValue,oldValue)
oldValue=newValue
}
})
oldValue=effectFn()
}
上面的代码:
1.traverse函数 循环读取对象的属性,进行track
2将traverse函数的返回值作为一个getter传入effect,下面的逻辑就和getter相似了
当我们执行代码时:
咦?啥情况,为啥会这样?🤔 🤔oldValue也被修改了 我们不妨再修改一下试试
newObj.name="王武"
效果如下:
看到这里,诸位小伙伴应该已经看到问题所在了吧?😁 😁
其实,原理很简单,对象赋值---是引用地址 比如:
let a={
x:1
}
let b
b=a
a.x=2
console.log(b);
同理,这里也是一样的,我们简单粗暴的修改一下
function watch(source,cb){
let getter
if(typeof source =='function'){
getter=source
}else {
getter=()=>traverse(source)
}
let oldValue,newValue
const effectFn=effect(getter,{
lazy:true,
schedule(fn){
//一通乱改🤔
newValue=JSON.parse(JSON.stringify(fn()))
cb(newValue,oldValue)
oldValue=JSON.parse(JSON.stringify(newValue))
}
})
let currentValue= effectFn()
//一通乱改🤔
if(typeof currentValue=='object'){
oldValue= JSON.parse(JSON.stringify(currentValue))
}else{
oldValue=currentValue
}
}
效果如下:
实现了!
当然,目前实现的watch并不是最优的方案,watch也还有很多功能点,比如,对数组的监听等等,欢迎各位大佬留言,提出意见~ 感谢🙏🙏