vue2源码学习 (6).响应式原理-4.数组的处理方式

155 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第 10 天,点击查看活动详情

6.响应式原理-4.数组的处理方式

start

  • 为什么需要单独处理数组?这篇文章来阅读一下对数组类型的数据处理

Observer

// 7. 如果是数组
if (Array.isArray(value)) {
  // 7.1 可以使用对象的 __proto__ 属性
  if (hasProto) {
    protoAugment(value, arrayMethods)
  } else {
    // 7.2 不可以使用对象的 __proto__ 属性
    copyAugment(value, arrayMethods, arrayKeys)
  }
  // 7.3执行 observeArray
  this.observeArray(value)
}

// hasProto
export const hasProto = '__proto__' in {}
  1. 判断是否是数组;
  2. 判断是否可以使用 对象的 __proto__ 属性;
  3. 根据判断执行 protoAugment 或者 copyAugment;
  4. 最后执行 observeArray

protoAugment && copyAugment

/**
 * Augment a target Object or Array by intercepting
 * the prototype chain using __proto__
 */
/**
 * 1.
 * 通过拦截来增强目标对象或数组
 * 使用原型链的 __proto__
 */

// 这里的函数名可以翻译为 原始增加
function protoAugment(target, src: Object) {
  /* eslint-disable no-proto */

  // 2. 这里做的操作就是,把数组的原型指向了我们定义的新对象`arrayMethod` 。新对象的原型是数组正式的原型。
  target.__proto__ = src
  /* eslint-enable no-proto */
}

/**
 * Augment a target Object or Array by defining
 * hidden properties.
 */
/**
 * 3.
 * 通过定义来扩大目标对象或数组
 * 隐藏属性
 */

/* istanbul ignore next */

// 这里的函数名可以翻译为 拷贝增加
function copyAugment(target: Object, src: Object, keys: Array<string>) {
  // 4. 遍历我们定义的 7 种方法;
  for (let i = 0, l = keys.length; i < l; i++) {
    // 4.1 拿到 方法名
    const key = keys[i]
    // 4.2 给目标数组添加同名方法,
    def(target, key, src[key])
  }
}
  1. protoAugment:将数组的隐式原型对象指向srcsrc就是我们定义的新对象arrayMethod
  2. copyAugment:在需要处理的数组上添加我们定义的 7 种方法; 两个方法本身不难,主要需要做的弄懂传入的参数是什么。

看一下 arrayMethods, arrayKeys 这两个传入的参数是什么。

\src\core\observer\index.js

import { arrayMethods } from './array'
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)

\src\core\observer\array.js

/*
 * not type checking this file because flow doesn't play well with
 * dynamically accessing methods on Array prototype
 * 没有类型检查该文件,因为 flow 不能很好地发挥作用
 * 动态访问数组原型的方法
 */

import { def } from '../util/index'

// 1. 数组的原型
const arrayProto = Array.prototype

// 2. 创建一个对象,原型指向数组的原型  `arrayMethods.__proto__ === Array.prototype` true
export const arrayMethods = Object.create(arrayProto)

// 3. 定义需要处理的数组的方法,这里可以看到改写了 7 种方法;
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse',
]

/**
 * Intercept mutating methods and emit events
 */
// 4. 拦截变化的方法并发出事件;
// 遍历我们需要拦截的方法
methodsToPatch.forEach(function (method) {
  // cache original method
  // 5. 缓存 原本数组身上的方法,用来后续调用
  const original = arrayProto[method]

  // 6. 在 arrayMethods上;  定义push,pop,shift,unshift,splice,sort,reverse方法;
  def(arrayMethods, method, function mutator(...args) {
    // 7. 先触发 数组原本对应方法
    const result = original.apply(this, args)

    // 8. 获取到 数据实例上的 Observer实例。
    const ob = this.__ob__
    let inserted // inserted : 插入项

    // 9. 选择方法
    switch (method) {
      case 'push':
      case 'unshift':
        // 9.1 push  unshift 传入的参数都是需要存入数组的参数,所以直接 =
        inserted = args
        break
      case 'splice':
        // 9.2 splice 参数依次为 ①从何处处理 ②处理多少 ③要添加到数组的新元素 ,所以这里取第二个参数以后的参数。
        inserted = args.slice(2)
        break
    }

    // 10. 有新添加来的数据,需要处理成响应式的
    if (inserted) ob.observeArray(inserted)

    // notify change
    // 11. 通知更改
    // 这个地方着重注意一下,我们自身实现数组的 7 种方法,使用它们的时候,也会触发视图更新,根本原因,就是因为这里`ob.dep.notify();`
    ob.dep.notify()

    // 12. result存储的是什么? 存储的是数组本身对应的方法
    return result
  })

  /* 
  所以最终返回的arrayMethod如下:
    {
      pop: ƒ mutator(...args)
      push: ƒ mutator(...args)
      reverse: ƒ mutator(...args)
      shift: ƒ mutator(...args)
      sort: ƒ mutator(...args)
      splice: ƒ mutator(...args)
      unshift: ƒ mutator(...args)
    }
  */
})

