vue源码解读--Array数据响应式(变化侦测)

115 阅读7分钟

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

参考自Vue源码系列-Vue中文社区

Array的变化侦测

前言

Object 数据可以直接使用对象原型上的方法 Object.defineProperty,但是 Array 无法使用这个方法,所以 Array 数据有另一套变化侦测机制。

基本的思路还是一致的,获取数据时收集依赖,数据变化时更新依赖。

使Array型数据可观测

其实 Array 数据和 Object 数据的依赖收集方式是相同,也是在 getter 函数中。

data(){
    return {
        arr:[1,2,3];
    }
}

想想看,data 返回的是一个 Object,如果要使用 arr 这个数据,就要从 Object 中获取 arr 数据,然后就会触发 arr 的 getter,所以,Array型数据还是在getter中收集依赖

现在知道了 Array 型数据在什么时候被读取了,但是什么时候被修改还不能知道,Object 的变化是通过 setter 函数,但是 Array 没有 setter函数。

Array型数据发生变化,肯定就是操作了 Array,而操作数组的方法就那几种,只需要在执行数组方法的时候通知变化就可以了,所以可以重写数组方法:

let arr = [1,2,3];
arr.push(4);
Array.prototype.newPush = function(val){
    console.log("arr被修改了");
    this.push(val);
}
arr.newPush(4);

在数组原型上重新定义了一个 newPush 方法,这个方法在内部调用了原生的 push 方法,保证了新的 newPush 方法和原生 push 方法具有一样的功能,在新定义的方法中,就可以想做的事了。

我们在使用数组方法时,Vue 中有一个数组方法拦截器,在拦截器重写了数组的一些方法,当使用操作数组方法时,实际使用的是拦截器中的方法。

// 源码位置:/src/core/observer/array.jsconst arrayProto = Array.prototype
// 创建一个对象作为拦截器
export const arrayMethods = Object.create(arrayProto)
​
// 改变数组自身内容的7个方法
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]
​
methodsToPatch.forEach(function (method) {
  const original = arrayProto[method]      // 缓存原生方法
  Object.defineProperty(arrayMethods, method, {
    enumerable: false,
    configurable: true,
    writable: true,
    value:function mutator(...args){
      const result = original.apply(this, args)
      return result
    }
  })
})

首先创建继承自 Array 原型的空对象 arrayMethods,接着在 Object.defineProperty 方法中把数组自身的7个原生方法封装进 arrayMethods 中,最后,使用数组方法的时候,执行的实际上是 mutator,在内部中调用了数组的原生方法。现在就可以在 mutator 函数中添加自己变化通知代码了。

拦截器是实现了,但是怎么在使用数组方法的时候调用拦截器中的方法呢?这个时候就需要将拦截器挂载到数组实例与 Array.prototype 之间,这样拦截器才能生效。只需要把数据的 __proto__ 属性设置为拦截器 arrayMethods 即可。

// 源码位置:/src/core/observer/index.js
export class Observer {
  constructor (value) {
    this.value = value
    if (Array.isArray(value)) {
      const augment = hasProto
        ? protoAugment
        : copyAugment
      augment(value, arrayMethods, arrayKeys)
    } else {
      this.walk(value)
    }
  }
}
// 能力检测:判断__proto__是否可用,因为有的浏览器不支持该属性
export const hasProto = '__proto__' in {}
​
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
​
function protoAugment (target, src, keys) {
  target.__proto__ = src
}
​
function copyAugment (target, src, keys) {
  for (let i = 0, l = keys.length; i < l; i++) {
    const key = keys[i]
    def(target, key, src[key])
  }
}

首先判断浏览器是否支持 __proto__,如果支持,则调用 protoAugment 函数把 value.__proto__ = arrayMethods;如果不支持,则调用 copyAugment 函数把拦截器中的方法循环加入到 value上。

依赖收集

在上面的讲解已经把 Array型数据变得可观测,所以只需要在 getter中收集依赖,数据变化后,在拦截器方法中通知变化。

实例化依赖管理器:

// 源码位置:/src/core/observer/index.js
export class Observer {
  constructor (value) {
    this.value = value
    this.dep = new Dep()    // 实例化一个依赖管理器,用来收集数组依赖
    if (Array.isArray(value)) {
      const augment = hasProto
        ? protoAugment
        : copyAugment
      augment(value, arrayMethods, arrayKeys)
    } else {
      this.walk(value)
    }
  }
}

