浅曦Vue源码-9-响应式数据-initState(3)-初始化prop

457 阅读2分钟

「这是我参与2022首次更文挑战的第10天,活动详情查看:2022首次更文挑战」。

一、前情回顾 & 背景

在上一篇中,我们基本上已经交到清楚 initProps 都发生了什么,其核心就在于通过 defineReactivevm.$options.propsOptions 上我们配置的 props 都变成响应式数据,核心在于 observe 方法,而observe 方法的核心是 Observe 类;

此外,在实现响应式数据的过程中,有一个时至今日我们一直在提及但从未谋面的狠人,他就是 Dep,我们口口声声说的依赖收集、派发更新就是他的看家本事了,今天他来了。

一、、Observer

类的位置:src/core/observer/index.js -> class Observer

类的作用:创建观察者类的实例,其构造函数接收一个被观察对象 value,创建实例则会将观察者实例作为属性 __ob__ 的值附加到 value 自身上,即 value.__ob__

观察者对象将会把目标对象 value 的属性转换为 gettersetter 用于收集依赖和派发更新。

在构造函数中主要做了以下工作:

  1. 给观察者实例上初始化一个 Dep 实例,这个就是用来做依赖收集用的;
  2. 在被观察的 value 对象上添加 __ob__ 属性,值是 this,即观察者对象本身;
  3. 如果判断 value 是数组,则重写数组原型上的操作数组的方法,用以实现数组的响应式,接着调用 this.observeArray(value) 将数组项也变成数据响应式,这就是为啥数组没有 gettersetter 照样能有响应式的根本原因;
  4. 如果 value 是个对象,则调用 this.walk(value) 方法遍历,把 value 中所有子属性及后代属性都变为响应式,这也解释了为啥 observe 里面没有见到递归的 defineReactive 调用,这一步已隐式的在这里完成了
export class Observer {
  value: any;
  dep: Dep;
  vmCount: number;

  constructor (value: any) {
    this.value = value

    // 实例化一个 dep
    this.dep = new Dep()
    this.vmCount = 0

    // 在 value 对象上设置 __ob__ 属性
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      if (hasProto) {
        // 有 __proto__ 属性,通过原型增强
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      this.observeArray(value)
    } else {
      // value 为对象,递归为对象的每个子属性设置响应式
      this.walk(value)
    }
  }

  // 遍历对象上的每个 key,为每个 key 设置响应式
  walk (obj: Object) {
    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])
    }
  }
}

1.1 protoAugment 方法

方法位置:src/core/observer/index.js -> protoAument 方法作用:接收 targetsrc 参数,用于修改 target 对象的 __proto__ 属性指向,即就修改原型对象的指向,在 new Observer 的时候,当 value 是数组时,通过修改 value__proto__ 指向一个重写过数组方法的对象;

function protoAugment (target, src: Object) {
  target.__proto__ = src
}

1.2 copyAugment

方法位置:src/core/observer/index.js -> copyAugment 方法作用:接收 target、src、keys,向 target 扩展 keys 中的 keykey 对应的值是 src[key]

function copyAugment (target: Object, src: Object, keys: Array<string>) {
  for (let i = 0, l = keys.length; i < l; i++) {
    const key = keys[i]
    def(target, key, src[key])
  }
}

可以看到,前面是先判断对象有没有 __proto__ 属性,如果有的话,就 protoAument 否则才 copyAument。为啥要这么处理?这是因为 __proto__ 不是个标准属性,很可能有的浏览器就没有实现,比如IE,所以就需要 copyAument 来增加实现数组的响应式。

二、Dep

类的位置:src/core/observer/dep.js -> class Dep

类的作用:当响应式数据读取时,收集依赖,前面有说过每个响应式的数据,一个 key 都会有一个 dep,而子属性也会有自己独立的 dep,所谓收集依赖就是收集 watcher,即哪个 watcher 读取了这个 key。当响应式数据更新时派发更新,所谓派发更新就是调用 watcher.update() 方法,使之重新求值;

