【Vue2】数据响应原理

【Vue2】数据响应原理
Aiden Kaovue 官方阐述:响应式原理
响应式数据的最终目标,是当对象本身或对象属性发生变化时,将会运行一些函数,最常见的就是 render 函数。
在具体实现上,vue 用到了几个核心部件:
- Observer
- Dep
- Watcher
- Scheduler
Observer
Observer 要实现的目标非常简单,就是把一个普通的对象转换为响应式的对象
为了实现这一点,Observer 把对象的每个属性通过Object.defineProperty
转换为带有getter
和setter
的属性,这样一来,当访问或设置属性时,vue
就有机会做一些别的事情。
Observer 是 vue 内部的构造器,我们可以通过 Vue 提供的静态方法Vue.observable( object )
间接的使用该功能。
在组件生命周期中,这件事(data 配置返回的对象转换成响应式数据)发生在beforeCreate
之后,created
之前。
具体实现上,它会递归遍历对象的所有属性,以完成深度的属性转换。
由于遍历时只能遍历到对象的当前属性,因此无法监测到将来动态增加或删除的属性,因此vue
提供了$set
和$delete
两个实例方法,让开发者通过这两个实例方法对已有响应式对象添加或删除属性。
对于数组,vue
会更改它的隐式原型(该隐式原型中 vue 实现了 pop,push,reverse,sort,splice,unshift 方法),之所以这样做,是因为 vue 需要监听那些可能改变数组内容的方法
arr.__protp__ === vue 自定义的对象
vue 自定义对象.__proto__ = Array.prototype
总之,Observer 的目标,就是要让一个对象,它属性的读取、赋值,内部数组的变化都要能够被 vue 感知到。
- Observer 是干什么的?
- 怎么干的?
- 什么时候干的?
- 这么干有什么问题?
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 次更新:
1 | state.a = "new data"; |
这样显然是不合适的,因此,watcher 收到派发更新的通知后,实际上不是立即执行对应函数,而是把自己交给一个叫调度器的东西
调度器维护一个执行队列,该队列中同一个 watcher 仅会存在一次,队列中的 watcher 不是立即执行,它会通过一个叫做nextTick
的工具方法,把这些需要执行的 watcher 放入到事件循环的微队列中,nextTick 的具体做法是通过Promise
完成的
nextTick 通过
this.$nextTick
暴露给开发者
也就是说,当响应式数据变化时,render
函数的执行是异步的,并且在微队列中