Vue源码解析-响应式原理

Vue源码解析-响应式原理

原理图:

数据变化到 DOM 变化,前端开发工作

  • 数据渲染到界面
  • 处理用户交互

响应式对象

Vue.js 实现响应式的核心是利用了 ES5Object.defineProperty,这也是为什么 Vue.js 不能兼容 IE8 及以下浏览器的原因,Object.defineProperty 在对象上定义一个新属性,或者修改一个对象的现有属性,并返回这个对象。

其中最关键的是实现了

  • getter,当访问属性的时候触发,依赖收集
  • setter,当修改属性的时候触发,通知更新

⚠️ 对象以及对象嵌套的对象都会被添加为响应式对象。

initState

initState 方法主要是对 propsmethodsdatacomputedwathcer 等属性做了初始化操作。

proxy

代理的作用是把 propsdata 上的属性代理到 vm 实例上,这也就是为什么比如我们定义了如下 props,却可以通过 vm 实例访问到它。

observe

observe 方法的作用就是给非 VNode 的对象类型数据添加一个 Observer,如果已经添加过则直接返回,否则在满足一定条件下去实例化一个 Observer 对象实例。

Observer

Observer 是一个类,它的作用是给对象的属性添加 gettersetter,用于依赖收集和派发更新。

在它的构造函数中会执行 this.walk(value)

1
2
3
4
5
6
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}

这里的 defineReactive 方法是给对象动态添加 gettersetter

函数最开始初始化 Dep 对象的实例,接着拿到 obj 的属性描述符,然后对子对象递归调用 observe 方法,这样就保证了无论 obj 的结构多复杂,它的所有子属性也能变成响应式的对象,这样我们访问或修改 obj 中一个嵌套较深的属性,也能触发 gettersetter

依赖收集

响应式对象 getter 相关的逻辑就是做依赖收集,const dep = new Dep() 实例化一个 Dep 的实例,在 get 函数中通过 dep.depend 做依赖收集。

Dep

Depdepend 的缩写,是整个 getter 依赖收集的核心,在 getter 方法中,通过 const dep = new Dep() 实例化一个 Dep 的实例,然后通过 dep.depend 做依赖收集。

Dep 有一个静态属性 target,这是当前唯一的全局 Watcher,因为同一时间只有一个全局的 Watcher 被计算,另外它的自身属性 subs 也是 Watcher 数组。

Watcher

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* A watcher parses an expression, collects dependencies,
* and fires callback when the expression value changes.
* This is used for both the $watch() api and directives.
*
* watcher 观察一个数据,当数据的值发生改变时执行回调方法,watcher也被用于 $watch() * api 和 directives
*/

// 构造函数
this.deps = [] // 表示 Watcher 实例持有的 Dep 实例数组
this.newDeps = [] // 表示 Watcher 实例持有的 Dep 实例数组
this.depIds = new Set()
this.newDepIds = new Set()

清空依赖

Vue 设计了在每次添加完新的订阅,会移除掉旧的订阅,这样就保证了在我们刚才的场景中,如果渲染 b 模板的时候去修改 a 模板的数据,a 数据订阅回调已经被移除,每次执行 dependrender 不会订阅 v-iffalse 的数据。

总结

  • 依赖收集就是订阅数据变化的 watcher 的收集
  • 依赖收集的目的是为了当这些响应式数据发送变化,触发它们的 setter 的时候,能知道应该通知哪里订阅者去做相应的逻辑处理

派发更新

setter 的逻辑有 2 个关键的点

  • 一个是 childOb = !shallow && observe(newVal),如果 shallowfalse 的情况,会对新设置的值变成一个响应式对象;

  • 另一个是 dep.notify(),通知所有的订阅者

dep.notify()

该方法是 Dep 实例的一个方法。

1
2
3
4
5
6
7
8
9
10
class Dep {
// ...
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}

逻辑非常简单,遍历所有的 subs,也就是 Watcher 的实例数组,然后调用每一个 watcherupdate 方法。

📌 每一个 Vue 的实例都有 Watcher

update 方法中,引入了一个队列,update 不会每次数据发生改变时都会触发 watcher 回调,而是将这些 watcher 先添加到一个队列中,然后再 nextTick 后再执行。

😜 nextTick 可以理解为一个事件循环周期

异常

在数据更新的同时又触发了数据更新,比如在 一个 computed 中执行了 data 数据的修改,而 data 又依赖于 computed,这是 Vue 有机制限定最大更新数为 100,超过则报异常。

nextTick

JS 运行机制

(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。

(2)主线程之外,还存在一个”任务队列”(task queue)。只要异步任务有了运行结果,就在”任务队列”之中放置一个事件。

(3)一旦”执行栈”中的所有同步任务执行完毕,系统就会读取”任务队列”,看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。

(4)主线程不断重复上面的第三步。

总结

比如从服务端接口去获取数据的时候,数据做了修改,如果我们的某些方法去依赖了数据修改后的 DOM 变化,我们就必须在 nextTick 后执行

Vue.js 提供了 2 种调用 nextTick 的方式

  • 一种是全局 API Vue.nextTick
  • 一种是实例上的方法 vm.$nextTick

检测变化的注意事项

  • 了解哪些数据变化不能被检测到
  • 了解如何解决数据变化检测的问题以及其中的原理

当我们去给这个对象添加一个新的属性的时候,是不能够触发它的 setter 的,比如:

1
2
3
4
5
6
7
8
9
var vm = new Vue({
data:{
a:1
aArray: []
}
})
// vm.b 是非响应的
vm.b = 2
aArray[1] = 1 // 直接修改数组也是非响应的

Vue.set

  • 参数:

    • {Object | Array} target
    • {string | number} key
    • {any} value
    • 返回值:设置的值。
  • 用法:

    向响应式对象中添加一个属性,并确保这个新属性同样是响应式的,且触发视图更新。它必须用于向响应式对象上添加新属性,因为 Vue 无法探测普通的新增属性 (比如 this.myObject.newProperty = 'hi‘)

⚠️ 注意对象不能是 Vue 实例,或者 Vue 实例的根数据对象。

数组的情况

操作数组时使用以下方法是可以检测到数组变化的。

1
2
3
4
5
6
7
8
9
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]

Vue 重写了 这些方法,如果有新值插入到数组中,就将这些新值变成响应式对象,并且再次调用 ob.dep.notify() 手动触发依赖通知(触发依赖收集)。

Vue.delete

Vue.delete( target, key ) 类似于 Vue.set 方法, 删除对象的属性。如果对象是响应式的,确保删除能触发更新视图。这个方法主要用于避开 Vue 不能检测到属性被删除的限制。