写点什么

vue 的响应式原理:依赖追踪

作者:EquatorCoco
  • 2023-12-06
    福建
  • 本文字数:2460 字

    阅读完需:约 8 分钟

在明白原理之前,我们有很多表面现象、使用场景需要记忆。明白了原理后,你会发现它们已经不需要记了,因为从原理出发,你自己都能把它们推导出来,一切是那么的自然而然。感觉就是:这还用记吗?很明显嘛!


之前我对 vue 的响应式原理,只是一知半解,导致开发中经常会出现疑问,比如:为什么有的数据它不响应?模板中用到的 methods 方法什么时候会执行?什么时候模板会重新渲染?渲染的过程是什么等等。所有的这些开发过程中的疑惑,都是因为不了解底层原理造成的。


今天我们就来一起捋一下,vue 的响应式原理。当然,只是入门级的,可以帮助和我一样不了解原理的同学,大佬勿喷。


一、数据劫持:getter 和 setter


在 vue 的 data 初始化阶段,vue 会递归地遍历 data 的每一个属性,把它们处理成响应式数据。这是一个深层次的遍历,也就是说 data 的属性如果是一个对象,这个对象的属性也是响应式的,不管嵌套几层。

具体来说,vue 对每一个属性执行 Object.defineProperty(),把每一个属性转换为 getter 和 setter,以此实现对属性取值、赋值的劫持,称为数据劫持。


二、watcher 和 dep


我们知道,模板中会用到 data 的数据,计算属性也是如此,它们会各用一个列表来保存自己用到了哪些 data 数据,称为依赖列表。


而 data 的属性,则可能会被模板以及多个计算属性用到,它也会用一个列表来保存哪些模板或计算属性用到了自己,也叫依赖列表。


模板和计算属性,通过 watcher 对象来做这件事,依赖列表存放在 watcher 的一个数组里。每一个 vue 实例,有一个 watcher,称为渲染 watcher。每一个计算属性,各自有一个 wathcer,称为计算属性 watcher。


data 的属性,通过 dep 对象来做这件事,依赖列表存放在 dep 的一个数组里。data 的每一个属性,都有一个 dep 对象。


watcher 的这个数组,成员是 dep 对象。dep 的这个数组,成员是 watcher 对象。


也就是说,通过维护对方的列表,模板和计算属性,知道我用到了哪些属性。data 的属性,也知道哪些模板和计算属性用到了我。

 

三、依赖收集


在模板第一次渲染、计算属性第一次被使用时,它们所依赖属性的 getter 会触发,然后就把这个模板或计算属性的 watcher 添加到该属性的依赖列表里(dep 对象的数组)。


同时,这些属性的 dep 对象,也会被添加到模板或计算属性的依赖列表里(watcher 对象的数组)。


这个过程是双向的。我曾经疑问为什么需要在 watcher 里维护依赖列表?因为看上去,属性更新时,通知它的依赖列表里的每一个 watcher,让它们去更新,这个模型似乎就可以了。


原来,有时我们是需要模板主动更新的,比如 $forceUpdate 函数,这时通过 watcher 的依赖列表,就可以查看这些依赖有没有更新,如果都没有更新,就无需重新渲染,提高了性能。

 

四、依赖更新


在一个属性发生变化时,这个属性的 setter 被触发,它会通知依赖列表里的每一个 watcher,让它们去更新。


渲染 watcher 接到通知,会重新渲染页面。计算属性 watcher 接到通知,会进行重新计算。


实际的模型比这要复杂。组件的更新过程是异步的,当被通知重新渲染时,不会立即触发,而是将组件标记为“待更新”。Vue 使用一个异步队列来批量处理这些更新,以提高性能。这意味着在同一事件循环中,多次改变数据只会导致一次组件更新和重新渲染。


同样,通知计算属性重新计算,也不会立即触发,而是把计算属性标记为“待更新”,直到该计算属性下一次被使用时(比如重新渲染),才会重新计算。

 

五、原理之上的应用


明白了原理,我们可以弄清楚很多问题,比如:


(1)vue 中的哪些数据是响应式的?


props、data、computed:前两个我们好理解,这里需要注意的是计算属性。思考下面一个问题:

模板中用到一个计算属性,那么它的渲染 watcher 的依赖列表里,是这个计算属性,还是这个计算属性所依赖的 data 属性?


答案是:这个计算属性。这是因为,计算属性本身也是响应式的,同样会被 Object.defineProperty 处理。计算属性的效果就是一层缓存,它不仅会被模板用到,还可能被其他计算属性用到。在这个案例中,当计算属性依赖的 data 改变时,会先触发计算属性的重新计算,只有计算后的值和原来不同,模板才会重新渲染,反之,就无需重新渲染。


另外,$route 和 $store.state 也是响应式的,原理和其他的一样。意味着,如果模板中用到了它俩,它俩改变时模板是会重新渲染的(计算属性也一样,会重新计算)。


(2)我们知道,一个模板中会用到各种数据:data 属性、计算属性、表达式、methods 中的方法、全局的自定义函数。那么当模板重新渲染时,它们各自会怎么样呢?


计算属性:只有计算属性的依赖发生变化时,它才会在重新渲染时重新计算。前者会把计算属性标记为“待更新”,重新计算则会等到下一次被使用(比如重新渲染)时才会进行。


表达式、methods 中的方法、全局的自定义函数:每次重新渲染都会重新计算,因为它们的值不会被缓存,所以要尽可能多的使用计算属性。


(3)什么会触发组件的重新渲染?


组件只有在模板依赖的数据发生变化时,才会重新渲染。那些模板中没用到的数据,改变并不会让模板重新渲染。并且,这种依赖是属性级别的,也就是说,模板中用到了 data 中的一个对象,但这个对象的改变不一定导致重新渲染,因为改变的属性不一定是模板用到的那个。


父组件和子组件,它们的渲染也没有必然的联系。子组件的 data 发生变化,不会导致父组件重新渲染,因为父组件不会用到子组件的数据。父组件的 data 发生变化,也只有它自己,和用到该数据的子组件会重新渲染。不过要注意,如果父组件是销毁了重新创建,那么子组件也只能跟着销毁重新创建。另外,如果父组件对子组件使用了 v-if、v-for(搭配 key 使用)、key,那么子组件很可能会随着它们的变化而销毁重建。


(4)为什么 vue 无法监听对象和数组的某些操作?


明白了 vue2 的响应式原理,也就理解了为什么,vue 无法监听到对象属性的添加和删除,因为 vue2 只能劫持对象属性的取值和赋值。想给响应对象添加属性,应该使用 Vue.set()或者 this.$set()。

数组的限制是,无法监听到通过索引直接赋值和修改数组的长度。我暂时无法解释,不过我的方法时统一用 splice 方法来替代。


文章转载自:路泽宇

原文链接:https://www.cnblogs.com/luzeyu/p/17877545.html

 

用户头像

EquatorCoco

关注

还未添加个人签名 2023-06-19 加入

还未添加个人简介

评论

发布
暂无评论
vue的响应式原理:依赖追踪_Vue_EquatorCoco_InfoQ写作社区