export default class Dep {
  static target: ?Watcher; // Dep.target 是个 Watcher 类型的值
  id: number;
  subs: Array<Watcher>;

  constructor () {
    this.id = uid++
    this.subs = []
  }

  // 在 dep.subs 中push watcher
  addSub (sub: Watcher) {
    this.subs.push(sub)
  }

  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }
  
  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }
  
  notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
    }
    // 遍历 dep 中存储的 watcher,执行 watcher.update()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

2.1 Dep.prototype.depend

该方法用于向 watcher 中增加 增加 dep,同时向 dep 中增加 watcher;在前面 defineReactive 方法中,最后的 Object.defineProperty 的 getter 中调用了该方法,

Dep.target 是 Dep 类的一个静态属性,值为 Watcher,在实例化 Watcher 时他会被设置,实例化 Watcher 时会执行 new Watcher 时 pushTarget(this),该方法此时会为 Dep.target 赋值当前的 Watcher 实例;

export function defineReactive (params was ignored) {
  // 实例化 dep,一个 key 一个 dep
  const dep = new Dep()
  
  let childOb = !shallow && observe(val)

  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    // get 拦截对 obj[key] 的读取操作
    get: function reactiveGetter () {
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
    }
  })
}

2.3 Dep.prototype.notify

notify 方法就是派发更新的,派发更新就是遍历 dep 收集的这些 watcher ,调用每个 watcher.update() 就可以了;

前面调用 defineReactive 中最后的 Object.defineProperty 法中中的 setter 就调用了 notify

export function defineReactive (params was ignored) {
  // 实例化 dep,一个 key 一个 dep
  const dep = new Dep()
  
  let childOb = !shallow && observe(val)

  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {

    },
    set: function reactiveSetter (newVal) {
      dep.notify() // 派发更新
    }
  })
}

三、总结

到这里我们已经说完了全部的 initProp 的过程,现在我们回顾一下:

  1. 初始化 vm._props 属性,
  2. 然后 for in 遍历 vm.$options.propOptions 对象,这里面放的都是我们 new Vue() 时传递的 props 对象,
  3. 在遍历时,通过 validateProp 方法获取每个 key 的默认值,如果这个值不再 propsData 中,在此过程中会调用 observe(val) 方法对默认值进行观察,将其转换成响应式数据结构:
    • 3.1 observe 方法内部是创建 Observe 的实例,而 Observer 的构造函数执行过程:

      • 3.1.1 会给 val 实例化 dep 属性
      • 3.1.2 并且给 val 添加 __ob__ 属性,值就是 Observer 实例自身
      • 3.1.3 如果 val 是数组,则通过 protoAument/copyAument 覆写数组方法实现数组响应式,接着调用 this.observerArray 实现数组子项的响应式
      • 3.1.4 如果 val 是对象,则 this.walk(),即调用 Observer 原型方法 walk 遍历 val,其核心是递归调用 defineReactive() 方法,将对象的子属性等深层次的子属性都变成响应式
    • 3.2 返回 new Observer 创建的实例,当然也会判重,如果 val 已经是 Observer 实例了则直接范返回

  4. 调用 defineReactive(obj, key, value) 方法,通过 Object.defineProperty() 方法,拦截属性的读取和设置,在读取时进行依赖收集,在设置时派发更新:
    • 4.1 实例化 Dep 的实例 dep = new Dep()
    • 4.2 获取 obj[key] 原有的属性描述对象,然后从描述对象获取 gettersetter
    • 4.3 调用 childObj = observe(val) 方法,observe 内部就是实例化 Observer 类,并会归调用 defineReactive 方法,这里算是 defineReactive 方法的隐式递归调用,实现所有嵌套子属性的响应式
    • 4.4 调用 Object.defineProperty 方法,重新定义 obj[key]getset
      • 4.4.1 get 时:dep.depend(), childObj.dep.denpend(),这就是引用到 obj.keyobj.key.childKey 时,都能被监听到的原因
      • 4.4.2 set 时,重新 observe 新值,使得新值也是响应式的,最后 dep.notify() 即派发更新