vue2响应式原理(5)-- 监测数组 Array

186 阅读3分钟

在通过 observe 方法去观察对象的时候会实例化 Observer,在它的构造函数中是专门对数组做了处理

覆盖原型或原型上某些方法

src/core/observer/index.ts

export const hasProto = '__proto__' in {}

const arrayKeys = Object.getOwnPropertyNames(arrayMethods) 
// Object.getOwnPropertyNames 方法返回一个由指定对象的所有自身属性的属性名组成的数组

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 {
     // ...
  }
  
  observeArray(value: any[]) {
    for (let i = 0, l = value.length; i < l; i++) {
      // 遍历数组,为数组的每一项设置观察,处理数组元素为对象的情况
      observe(value[i], false, this.mock)
    }
  }
  
}

从上面代码可以看出,如果value是数组:

1.如果是浏览器支持_proto_属性,那就用arrayMethod覆盖Array实例value的原型,而不是覆盖所有数组的原型;

2.如果不支持_proto_实例,那么就遍历数组,def(value, key, arrayMethods[key]),那就对数组的某些方法做覆盖;

3.一般默认为是深度响应式,调用this.observeArray(value),对数组里面的值做响应式处理

下面看一下arrayMethods这个对象里面做了什么,:

改写Array原型的7个方法

在改写的方法中手动触发依赖更新

src/core/observer/array.ts

const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)

const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

methodsToPatch.forEach(function (method) {
  // cache original method
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator(...args) {
    const result = original.apply(this, args)
    const ob = this.__ob__
    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    if (inserted) ob.observeArray(inserted)
    // notify change
    if (__DEV__) {
     
    } else {
      ob.dep.notify()
    }
    return result
  })
})

export function def(obj: Object, key: string, val: any, enumerable?: boolean) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable: !!enumerable,
    writable: true,
    configurable: true
  })
}

  1. 克隆一份Array原型,const arrayProto = Array.prototype,防止改掉所有数组,创建一个以Array.prototype为原型的对象arrayMethodsconst arrayMethods = Object.create(arrayProto)

  2. 需要覆盖7个方法, push,pop,shift,unshift,splice, sort, reverse,这7个方法改变了原数组;

  3. 遍历这7个方法, def(arrayMethods, method, function mutator(...args) {}),将每个方法改写值定义到arrayMethods对象上,用户使用push方法,实际上使用的是arrayMethods.push方法,而arrayMethods.push实际上是函数mutator,接下来看一下mutator函数的逻辑:

  4. 首先每次从原型中拿出原始的方法 original = arrayProto[method],执行原始的方法,保持数组原始的行为,const result = original.apply(this, args)push,unshift,splice都是可能会向数组插入值的,用inserted变量来标识插入的值,调用ob.observeArray(inserted),看看插入的是不是对象,如果是对象,就将其转变为响应式;

  5. 最后调用ob.dep.notify()手动通知更新依赖,只要用户调用这7个方法,vue内部才会走到这一步,触发依赖更新

那么数组的依赖是怎么收集的呢?往下看:

数组收集依赖

src/core/observer/index.ts

export function defineReactive(
  obj: object,
  key: string,
  val?: any,
  customSetter?: Function | null,
  shallow?: boolean,
  mock?: boolean
) {
  const dep = new Dep()
  const property = Object.getOwnPropertyDescriptor(obj, key)
  const getter = property && property.get
  let childOb = !shallow && observe(val, false, mock)
  
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      const value = getter ? getter.call(obj) : val 
      if (Dep.target) {
      if (__DEV__) {
      
        } else {
          dep.depend() 
        }
        if (childOb) {
          childOb.dep.depend()
          if (isArray(value)) { 
            dependArray(value)
          }
        }
      }
      return isRef(value) && !shallow ? value.value : value
    },
   //...
  })
}

function dependArray(value: Array<any>) {
  for (let e, i = 0, l = value.length; i < l; i++) {
    e = value[i]
    if (e && e.__ob__) {
      e.__ob__.dep.depend()
    }
    if (isArray(e)) {
      dependArray(e)
    }
  }
}
// 举个数组例子
{
   list1:[1,2,3,4,5],
   list2:[{name:"Cristiano"}]
}
  1. 当读取list1或者list2这个key的时候,就会触发它的getter,收集对应的依赖

  2. 调用dependArray(value),遍历数组,取出数组的每一项,如果是对象,被监测过,有_ob_属性,就收集这个对象的依赖,通过上面的observeArray方法知道,数组里面的对象会被监测,转换为响应式,递归执行

数组的vm.$set方法

export function set(
  target: any[] | Record<string, any>,
  key: any,
  val: any
): any {
  const ob = (target as any).__ob__
 
  //如果是数组并且key是个有效的索引值
  if (isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key)
    //splice 数组拦截器会监听到变化
    target.splice(key, 1, val)
    // when mocking for SSR, array methods are not hijacked
    if (ob && !ob.shallow && ob.mock) {
      observe(val, false, true)
    }
    return val
  }

}

可以看到,对于数组,对传递的索引key做了处理,set内部也是调用改写的7个方法中的splice方法,实际上使用的是arrayMethods.splice方法,会调用ob.dep.notify触发依赖更新, 如果数组是响应式的,那么肯定有_ob_属性,如果往数组中添加对象,在 observe函数中,这个对象也会被转换为响应式

数组的vn.$delete

export function del(target: any[] | object, key: any) {
 
  if (isArray(target) && isValidArrayIndex(key)) {
    target.splice(key, 1)
    return
  }
 
}

删除方法直接调用splice删除,实际上使用的是arrayMethods.splice方法,会调用ob.dep.notify触发依赖更新

所以我们平时要改变数组,可以直接用splice方法就好了