依赖管理器定义在 Observer 类中,而我们需要在 getter 中收集依赖,所以必须要在 getter 中能够访问到 Observer类中的依赖管理器,才能把依赖存进去。

function defineReactive (obj,key,val) {
  let childOb = observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get(){
      if (childOb) {
        childOb.dep.depend()
      }
      return val;
    },
    set(newVal){
      if(val === newVal){
        return
      }
      val = newVal;
      dep.notify()   // 在setter中通知依赖更新
    }
  })
}
​
/**
 * 尝试为value创建一个0bserver实例,如果创建成功,直接返回新创建的Observer实例。
 * 如果 Value 已经存在一个Observer实例,则直接返回它
 */
export function observe (value, asRootData){
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else {
    ob = new Observer(value)
  }
  return ob
}

首先通过 observer 函数给被获取的数据尝试创建一个 Observer实例,判断当前传入的数据上是否有 __ob__ 属性,如果有,表示它已经被转化成响应式的了,如果没有,就调用 new Observer(value)将其转化成响应式的,并把数据对应的 Observer实例返回。

defineReactive 函数中,获取数据对应的 Observer 实例 childOb,然后在 getter 中调用 Observer实例上的依赖管理器,将依赖收集起来。

通知依赖数据发生变化只需要在数组方法拦截器中调用依赖管理器的 dep.notify() 方法,因为 value 上的 __ob__就是对应的 Observer 实例,通过实例就能访问到依赖管理器,

methodsToPatch.forEach(function (method) {
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator (...args) {
    const result = original.apply(this, args)
    const ob = this.__ob__
    // notify change
    ob.dep.notify()
    return result
  })
})

拦截器是挂载到数组原型上的,所以拦截器中的this就是数据value,调用 valueObserver类实例上面依赖管理器的dep.notify()方法,以达到通知依赖的目的。

深度侦测

如果数组中包含了一个对象,对象的属性发生了变化也应该被侦测到,这就是深度侦测。在Object中使用递归实现了所有子属性的变化侦测,但是 Array到现在为止还只是对自身变化的侦测。

export class Observer {
  value: any;
  dep: Dep;
​
  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      const augment = hasProto
        ? protoAugment
        : copyAugment
      augment(value, arrayMethods, arrayKeys)
      this.observeArray(value)   // 将数组中的所有元素都转化为可被侦测的响应式
    } else {
      this.walk(value)
    }
  }
​
  observeArray (items) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}
​
export function observe (value, asRootData){
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else {
    ob = new Observer(value)
  }
  return ob
}

对 Array型数据,调用 observeArray方法,遍历数组中的每一个元素,然后调用 observer函数,把每个元素都变成响应式数据。

数组新增元素的侦测

对于数组已有的元素已经全部转化成响应式数据了,但是新增的元素也应该变成响应式数据。向数组新增元素的方法有3个:pushunshiftsplice。只需在这3个方法中将新增的元素转化成响应式数据即可。

methodsToPatch.forEach(function (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   // 如果是push或unshift方法,那么传入参数就是新增的元素
        break
      case 'splice':
        inserted = args.slice(2) // 如果是splice方法,那么传入参数列表中下标为2的就是新增的元素
        break
    }
    if (inserted) ob.observeArray(inserted) // 调用observe函数将新增的元素转化成响应式
    // notify change
    ob.dep.notify()
    return result
  })
})

如果是 pushunshift 方法,传入的参数就是新增的元素,如果是 splice 方法,参数列表中下标为2的是新增的元素,调用 observer 函数就可以将新增元素转化成响应式的了。

不足之处

在日常开发中,我们经常使用下标来操作数组,比如:

let arr = [1,2,3];
arr[0] = 5;
arr.length = 0

像这样修改数组是无法被侦测的,Vue增加了两个全局 API 解决了这个问题

  • vue.set
  • vue.delete

小结

  1. 分析出Array型数据被获取是也会调用 getter ,所以也是通过 getter 收集依赖
  2. 数组被修改会使用数组方法,所以创建数组方法拦截器,在拦截器中重写数组方法,从而把数组变得可观测。
  3. 在拦截器中通知依赖数据发生变化,数组中的每一个元素都会通过 observer 转化成响应式数据,新增的元素也会调用 observer