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 不能检测到属性被删除的限制。