Vue源码从什么玩意儿到真香(2)-Array的变化侦测

在上一篇中,主要讲了Object类型的变化侦测,那么为何要讲Array类型呢?
举个栗子:
this.array.push('ntyang')
请各位思考一下,Object类型的变化侦测是经过Object.defineProperty方法的getter/setter实现的,可是使用Array.prototype.push来改变数组,并不会触发getter/setter.那么Array类型数据是如何实现变化侦测的?
接下来让我给您说道说道数组

1.如何知道变化了呢?

上面的push方法,是原型链上的方法,既然没有为数组提供相似Object.defineProperty的方法,那么咱们换个角度想想,是否是能够从数组的原型链方法做为切入点.
用一个拦截器覆盖Array.prototype,以后当咱们每次使用数组原型方法时,实际上执行的都是拦截器中的方法,再在拦截器中使用原生的Array.prototype方法.这样,咱们就能够在拦截器中监听到Array的变化了.
Array原型中能够改变自身的方法有七个:push,pop,shift,unshift,splice,sort,reverse
想到这里,那就实现一下:浏览器

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

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

methodsToPatch.forEach(method => {
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator () {
    let args = [], len = arguments.length;
    while ( len-- ) args[ len ] = arguments[ len ];
    return original.apply(this, args)
  })
})


function def (obj, key, val, enumerable) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable: !!enumerable,
    writable: true,
    configurable: true
  });
}

咱们用变量arrayPrototype保存Array.prototype,具有其全部功能.接着在arrayMethods上使用Object.defineProperty对那七个方法进行封装.若是咱们使用一个骚操做,用arrayMethods覆盖掉Array.prototype,那么,当咱们使用如push等方法时,其实调用的是arrayMethods.push,也就是mutator函数.而mutator函数最终会执行数组的原生方法.所以咱们就能够在mutator函数中搞一些事情,好比发送通知.
那么如何覆盖掉Array.prototype呢?app

2. 覆盖Array.prototype

想让拦截器生效就必需要覆盖Array.prototype.可是若是直接覆盖的话,就污染了全局Array.我只是想拦截那些响应式的数据啊.
对了,是否是想起上一章的Obverser了?函数

function Observer(value) {
  this.value = value
  if(!Array.isArray(value)){
    this.walk(value)
  } else {
    value.__proto__ = arrayMethods //新增
  }
}

若是当前valueArray类型,直接赋值给它的隐式原型__proto__
可是呢,鉴于目前浏览器对ES6的支持,咱们须要处理不能使用__proto__的状况.
Vue的处理也是简单直接,既然不能操做你的原型,那就直接操做你吧,毕竟只要自身存在对应的方法就不会再去原型上寻找,即把arrayMethods中的方法设置到数组上.
修改一下Observe:工具

const hasProto = '__proto__' in {}
const arrayKeys = Object.getOwnPropertyNames(arrayMethods);

function Observer(value) {
  this.value = value
  if(!Array.isArray(value)){
    this.walk(value)
  } else { //新增
    if (hasProto) {
      protoAugment(value, arrayMethods)
    } else {
      copyAugment(value,arrayMethods, arrayKeys)
    }
  }
}

function protoAugment (target, src) {
  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])
  }
}

hasProto来判断浏览器是否支持__proto__,若支持,调用protoAugment,直接将arrayMethods添加到要监测的数组原型上;若不支持,则调用copyAugment,将arrayMethods设置到数组上.
通过这样一番操做,无论您是啥浏览器,都能被安排的明明白白.开发工具

3. 依赖怎么收集呢?又是怎么通知呢?

如今,拦截器有了,只能作到当数组的内容发生变化时去通知.可是通知谁呢?
参考Object的,在getter中收集依赖(watcher),在setter中通知依赖.
其实Array的依赖收集也是在getter中.
这下你可能就有点懵了,别方,让咱们捋一下.
试想一下你在data中定义了一个Array:this

myArray:[1,3,5,7]

在组件中,你会如何使用它呢?确定会是this.myArray的方式吧,这样确定会触发名字叫作myArray属性的getter方法,因此,关注一下重点:spa

function defineReactive(data, key, val) {
  if(typeof val === 'object'){
    new Observer(val)
  }
  const dep = new Dep()
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get: function () {
      if (Dep.target) {
        dep.depend()
        //在这里收集Array的依赖
      }
      return val
    },
    set: function (newVal) {
      if (val === newVal) return
      val = newVal
      dep.notify()
    }
  })
}

Array在getter中收集依赖,在拦截器中触发依赖
接下来就要思考,数组的依赖要保存到哪里?
Vue把数组的依赖保存到Observer中了,由于getter和拦截器中都能访问的到Observer的实例,下面会详细解释prototype

function Observer(value) {
  this.value = value
  this.dep = new Dep() //新增
  if(!Array.isArray(value)){
    this.walk(value)
  } else {
    if (hasProto) {
      protoAugment(value, arrayMethods)
    } else {
      copyAugment(value,arrayMethods, arrayKeys)
    }
  }
}

