Vue Watch中的 deep:true 是如何实现的
想象一下你有一个大盒子(对象或数组),里面装了很多小盒子(对象的属性或数组的元素)。当你只关注大盒子本身时,你并不知道里面的小盒子发生了什么变化。但是,如果你非常好奇,想要知道里面每一个小盒子的变化,那么你就需要一种方法来“深入”观察这个大盒子。
在 Vue.js 中,watch
就像是一个观察者,它可以帮助你监视某个数据的变化。但是,默认情况下,watch
只观察大盒子本身的变化,比如整个对象被替换了,或者整个数组被修改了。它不会深入到里面去看小盒子(属性或元素)发生了什么。
这时候,deep: true
就像一个放大镜,它告诉 Vue.js 的 watch
:“我不仅要观察大盒子的变化,我还要深入到每一个小盒子里去,看它们发生了什么变化。”
在 Vue 2 中,这个“深入观察”的实现方式是,Vue 会遍历大盒子里的每一个小盒子,然后给它们每一个都加上一个“小标签”(getter 和 setter)。这样,每当任何一个小盒子里的内容发生变化时,这个小标签就会告诉 Vue.js:“嘿,这里有个变化!”然后 Vue.js 就可以执行你指定的 watch
回调函数了。
但是,这种方式有一个问题,就是如果大盒子里的小盒子非常多,或者小盒子里面还有更多的小盒子(深层嵌套的对象),那么给它们每一个都加上“小标签”就会消耗很多的时间和资源。
到了 Vue 3,Vue.js 使用了一种更先进的方法来实现“深入观察”,它叫做 Proxy。Proxy 可以让你直接在大盒子上放一个“超级标签”,这个标签会自动监视大盒子里所有小盒子的变化,而不需要你给每一个小盒子都单独加标签。这样,Vue.js 就可以更高效地实现 deep: true
的功能了。
所以,简单来说,deep: true
就是让 Vue.js 的 watch
能够“深入”观察对象或数组内部的每一个小变化,它背后的实现原理是通过递归地为每个属性或元素添加监视器(Vue2)或使用 Proxy 来实现更高效的全局监视(Vue3)。
1)源码解析:
// 用于遍历一个对象或数组中的所有属性或元素
function _traverse (val: any, seen: SimpleSet) {
let i, keys
const isA = Array.isArray(val)
/* 1、类型检查:
首先,检查传入的值 val 是否是数组或对象,并且没有被冻结(Object.isFrozen(val)),
也不是Vue 的虚拟节点(VNode)。如果不是这些类型之一,则直接返回。 */
if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) {
return
}
/* 2、处理已观察的对象:
如果 val 是一个已经被 Vue 观察的对象(即 val.__ob__ 存在),则检查它是否已经在 seen 集合中
出现过。这是为了避免无限循环和重复观察。*/
if (val.__ob__) {
const depId = val.__ob__.dep.id
if (seen.has(depId)) {
return
}
seen.add(depId)
}
/* 3、遍历:
如果 val 是数组,则使用逆序遍历(从最后一个元素开始)来遍历数组中的每一个元素,
并对它们调用 _traverse 函数。
如果 val 是对象,则获取对象的所有键,并同样使用逆序遍历来遍历这些键对应的值,
并对它们调用 _traverse 函数。*/
if (isA) {
i = val.length
while (i--) _traverse(val[i], seen)
} else {
keys = Object.keys(val)
i = keys.length
while (i--) _traverse(val[keys[i]], seen)
}
}
/* 下面的 get 函数,是 Vue.js 中响应式系统的一部分,用于在访问一个属性时执行一系列操作。
这里的 get 函数特别与 watcher 相关,因为它在 watcher 被创建时用来获取被观察属性的值。*/
get () {
/* 1、pushTarget(this):
这一行将当前的 watcher 实例设置为全局的 Dep.target。
在 Vue.js 中,Dep.target 是一个栈,用于记录当前正在进行的依赖收集的目标 watcher。
这样做是为了在属性的 getter 被调用时,能够将这个 watcher 添加到属性的依赖列表中。*/
pushTarget(this) // 先将当前依赖放到 Dep.target上
let value
const vm = this.vm
try {
/* 2、获取值:
通过调用 this.getter.call(vm, vm) 来获取被观察属性的值。
这里的 getter 是一个函数,它会在创建 watcher 时被设置,用于访问实际的属性值。
vm 是 Vue 实例,这里将它作为 getter 函数的上下文(this)和参数传入。*/
value = this.getter.call(vm, vm)
} catch (e) {
/* 3、异常处理:
如果在获取值的过程中发生异常,会根据 this.user 的值来决定是否捕获这个异常。
如果是用户定义的 watcher(this.user 为 true),则会调用 handleError 函数来处理异常;
否则,直接抛出异常。*/
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
/* 4、异常处理:
如果 this.deep 为 true,则表示需要进行深度观察。
此时,会调用 _traverse 函数来遍历获取到的值(无论是一个对象还是数组),
并对其中的每一个属性或元素执行相同的操作(即再次调用 get 方法)。
这样,就可以确保对象或数组内部的所有嵌套属性都被观察到。*/
if (this.deep) { // 如果需要深度监控
traverse(value) // 会对对象中的每一项取值,取值时会执行对应的get方法
}
/* 5、popTarget():
最后,将 Dep.target 栈顶的元素弹出,恢复之前的状态。*/
popTarget()
}
return value
}
为何 Vue 采用异步渲染
如果不采用异步更新,那么每次更新数据都会对当前组件进行重新渲染。所以为了性能考虑,Vue
会在本轮数据更新后,再去异步更新视图。
举个例子:假如你正在画画,但是你的颜料还没干,每次画完一笔都要等它干了才能继续画下一笔。这就像同步