整体代码看下来,就返回了一个对象,arrayMethods,结构如注释所示。 该对象有这么几个特点:

  1. 7 个数组同名方法名,作为 key 值;
  2. arrayMethods隐式原型指向数组的显式原型,arrayMethods.__proto__ === Array.prototype;
  3. 本质上是,拿到数组原本的方法,在数组原本方法的基础外,包一层,处理新添加进来的数据(ob.observeArray(inserted)); 通知更新ob.dep.notify()

需要注意的是 ,这里的通知更新借助了数据上的__ob__属性来访问 dep 从而通知更新。这就是__ob__存储了Observer实例的作用之一。

上述代码,就是对数组方法的处理,后续还有数组每一项的处理observeArray

observeArray

/**
 * Observe a list of Array items.
 */
// 11.观察Array项
observeArray(items: Array<any>) {
  // 12. 遍历数组的每一项,全部都observe一下。
  for (let i = 0, l = items.length; i < l; i++) {
    observe(items[i]);
  }
}

这里主要是遍历数组,给每一项进行 observe()

提一个问题,上面使用的 ob.dep 是什么时候初始化的?

/**
 * Define a reactive property on an Object.
 */
// 1. 在对象上定义响应式属性
export function defineReactive(
  obj: Object, // 传入的 对象:
  key: string, // 对象的 属性;
  val: any, //对象的属性值,在没有 getset的时候,直接返回对应的值。
  customSetter?: ?Function, // 自定义 setter
  shallow?: boolean // 是否是 浅层的响应式
) {
  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    get: function reactiveGetter() {
      if (Dep.target) {
        dep.depend()

        // 1. 子对象的 Observer实例,存在,子对象也收集依赖 (递归下去,就可以导致所有的属性都收集了依赖)
        if (childOb) {
          childOb.dep.depend()

          // 2. 数组处理,数组的每一项并不会(observe), 所以如果是数组,手动变量在dep中收集依赖
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
}


function dependArray(value) {
  for (let e, i = 0, l = value.length; i < l; i++) {
    e = value[i];
    // e存在; e.__ob__存在; e.__ob__.dep.depend()收集依赖;
    e && e.__ob__ && e.__ob__.dep.depend();
    if (Array.isArray(e)) {
      dependArray(e);
    }
  }
}

有关 dep 后续我会专门写一篇文章研究研究。

思考

  1. 数组的响应式处理方式?
  • observe 数组每一项的值;
  • 重写数组自带的 7 种方法:'push','pop','shift','unshift','splice','sort','reverse',, 调用时: 1. 利用observeArray处理新增的项,2.利用 __ob__.dep.notify来通知更新;
  1. 为什么要给我们的数据绑定一个__ob__属性?

__ob__上存储的是 Observer实例; Observer实例上又存储着依赖的 dep 绑定了__ob__属性,方便我们在代码中手动触发__ob__.dep.notify来通知更新。(例如:处理数组的方法,就有具体的使用案例) 我们熟悉的 $set 方法其实也会借助__ob__.dep.notify,来通知更新。

  1. 其他注意事项
  1. 这里可以看到 Vue.js 源码对 对象的__proto__属性做了兼容处理。
  2. 这里可以学习到如何重写数组原型上的方法。1.修改隐式原型; 2.直接暴力塞入对应方法; 以后如果有类似的需求可以模仿。
  3. Vue.js 只重写了 'push','pop','shift','unshift','splice','sort','reverse', 这七种方法。 因为 Vue.js 重写了这些方法,所以是通过这几个方法新增了数据,新增的数据也是响应式的,放心使用。
  4. 见到了__ob__ 存储 Observer实例 的使用场景(之一)