什么是数据响应式
vue 数据响应式设计的初衷是为了实现数据和函数的联动,当数据变化后,用到该数据的联动函数会自动重新运行。
具体在 vue 的开发中,数据和组件的 render 函数关联在一起,从而实现了数据变化自动运行 render,在感官上就看到了组件的重新渲染。
除了 vue 自动关联的 render 函数,其他还有很多使用到 vue 响应式的场,景,比如 computed、watch 等等,不能仅把 vue 的数据响应式想象成和 render 的关联。
响应式原理
响应式数据的最终目标,是当对象本身或对象属性发生变化时,将会运行一些函数,最常见的就是 render 函数。在具体实现上,vue 用到了几个核心部件:
- Observer
- Dep
- Watcher
- Scheduler
Observer 在底层都做了什么
Observer 要实现的目标非常简单,就是把一个普通的对象转换为响应式的对象
为了实现这一点,Observer 把对象的每个属性通过Object.defineProperty
转换为带有getter
和setter
的属性, 这样一来,当访问或设置属性时,vue 就有机会做一些别的事情。
Observer 是 vue 内部的构造器,我们可以通过 Vue 提供的静态方法Vue.observable(object)
间接的使用该功能。
在组件生命周期中,这件事发生在beforeCreate
之后,created
之前。
具体实现上,它会递归遍历对象的所有属性,以完成深度的属性转换。
由于遍历时只能遍历到对象的当前属性,因此无法监测到将来动态增加或删除的属性,因此vue
提供了 $set
和$delete
两个实例方法,让开发者通过这两个实例方法对已有响应式对象添加或删除属性。
对于数组,vue
会更改它的隐式原型,之所以这样做,是因为 vue 需要监听那些可能改变数组内容的方法
总之,Observer 的目标,就是要让一个对象,它属性的读取、赋值,内部数组的变化都要能够被 vue 感知到。
function Observer(data_instance) {
if (!data_instance || typeof data_instance !== "object") return;
Object.keys(data_instance).forEach((key) => {
let value = data_instance[key];
Observer(value);
Object.defineProperty(data_instance, key, {
enumerable: true,
configurable: true,
get() {
console.log(`获取了属性值 ${value}`);
return value;
},
set(newVal) {
console.log(`修改了属性值`);
value = newVal;
},
});
});
}
Dep 解决读取属性和属性变化时要做什么事儿的问题
这里有两个问题没解决,就是读取属性时要做什么事,而属性变化时要做什么事,这个问题需要依靠 Dep 来解决。
Dep 的含义是 Dependency
,表示依赖的意思。
vue
会为响应式对象中的每个属性、对象本身、数组本身创建一个Dep
实例,每个Dep
实例都有能力做以下两件事:
-
记录依赖:是谁在用我
-
派发更新:我变了,我要通知那些用到我的人
当读取响应式对象的某个属性时,它会进行依赖收集:有人用到了我
当改变某个属性时,它会派发更新:那些用我的人听好了,我变了
Watcher,在数据响应式过程中扮演“观察者”
这里又出现一个问题,就是 Dep 如何知道是谁在用我?
要解決这个问题,需要依靠另一个东西,就是 Watcher。
当某个函数执行的过程中,用到了响应式数据,响应式数据是无法知道是哪个函数在用自己的。因此,vue 通过一种巧妙的办法来解决这个问题
我们不要直接执行函数,而是把函数交给一个叫做 watcher 的东西去执行,watcher 是一个对象,每个这样的函数执行时都应该创建一个 watcher,通过 watcher 去执行
watcher 会设置一个全局变量,让全局变量记录当前负责执行的 watcher 等于自己,然后再去执行函数,在函数的执行过程中,如果发生了依赖记录 dep.depend()
,那么 Dep
就会把这个全局变量记录下来,表示:有一个 watcher 用到了我这个属性
当 Dep 进行派发更新时,它会通知之前记录的所有 watcher:我变了
每一个 vue 组件实例,都至少对应一个 watcher,该 watcher 中记录了该组件的 render 函数。
watcher 首先会把 render 函数运行一次以收集依赖,于是那些在 render 中用到的响应式数据就会记录这个
watcher。
当数据变化时,dep 就会通知该 watcher,而 watcher 将重新运行 render 函数,从而让界面重新渲染同时重新记录当前的依赖。
Scheduler
现在还剩下最后一个问题。就是 Dep 通知 watcher 之后,如果 watcher 执行重运行对应的函数,就有可能导致函数频繁运行,从而导致效率低下
试想,如果一个交给 watcher 的函数,它里面用到了属性 a、b、c、d 那么 a、b、c、d 属性都会记录依赖,于是
下面的代码将触发 4 次更新:
state.a = "new data";
state.b = "new data";
state.c = "new data";
state.d = "new data";
这样显然是不合适的,因此,watcher 收到派发更新的通知后,实际上不是立即执行对应函数,而是把自己交给一个叫调度器的东西
调度器维护一个执行队列,该队列同一个 watcher 仅会存在一次,队列中的 watcher 不是立即执行,它会通过一个叫做 nextTick
的工具方法,把这些需要执行的 watcher 放入到事件循环的微队列中,nextTick 的具体做法是通过Promise
完成的
也就是说,当响应式致据变化时,render
函数的执行是异步的,并且在微队列中