背景
近期发现组内的同学在需求开发中,经常会遇到Vue数据改变但视图没有更新的问题,最后发现主要的问题还是大家对Vue的响应式原理理解的不够深入,为了帮助大家真正的理解Vue响应式,避免在以后的开发过程中出现同样的问题,我们在组内组织了Vue响应式相关的源码分享,下边我将分享的内容整理如下,方便大家日后回顾和理解
什么是响应式
在开始之前,我们首先需要清楚究竟什么是响应式。简单来说,就是当你修改data中的数据时,视图会重新渲染,更新为最新的值。这使得我们的状态管理变的非常的简单,我们只需要关注数据本身,而不用关心数据的渲染,大大的提高了我们的开发效率。但这也时常会给我们带来一些问题,当我们的代码没有得到我们预期的执行结果时,我们常常也会束手无策,不知道如何去排查问题的原因,所以理解其工作原理就变得十分重要
Vue是如何实现响应式的
现在,假设我们自己要实现Vue的响应式,需要解决哪些问题?
- Vue如何侦测数据的变更?
- Vue如何知道应该通知哪些视图进行更新?
- Vue如何通知需要更新的视图?
对于上边的三个问题,Vue都给出了对应的解决方案
- 数据劫持
- 依赖收集
- 依赖更新
数据劫持的核心其实就是Object.defineProperty方法,这个方法的具体使用方式这里我们不做过多解释,想要了解的可以移步此处进行查看,这里我们需要重点关注的是它的get和set,可以对属性的获取和设置操作进行拦截。当然不同于Vue2,Vue3是通过ES6的Proxy进行数据代理的方式进行数据变更侦测的,Proxy 的代理是针对整个对象的,而不是对象的某个属性,因此不同于 Object.defineProperty 的必须遍历对象每个属性,Proxy 只需要做一层代理就可以监听同级结构下的所有属性变化,此外Proxy支持代理数组的变化
关于依赖收集和依赖更新,在Vue中主要依赖三个类来实现,分别是Observer、Dep、Watcher,它们三个一起就组成了一个发布订阅模式(不了解发布订阅模式的可以参考这篇文章)
Observer
Observer作为一个发布者,它会将普通对象变成响应式的对象,这样当对象的属性被引用时它会收集依赖,当对象的属性被修改时,它会触发依赖更新。Observer会递归遍历对象的每一个属性,确保对象及子对象的所有属性都变成响应式的
observe方法作为Vue响应式的一个重要的入口方法,其作用就是把参数传入的对象变成响应式对象
export function observe (value: any, asRootData: ?boolean): Observer | void {
if (!isObject(value) || value instanceof VNode) { // 如果不是对象类型或者属于VNode类型就直接返回
return
}
let ob: Observer | void
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { // 判断是否存在__ob__属性,如果存在说明已经是响应式对象了
ob = value.__ob__
} else {
ob = new Observer(value) // 如果不是响应式对象,则创建Observer实例,将其变成响应式对象
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
Observer类会对对象和数组分别进行处理,如果是对象,就遍历对象,调用defineReactive方法对每一个属性进行数据劫持;如果是数组,就遍历数组,调用observe方法对每一个元素进行Observer实例化
export class Observer {
value: any;
dep: Dep;
constructor (value: any) {
this.value = value
this.dep = new Dep() // 创建Dep实例用于收集依赖
def(value, '__ob__', this) // 将当前Observer实例挂载到目标对象的__ob__属性上,代表此对象已经变成了响应式对象
if (Array.isArray(value)) { // 判断是否为数组
if (hasProto) { // 判断当前环境是否支持__proto__属性
protoAugment(value, arrayMethods) // 如果支持,就通过覆盖__proto__的方式重写数组的方法
} else {
copyAugment(value, arrayMethods, arrayKeys) // 否则,就通过Object.defineProperty的方式去重新定义数组的方法
}
this.observeArray(value)
} else {
this.walk(value)
}
}
walk (obj: Object) { // 遍历对象的每一个属性通过defineReactive进行数据劫持
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
observeArray (items: Array<any>) { // 遍历数组的每一个元素对其进行响应式处理
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
defineReactive方法作为Vue响应式里边最核心的一个方法,主要的作用是劫持对象属性的get和set操作,并在get中收集依赖,在set中通知依赖更新
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep() // 创建Dep实例用于收集依赖
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () { // 拦截get方法,进行依赖收集
const value = val
if (Dep.target) { // 判断是否存在监听器Watcher
dep.depend() // 如果存在则将其添加到dep.subs中去,此处很多同学可能会问为什么不直接调用dep.addSub添加依赖,在下边的Dep源码分析的时候我会再做说明
}
return value
},
set: function reactiveSetter (newVal) { // 拦截set方法进行依赖更新
val = newVal
dep.notify()
}
})
}
对于此处的Dep.target容易让人产生困惑,它的值对应的是一个Watcher实例,在Vue实例初始化解析模板的时候会创建当前实例的Watcher,而这个Watcher就会被赋值给Dep.target,因此这个Dep.target代表的就是触发get回调的当前正在被解析的模板所属的Vue实例对应的Watcher,所以它是全局唯一的
Dep
Dep扮演的是调度中心的角色,它用于存储依赖,所以每个属性都会拥有自己的Dep实例,当属性的值发生变化时,会遍历订阅者列表,通知所有的订阅者执行自己的更新操作
export default class Dep {
static target: ?Watcher; // 全局的Watcher
id: number;
subs: Array<Watcher>; // 用于存储依赖Watcher的列表
constructor () {
this.id = uid++
this.subs = []
}
addSub (sub: Watcher) { // 添加依赖
this.subs.push(sub)
}
removeSub (sub: Watcher) { // 删除依赖
remove(this.subs, sub)
}
depend () {
if (Dep.target) { // 如果当前有订阅者Watcher,将该dep放进当前订阅者的deps中,并且将当前的订阅者放入订阅者列表subs中
Dep.target.addDep(this)
}
}
notify () { // 通知依赖更新
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update() // 获取到每一个Watcher,并调用其update方法进行视图的更新
}
}
}
对于上边depend方法,跟踪源码一圈后发现最终还是调用了addSub方法添加了依赖,那为什么要绕这么一圈而不是直接调用addSub呢,其实看过Watcher的源码后就会发现,在Watcher的addDep方法中首先将当前dep添加到了Watcher维护deps列表中,然后才将当前Watcher实例添加到了dep维护的subs列表中,这样其实形成了一个双向依赖的关系,所以可以看出,在Vue中一个发布者会被多个订阅者订阅,同时一个订阅者也会订阅多个发布者
Watcher
Watcher就是我们上边说的订阅者,它的主要作用就是接收Dep调度中心的通知进行视图的更新,所以每一个Vue实例都会拥有一个专属的Watcher。在Vue中,Watcher分为渲染Watcher、计算属性Watcher和侦听器Watcher三种,它们分别会监听data、computed和watch。Watcher的代码相对比较复杂,下边的代码会做相应的简化处理方便大家理解核心原理
export default class Watcher {
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
this.vm = vm
if (isRenderWatcher) { // 如果是渲染Watcher,将当前Watcher实例绑定到当前Vue实例的_watcher属性上
vm._watcher = this
}
vm._watchers.push(this) // 将当前Watcher实例加入到Vue实例维护的_watchers列表中
...
this.deps = [] // Watcher实例维护的Dep实例的依赖列表,其实Watcher和Dep是一个双向依赖的关系
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
this.expression = process.env.NODE_ENV !== 'production'
? expOrFn.toString()
: ''
if (typeof expOrFn === 'function') { // 如果是函数则为渲染Watcher或者侦听器Watcher
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn) // 如果是表达式则为计算属性Watcher
}
this.value = this.lazy // lazy为true则说明是计算属性Watcher,不需要立即求值
? undefined
: this.get()
}
get () {
...
let value
const vm = this.vm
...
value = this.getter.call(vm, vm) // 调用回调函数,也就是upcateComponent,触发依赖收集
...
return value
}
addDep (dep: Dep) { // 此处实现了Dep和Watcher的双向依赖
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}
update () { // Watcher更新视图的方法
if (this.lazy) { // 计算属性Watcher
this.dirty = true
} else if (this.sync) { // sync为true的侦听器Watcher
this.run()
} else { // 将当前Watcher实例追加到队列中进行异步更新
queueWatcher(this)
}
}
run () { // 执行watcher的回调函数cb
if (this.active) {
const value = this.get()
if (
value !== this.value ||
isObject(value) ||
this.deep
) {
// set new value
const oldValue = this.value
this.value = value
this.cb.call(this.vm, value, oldValue) // 此处会传入侦听器Watcher回调中的value和oldValue
}
}
}
}
这里有一块比较关键的代码,串联起了整个依赖收集的过程,代码如下
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
callHook(vm, 'beforeMount')
let updateComponent = () => {
vm._update(vm._render(), hydrating) // 调用渲染函数,生成虚拟dom,
}
new Watcher(vm, updateComponent, noop, { // 创建当时Vue实例的watcher,当数据发生变化的时候就会触发updateComponent
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
hydrating = false
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
总结
最终,Vue实例的data对象会形成如下的依赖关系
现在,我们梳理一下整个流程
- Vue实例初始化,调用observe方法将data对象转换成响应式对象,同时创建Dep用来收集依赖Watcher
- 收集依赖,过程如下:
- Vue实例挂载之前会实例化一个渲染watcher,在Watcher构造函数里get方法被执行
- 执行this.getter.call(vm, vm),其实就是我们上面的看到的updateComponent方法
- 执行vm._render()生成虚拟DOM vnode,这个过程中就会触发响应式对象的getter回调
- 在getter中调用Dep.depend()方法收集依赖
- 当属性的值被修改时,会触发属性的set方法,然后调用Dep.notify通知所有依赖该属性的Watcher去更新视图