💥 前言
因为一直使用Vue搬砖,我觉得自己已经是一个熟练的Api调用工程师了,前两天看到一个Watch、WatchEffect相关的问题却给我好好上了一课。😅
看下面的代码问:为什么Watch能侦听到,WatchEffect却不行❓
let test = ref({});
watchEffect(() => {
console.log(test.value);
});
watch(
() => test.value,
(newVal) => {
console.log(newVal);
},
{
deep: true,
},
);
const testChange = () => {
test.value.aa = 1;
};
// 打印结果:Proxy {aa: 1}
Tipes:各位大佬可以停下来思考一下,然后带着自己的想法往下看
⚙️ 缘由
当时下面有一个答案是这样说的:
Vue内部应该是做了优化,不会盲目检测内部数据,所以WatchEffect侦听不到,而watch 应该是第一个参数能接收对象和数组,所以能侦听到。
显然这种模棱两可的回答不能说服我,所以我就去找春哥问了问,然后我自以为我理解了春哥给我的答案,然后写出了这样的话:
在上面代码的写法中,test.value里面包含的只是一个对象的指针,在下面的方法里面修改内容,修改的是内存地址的内容,指针实际上并没有发生变化,而watchEffect收集的依赖就是这个指针,所以回调函数不会被执行。
「更新:」根据骑自行车大佬的评论,然后我又重新看了看春哥的答复,是我思想出了问题😂,最后的结论是这样的:
当在代码里面打印 test.value,最多打印出来的就是处理过后的Proxy对象,实际上并没有触发test的 getter,自然也不会添加watcher,所以就不会修改也不会触发watchEffect的回调函数。
验证一下:
const obj = {};
const proObj = new Proxy(obj, {
get: function() {
console.log('00')
}
});
console.log(proObj); // Proxy {}
console.log(proObj.a); // 00 undefined
Tipes:春哥的掘金地址:摸鱼的春哥,关注春哥不仅能解决技术问题,还有精彩的故事可以看哦🤩
我走神的时候敲出来了这样的代码,结果发现居然也能行🤔️?
watch(test.value, (newVal) => {
console.log(newVal);
});
Tipes: Js数据类型分为基本数据类型和引用数据类型。基本数据类型一般保存在栈内存中,引用数据类型分为两部分存储:一部分为指针,一般保存在栈内存中,一部分为内容,一般保存在堆内存中,指针指向内容所在的对内存地址。由此也引发出了深浅拷贝的问题。对于深浅拷贝还掌握不够彻底小伙伴可以看看这两篇文章:
十分钟带你手撕一份"渐进式"JS深拷贝
最新HTML规范——structuredClone深拷贝函数,能取代JSON或者lodash吗?
为了避免以后还出现这样的问题,所以我们一块去手撕一下源码,弄清楚他是怎么实现的。
🔧 解析
github1s源码地址
如果说排除响应式的实现,只看Watch、WatchEffect还是比较简单的:
export function watchEffect(
effect: WatchEffect,
options?: WatchOptionsBase
): WatchStopHandle {
return doWatch(effect, null, options)
}
// ...省略watchEffect的别称
// ...省略watch的重载
// implementation
export function watch<T = any, Immediate extends Readonly<boolean> = false>(
source: T | WatchSource<T>,
cb: any,
options?: WatchOptions<Immediate>
): WatchStopHandle {
if (__DEV__ && !isFunction(cb)) {
// ...省略报错信息
}
return doWatch(source as any, cb, options)
}
WatchEffect和Watch共用doWatch函数实现,接下来我们就带着上面的问题去看看doWatch里面是怎么做具体处理的:
// 简化版
function doWatch(
source: WatchSource | WatchSource[] | WatchEffect | object,
cb: WatchCallback | null,
{ immediate, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ
): WatchStopHandle {
// ...省略错误处理
const instance = currentInstance
let getter: () => any
let forceTrigger = false
let isMultiSource = false
if (isRef(source)) { // 判断是否是ref对象
getter = () => source.value 则取得其value
forceTrigger = isShallow(source)
} else if (isReactive(source)) { // 判断是否是reactive对象
getter = () => source
deep = true // 重点!!!!!!!!!!
} else if (isArray(source)) {
isMultiSource = true
forceTrigger = source.some(isReactive)
getter = () =>
source.map(s => {
if (isRef(s)) {
return s.value
} else if (isReactive(s)) {
return traverse(s)
} else if (isFunction(s)) {
return callWithErrorHandling(s, instance, ErrorCodes.WATCH_GETTER)
} else {
__DEV__ && warnInvalidSource(s)
}
})
} else if (isFunction(source)) {
if (cb) { // 第二个参数有值是Watch
// getter with cb
getter = () =>
callWithErrorHandling(source, instance, ErrorCodes.WATCH_GETTER)
} else { // 没有值则是WatchEffect
// no cb -> simple effect
getter = () => {
if (instance && instance.isUnmounted) {
return
}
if (cleanup) { // 如果清楚函数存在,先清除
cleanup()
}
return callWithAsyncErrorHandling(
source,
instance,
ErrorCodes.WATCH_CALLBACK,
[onCleanup]
)
}
}
} else {
getter = NOOP
__DEV__ && warnInvalidSource(source)
}
// 省略向下兼容
if (cb && deep) { // deep 深度侦听
const baseGetter = getter
getter = () => traverse(baseGetter())
}
// 省略SSR操作
// 省略设置清除监听
📑 总结
看一下简化后的代码,是不是抛开响应式的实现其实Watch和WatchEffect还是很简单的。😄再去看上面这样也行的代码是怎么实现的,关键就是在下面这一步:
if (isReactive(source)) { // 判断是否是reactive对象
getter = () => source
deep = true // 重点!!!!!!!!!!
}
当检测到是reactive对象的时候,Vue会帮我们把deep设置为true,开启深度侦听,所以Watch的第一个参数按照上面代码的写法也会被侦听到。
这时候就会有小伙伴说了,之前的代码明明用的是ref
声明的,为什么不走isRef
判断,而是走isReactive
呐?这就又要涉及到Ref和Reactive的源码了:
有兴趣的小伙伴可以自行了解,这里只放一个关键的地方:当ref
入参为对象时会走reactive
的proxy
的代理,否则走RefImpl
的set
和get
方法。
const convert = <T extends unknown>(val: T): T => isObject(val) ? reactive(val) : val
📋 收尾
总体来说,光看Watch、WatchEffect的实现对于各位大佬来说还是洒洒水。不过从面试角度来说,常和Watch一起出现的就是Computed了
当依赖变量发生变化的时候,计算属性的watcher会将dirty修改为true,代表数据已经脏,但是不会立即求值,当获取计算属性内容的时候「通过 this.dep.subs.length 判断有没有订阅者」,会去判断dirty的值,如果为true,则重新求值「重新求值之后会比较新老值,如果没有变化则不会重新渲染(性能优化)」,如果为false则返回缓存值。
所以大佬们有空了也可以看看Computed的实现,再看看响应式的实现,到时候吊打面试官哈哈哈。
有帮助记得帮我点点赞哦,最后祝各位大佬学习进步,事业有成!🎆🎆
Tipes:往期内容
「Vue系列」之面试官问NextTick是想考察什么?
面试的时候面试官是这样问我Js基础的,角度真刁钻
「算法基础」之二叉树的遍历和搜索
「Vue系列」使用Teleport封装一个弹框组件
「Vue系列」为什么用Proxy取代Object.defineProperty?