Vue 2 阅读理解(十六)之响应式系统(二)Observer
响应式系统(二)
在上一节 响应式系统(一) 中,对 Vue 的数据响应式处理做了一点点介绍。整个数据的处理过程,即是通过 Object.defineProperty 方法来处理组件 实例化时传递并被 mergeOptions 方法处理成标准对象形式的参数 options 中的部分数据,例如 props,data 等。
而 Object.defineProperty 为了避免对原始的项目或者代码造成侵入性改变,内部依然沿用 默认的对象 getter/setter 方法,只是在 getter/setter 过程中增加了 对该属性的依赖收集与更新派发的处理。该过程位于函数 defineReactive() 方法内部,代码位于 src/core/observer/index.ts
Vue 2 的 v3 语法支持所使用的 ref、reactive 等相关方法,核心也是利用的 Observer 和 defineReactive,而构造函数 Observer 一样使用 defineReactive 来完成数据响应式处理。
很多文章都说过 Vue 的响应式系统包括三个部分:Observer,Dep,Watcher。这里逐一介绍
1. Observer
作为一个构造函数(也可以称为“类”),官方定义是:附加(挂载)到每个被观察对象的观察者类(构造函数)。当附加结束后,观察者将目标对象的“属性-键”转换为可以实现“收集依赖项”和“调度更新”的 getter/setter。 该类接收一个必传参数 value,当然这个 value 必须是一个对象或者数组;在 2.7 版本之后增加了两个新参数 shadow 和 mock,用来处理 v3 语法的 浅层响应 和 服务端响应式模拟
函数基本定义如下:
export class Observer {
dep: Dep
vmCount: number
constructor(public value: any, public shallow = false, public mock = false) {
this.dep = mock ? mockDep : new Dep()
this.vmCount = 0
def(value, '__ob__', this)
if (isArray(value)) {
if (!mock) {
if (hasProto) {
;(value as any).__proto__ = arrayMethods
} else {
for (let i = 0, l = arrayKeys.length; i < l; i++) {
const key = arrayKeys[i]
def(value, key, arrayMethods[key])
}
}
}
if (!shallow) {
this.observeArray(value)
}
} else {
const keys = Object.keys(value)
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
defineReactive(value, key, NO_INIITIAL_VALUE, undefined, shallow, mock)
}
}
}
observeArray(value: any[]) {
for (let i = 0, l = value.length; i < l; i++) {
observe(value[i], false, this.mock)
}
}
}
上面的代码中,在 Observer 类上声明了两个属性:dep 和 vmCount,用来存放依赖该对象的 watcher 和将该对象作为根数据的 vm 实例的数量。
在 new Observer() 时,大致过程如下:
-
给该对象增加一个 **非枚举属性 __ob__ **,用来标识该对象已经被响应式处理
-
区分对象/数组,分别进行响应式处理
-
对象
作为对象时,会直接遍历对象的 key,通过 defineReactive 来处理每个属性;如果这个属性值没有初始值的话,还会初始化一个空对象作为默认值来进行处理
-
数组
因为数组的某些特性虽然与对象类似,但是在使用时通常数组有更多的内置(Array 原型)方法,并且通过默认方法来更新数组的话没有办法触发整个响应式系统;所以 Vue 会重写数组原型链方法,并且将重写后的方法重新更新到数组上(如果可以直接访问 __proto__ 的话,则会直接修改原型链指向)。
然后,则是遍历整个数组,通过 observe 方法来处理每一项数组的元素
-
2. observe
该方法其实就是对数据进行一定校验,并返回一个 Observer 实例。官方解释为:尝试为一个值创建一个 Observer 观察者实例,如果成功则返回新观察者;如果该值已经有一个观察者实例,则返回现有观察者。
基本定义如下:
export function observe(value: any, shallow?: boolean, ssrMockReactivity?: boolean): Observer | void {
if (!isObject(value) || isRef(value) || value instanceof VNode) {
return
}
let ob: Observer | void
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if (
shouldObserve &&
(ssrMockReactivity || !isServerRendering()) &&
(isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value.__v_skip
) {
ob = new Observer(value, shallow, ssrMockReactivity)
}
return ob
}
这个逻辑也很清晰,排除掉 ssr 等其他因素,简单介绍就是:
如果这个值 是个对象且已经包含一个 Observer 实例属性
__ob__
,则直接返回原有的观察者__ob__
如果这个值 是个对象或者数组 且这个值支持扩展属性,则创建一个新的 Observer 观察者实例并返回
当然细心的同学可能发现这里还有一个判断条件 shouldObserve,这个遍历是整个 observer.ts 文件中的一个 boolean 变量(也可以看做闭包保存的一个变量),会在整个过程中保持一个状态,并且可以通过 toggleObserving() 方法来改变,用来开启/关闭某些时刻的数据响应式处理。
3. defineReactive
这个方法其实在上一节中已经有了介绍,内部就是 Object.defineProperty 处理对象属性的过程,这里就不再赘述了。
但是,该方法会返回一个 依赖该属性数据变化的响应订阅器 Dep 实例,并且将该依赖添加到订阅列表 subs 中。
当然,Vue 为了避免收集多余依赖,在后面初始化渲染 watcher (computed、watch 当然也会)的时候,会对没有使用到的 dep 依赖订阅进行清理。
4. set 与 delete
总所周知,Vue 2 没有办法直接监听 对象的未声明属性直接赋值以及数组下标修改和删除,所以为了解决这种情况,提供了 set/del(delete) 两个方法来重新触发数据响应。
两个方法的基本定义如下:
export function set<T>(array: T[], key: number, value: T): T
export function set<T>(object: object, key: string | number, value: T): T
export function set(target: any[] | Record<string, any>, key: any, val: any): any {
if (isReadonly(target)) {
return
}
const ob = (target as any).__ob__
if (isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key)
target.splice(key, 1, val)
if (ob && !ob.shallow && ob.mock) {
observe(val, false, true)
}
return val
}
if (key in target && !(key in Object.prototype)) {
target[key] = val
return val
}
if ((target as any)._isVue || (ob && ob.vmCount)) {
return val
}
if (!ob) {
target[key] = val
return val
}
defineReactive(ob.value, key, val, undefined, ob.shallow, ob.mock)
ob.dep.notify()
return val
}
export function del<T>(array: T[], key: number): void
export function del(object: object, key: string | number): void
export function del(target: any[] | object, key: any) {
if (isArray(target) && isValidArrayIndex(key)) {
target.splice(key, 1)
return
}
const ob = (target as any).__ob__
if ((target as any)._isVue || (ob && ob.vmCount)) return
if (isReadonly(target)) return
if (!hasOwn(target, key)) return
delete target[key]
if (!ob) return
ob.dep.notify()
}
上面的代码省略了开发环境的错误提示
其实两个方法实现的功能都很单一:更新/删除对应的属性,通过 dep.notify() 触发更新。
当然,这两个方法也有一点基础判断:
set:
- 只读属性禁止更新,直接返回
- 更新数组数据,会校验下标,并调用 splice 更新原数组(这里为什么直接 return?因为 Vue 重写了数组的 splice 方法,在执行 target.splice 就会触发更新),如果不是浅层响应,还会重新对传入的属性值通过 observe 方法进行初始化
- 对于 已经定义的非原型链属性,会直接设置新值
- 如果更新的值是一个 Vue 组件实例或者根数据对象,也会直接return
- 原对象不是一个响应对象,当然也直接返回
- 最后的情况就是,原对象是一个响应式对象,并且新的对象属性没有被定义过,则通过 defineReactive 重新处理新属性和属性值,并派发数据更新
del(delete):
- 删除数组元素,调用 splice
- 如果原对象是一个 Vue 组件实例或者根数据对象,也会直接return
- 只读数据或者无法找到的属性,直接 return
- 删除对象属性,如果原对象是一个响应式数据,则触发 dep.notify() 派发数据更新,否则直接 return
转载自:https://juejin.cn/post/7136509987953573895