首先收集依赖,那就要改造一下defineReactive方法:code

function defineReactive(data, key, val) {
  if(typeof val === 'object'){
    new Observer(val)
  }
  const dep = new Dep()
  const childOb = observe(val)  //新增
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get: function () {
      if (Dep.target) {
        dep.depend()
        if(childOb){ //新增
          childOb.dep.depend()
        }
      }
      return val
    },
    set: function (newVal) {
      if (val === newVal) return
      val = newVal
      dep.notify()
    }
  })
}

function observe(value, asRootData){
  if(!isObject(value)){
    return
  }
  let ob
  if(hasOwn(value, '__ob__') && value.__ob__ instanceof Observer){
    ob = value.__ob__
  } else {
    ob = new Observer(value)
  }
  return ob
}

function isObject (obj) {
  return obj !== null && typeof obj === 'object'
}

const hasOwnProperty = Object.prototype.hasOwnProperty;
function hasOwn (obj, key) {
  return hasOwnProperty.call(obj, key)
}

新增一个observe函数,尝试建立或返回一个Observer实例.若是value已是响应式数据,直接返回Observer实例,避免重复侦测
defineReactive函数中,调用observe(val),获得当前val对应的Observer实例childOb,最后经过childOb.dep执行depend方法收集依赖.
可能你仍是不太明白为何要把依赖收集到Observer中,继续往下看
如今,咱们已经实现了在getter中把数组的依赖收集到Observer实例中了,那么如何获取Observer实例呢?
再改造一个Observer:

function Observer(value) {
  this.value = value
  this.dep = new Dep() 
  def(value, '__ob__', this) //新增
  if(!Array.isArray(value)){
    this.walk(value)
  } else {
    if (hasProto) {
      protoAugment(value, arrayMethods)
    } else {
      copyAugment(value,arrayMethods, arrayKeys)
    }
  }
}

给value设置一个__ob__属性,值是Observer的实例
这样,就能够经过数组的__ob__属性获得Observer实例了,而后就能拿到Dep实例了.
__ob__的做用不仅这些,好比它能够标识当前数据是否是响应式的,有心的你必定会在开发工具中看到响应式的数据都有一个__ob__属性,好比这样:
image.png
再让咱们回到拦截器方法,既然能够拿到Observer实例,那就要用起来:

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

methodsToPatch.forEach(method => {
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator () {
    let args = [], len = arguments.length;
    while ( len-- ) args[ len ] = arguments[ len ];
    const result = original.apply(this, args)
    const ob = this.__ob__ //新增
    ob.dep.notify() //新增
    return result
  })
})

不论是覆盖value的原型链仍是直接将方法添加到value上,均可以经过this.__ob__获取到Observer实例,而后就能够愉快的ob.dep.notify()

4.侦测数组内各个元素的变化

以上说的都是侦测数组自身的变化,接下来就要对数组内的每个元素进行侦测
改造一下Observer,当value是数组是,要递归每个元素:

function Observer(value) {
  this.value = value
  this.dep = new Dep()
  def(value, '__ob__', this)
  if(!Array.isArray(value)){
    this.walk(value)
  } else {
    if (hasProto) {
      protoAugment(value, arrayMethods)
    } else {
      copyAugment(value,arrayMethods, arrayKeys)
    }
    this.observeArray(value) //新增
  }
}

Observer.prototype.observeArray = function (items) {
  for (let i = 0, l = items.length; i < l; i++) {
    observe(items[i]);
  }
};

新增observeArray方法,对value的每一项都执行observe函数,observe函数内会new Observer(),以后再进入value是Object/Array的判断,递归全部层次.
这样,只要将Array类型的数据丢给Observer,就会把数组的全部子元素都转换成响应式.
好像还忽略了一点东西,对的,就是经过push,unshift和splice添加的数据也须要转换成响应式的.再修改一下拦截器方法:

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

methodsToPatch.forEach(method => {
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator () {
    let args = [], len = arguments.length;
    while ( len-- ) args[ len ] = arguments[ len ];
    const result = original.apply(this, args)
    const ob = this.__ob__ 
    
    let instered  //新增
    switch (method) {
      case 'push':
      case 'unshift':
        instered = args
        break
      case 'splice':
        instered = args.slice[2]
        break
    }
    
    if (instered) ob.observeArray(instered)  //新增
    
    ob.dep.notify() 
    return result
  })
})

对method进行switch判断,把方法中的参数保存到inserted中,若是inserted有值,就用ob.observeArray来侦测一下

5.关于Array的问题

因为是经过拦截器的方式实现的数组类型数据的变化侦测,那么就表示必定会有一些操做是Vue拦截不到的,好比:

this.myArray[0] = 123
this.myArray.length = 0

以上这两种操做,Vue彻底侦测不到,也就没法触发watch或re-render操做,页面也就不会有更新,请看官注意一下,除非您想故意这样...

相关文章
相关标签/搜索