Vue源码解析-响应式原理
原理图:

数据变化到 DOM 变化,前端开发工作
- 数据渲染到界面
- 处理用户交互
响应式对象
Vue.js 实现响应式的核心是利用了 ES5 的 Object.defineProperty,这也是为什么 Vue.js 不能兼容 IE8 及以下浏览器的原因,Object.defineProperty 在对象上定义一个新属性,或者修改一个对象的现有属性,并返回这个对象。
其中最关键的是实现了
getter,当访问属性的时候触发,依赖收集setter,当修改属性的时候触发,通知更新
⚠️ 对象以及对象嵌套的对象都会被添加为响应式对象。
initState
initState 方法主要是对 props、methods、data、computed 和 wathcer 等属性做了初始化操作。
proxy
代理的作用是把 props 和 data 上的属性代理到 vm 实例上,这也就是为什么比如我们定义了如下 props,却可以通过 vm 实例访问到它。
observe
observe 方法的作用就是给非 VNode 的对象类型数据添加一个 Observer,如果已经添加过则直接返回,否则在满足一定条件下去实例化一个 Observer 对象实例。
Observer
Observer 是一个类,它的作用是给对象的属性添加 getter 和 setter,用于依赖收集和派发更新。
在它的构造函数中会执行 this.walk(value),
1 | walk (obj: Object) { |
这里的 defineReactive 方法是给对象动态添加 getter、setter,
函数最开始初始化 Dep 对象的实例,接着拿到 obj 的属性描述符,然后对子对象递归调用 observe 方法,这样就保证了无论 obj 的结构多复杂,它的所有子属性也能变成响应式的对象,这样我们访问或修改 obj 中一个嵌套较深的属性,也能触发 getter 和 setter。
依赖收集
响应式对象 getter 相关的逻辑就是做依赖收集,const dep = new Dep() 实例化一个 Dep 的实例,在 get 函数中通过 dep.depend 做依赖收集。
Dep
Dep 是 depend 的缩写,是整个 getter 依赖收集的核心,在 getter 方法中,通过 const dep = new Dep() 实例化一个 Dep 的实例,然后通过 dep.depend 做依赖收集。
Dep 有一个静态属性 target,这是当前唯一的全局 Watcher,因为同一时间只有一个全局的 Watcher 被计算,另外它的自身属性 subs 也是 Watcher 数组。
Watcher
1 | /** |
清空依赖
Vue 设计了在每次添加完新的订阅,会移除掉旧的订阅,这样就保证了在我们刚才的场景中,如果渲染 b 模板的时候去修改 a 模板的数据,a 数据订阅回调已经被移除,每次执行 depend, render 不会订阅 v-if 为 false 的数据。
总结
- 依赖收集就是订阅数据变化的
watcher的收集 - 依赖收集的目的是为了当这些响应式数据发送变化,触发它们的
setter的时候,能知道应该通知哪里订阅者去做相应的逻辑处理
派发更新
setter 的逻辑有 2 个关键的点
一个是
childOb = !shallow && observe(newVal),如果shallow为false的情况,会对新设置的值变成一个响应式对象;另一个是
dep.notify(),通知所有的订阅者
dep.notify()
该方法是 Dep 实例的一个方法。1
2
3
4
5
6
7
8
9
10class 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 的实例数组,然后调用每一个 watcher 的 update 方法。
📌 每一个
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
9var 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
9const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
Vue 重写了 这些方法,如果有新值插入到数组中,就将这些新值变成响应式对象,并且再次调用 ob.dep.notify() 手动触发依赖通知(触发依赖收集)。
Vue.delete
Vue.delete( target, key ) 类似于 Vue.set 方法, 删除对象的属性。如果对象是响应式的,确保删除能触发更新视图。这个方法主要用于避开 Vue 不能检测到属性被删除的限制。