likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 43

响应式系统(二)

在上一节 响应式系统(一) 中,对 Vue 的数据响应式处理做了一点点介绍。整个数据的处理过程,即是通过 Object.defineProperty 方法来处理组件 实例化时传递并被 mergeOptions 方法处理成标准对象形式的参数 options 中的部分数据,例如 props,data 等。

Object.defineProperty 为了避免对原始的项目或者代码造成侵入性改变,内部依然沿用 默认的对象 getter/setter 方法,只是在 getter/setter 过程中增加了 对该属性的依赖收集与更新派发的处理。该过程位于函数 defineReactive() 方法内部,代码位于 src/core/observer/index.ts

Vue 2 的 v3 语法支持所使用的 ref、reactive 等相关方法,核心也是利用的 ObserverdefineReactive,而构造函数 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
转载自:https://juejin.cn/post/7136509987953573895
评论
请登录