在明白原理之前,我们有很多表面现象、使用场景需要记忆。明白了原理后,你会发现它们已经不需要记了,因为从原理出发,你自己都能把它们推导出来,一切是那么的自然而然。感觉就是:这还用记吗?很明显嘛!
之前我对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方法来替代。
本人水平非常有限,写作主要是为了把自己学过的东西捋清楚。如有错误,还请指正,感激不尽。