Vue 2 阅读理解(十六)之响应式系统(二)Observer

1,882 阅读5分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第30天,点击查看活动详情

响应式系统(二)

在上一节 响应式系统(一) 中,对 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() 时,大致过程如下:

  1. 给该对象增加一个 **非枚举属性 __ob__ **,用来标识该对象已经被响应式处理

  2. 区分对象/数组,分别进行响应式处理

    • 对象

      作为对象时,会直接遍历对象的 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:

  1. 只读属性禁止更新,直接返回
  2. 更新数组数据,会校验下标,并调用 splice 更新原数组(这里为什么直接 return?因为 Vue 重写了数组的 splice 方法,在执行 target.splice 就会触发更新),如果不是浅层响应,还会重新对传入的属性值通过 observe 方法进行初始化
  3. 对于 已经定义的非原型链属性,会直接设置新值
  4. 如果更新的值是一个 Vue 组件实例或者根数据对象,也会直接return
  5. 原对象不是一个响应对象,当然也直接返回
  6. 最后的情况就是,原对象是一个响应式对象,并且新的对象属性没有被定义过,则通过 defineReactive 重新处理新属性和属性值,并派发数据更新

del(delete):

  1. 删除数组元素,调用 splice
  2. 如果原对象是一个 Vue 组件实例或者根数据对象,也会直接return
  3. 只读数据或者无法找到的属性,直接 return
  4. 删除对象属性,如果原对象是一个响应式数据,则触发 dep.notify() 派发数据更新,否则直接 return