Vue原理解析(七):全面深刻理解响应式原理(下)-数组进阶篇

上一篇:Vue原理解析(六):全面深刻理解响应式原理(上)-对象基础篇html

再初步了解了响应式的原理后,接下来咱们深刻响应式,解析数组响应式的原理。vue

数组更新

首先来看下改变数组的两种方式:面试

export default {
  data() {
    list: [1, 2, 3]
  },
  methods: {
    changeArr1() {  // 从新赋值
      this.list = [4, 5, 6]
    },
    changeArr2() {  // 方法改变
      this.list.push(7)
    }
  }
}
复制代码

对于这两种改变数据的方式,vue内部的实现并不相同。算法

方式一:从新赋值

  • 实现原理和对象是同样的,再vm._render()时有用到list,就将依赖收集起来,从新赋值后走对象派发更新的那一套。

方式二:方法改变

  • 走对象的那一套就不行了,由于并非从新赋值,虽然改变了数组自身但并不会触发set,原有的响应式系统根本感知不到,因此咱们接下来就分析,vue是如何解决使用数组方法改变自身触发视图的。

Dep收集依赖的位置

上一篇它的声音并不大,如今咱们来从新认识它。Dep类的主要做用就是管理依赖,在响应式系统中会有两个地方要实例化它,固然它们都会进行依赖的收集,首先是以前具体包装的时候:数组

function defineReactive(obj, key, val) {
  const dep = new Dep()  // 自动依赖管理器
  ...
  Object.defineProperty(obj, key, {
    get() {...},
    set() {...}
  })
}
复制代码

这里它会对每一个读取到的key都进行依赖收集,不管是对象/数组/原始类型,若是是经过从新赋值触发set就会使用这里收集到的依赖进行更新,笔者这里就把它命名为自动依赖管理器,方便和以后的区分。浏览器

还有一个地方也会对它进行实例化就是Observer类中:bash

class Observer {
  constructor(value) {
    this.dep = new Dep() //  手动依赖管理器
    ...
  }
}
复制代码

这个依赖管理器并不能经过set触发,并且是只会收集对象/数组的依赖。也就是说对象的依赖会被收集两次,一次在自动依赖管理器内,一次在这里,为何要收集两次,本章以后说明。而最重要的是数组使用方法改变自身去触发更新的依赖就是再这收集的,这个前提仍是颇有必要交代下的。app

数组的响应式原理

数组响应式数据的建立

数组示例:
export default {
  data() {
    return {
      list: [{
        name: 'cc',
        sex: 'man'
      }, {
        name: 'ww',
        sex: 'woman'
      }]
    }
  }
}
复制代码

流程开始仍是执行observe方法,接下来咱们更加详细分析响应式系统:post

function observe(value) {
  if (!isObject(value) { //不是数组或对象,再见
    return
  }
  
  let ob
  if(hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {  // 避免重复包装
    ob = value.__ob__
  } else {
    ob = new Observer(value)
  }
  return ob
}
复制代码

只要是响应式的数据都会有一个__ob__的属性,它是在Observer类中挂载的,若是已经有__ob__属性就直接赋值给ob,不会再次去建立Observer实例,避免重复包装。首次确定没__ob__属性了,因此再从新看下Observer类的定义:ui

class Observer {
  constructor(value) {
    this.value = value
    this.dep = new Dep()  // 手动依赖管理器
    
    def(value, '__ob__', this)  // 挂载__ob__属性,三个参数
    ...
  }
}
复制代码

首先定义一个手动依赖管理器,而后挂载一个不可枚举的__ob__属性到传入的value下,表示它的一个响应式的数据,并且__ob__的值就是当前Observer类的实例,它拥有实例上的全部属性和方法,这很重要,咱们接下来看下def是如何完成属性挂载的:

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

其实就是一个简单的封装,若是第四个参数不传,enumerable项就是不可枚举的了。接着看Observer类的定义:

class Observer {
  constructor(value) {
	...
    if (Array.isArray(value)) {  // 数组
      ...
    } else {  // 对象
      this.walk(value)  // {list: [{...}, {...}]}
    }
  }
  
  walk (obj) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }
}
复制代码

首次传入仍是对象的格式,因此会执行walk遍历的将对象每一个属性包装为响应式的,再来看下defineReactive方法:

function defineReactive(obj, key, val) { 

  const dep = new Dep()  // 自动依赖管理器
  
  val = obj[key]  // val为数组 [{...}, {...}]
  
  let childOb = observe(val)  // 传入到observe里,返回Observer类实例
  
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get() {  // 依赖收集
      if (Dep.target) {
        dep.depend()  // 自动依赖管理器收集依赖
        if (childOb) {  // 只有对象或数组才有返回值
          childOb.dep.depend()  // 手动依赖管理器收集依赖
          if (Array.isArray(val)) { 若是是数组
            dependArray(val) // 将数组每一项包装为响应式
          }
        }
      }
      return value
    },
    set(newVal) {
      ...
    }
  }
}
复制代码

首先递归执行observe(val)会有一个返回值了,若是是对象或数组的话,childOb就是Observer类的实例,以数组格式在observe内作了什么,咱们以后分析。接下来在get内的childOb.dep.depend()执行的就是Observer类里定义的dep进行依赖收集,收集的render-watcher跟自动依赖管理器是同样的。接下来若是是数组就执行dependArray方法:

function dependArray (value) {
  for (let e, i = 0, i < value.length; i++) {
    e = value[i]
    e && e.__ob__ && e.__ob__.dep.depend()  // 是响应式数据
    if (Array.isArray(e)) {  // 若是是嵌套数组
      dependArray(e)  // 递归调用本身
    }
  }
}
复制代码

这个方法的做用就是递归的为每一项收集依赖,这里每一项都必需要有__ob__属性,而后执行Observer类里的dep手动依赖收集器进行依赖收集。咱们如今知道数组的依赖是放在Observer类里的dep属性内,如今来看下怎么去更新这个收集到的依赖。

数组方法更新依赖

在以前defineReactive方法里有这么一句,let childOb = observe(val),经过求值,val如今就是具体的数组,以数组的形式传入到observe方法内,咱们来看下在Observer类中作什么:

class Observer {
  constructor(value) {
    if (Array.isArray(value)) {  // 数组
      
      const augment = hasProto ? protoAugment : copyAugment  // 第一句
      
      augment(value, arrayMethods, arrayKeys)  // 第二句
      
      this.observeArray(value)  // 第三句
      
    }
  }
}
复制代码

主要就是执行了三句逻辑,因此咱们首先来看下分别作了什么。

数组方法改变自身触发视图原理:首先覆盖数组的__proto__隐式原型,借用数组原生的方法,定义vue内部自定义的数组异变方法拦截原生方法,再调用异变方法改变自身以后手动触发依赖。

有了这只指向月亮的手,咱们如今就一块儿去往心中的月亮。首先分析第一句:

const augment = hasProto ? protoAugment : copyAugment

--------------------------------------------------------

const hasProto = '__proto__' in {}

function protoAugment (target, src) {  // src为拦截器
  target.__proto__ = src
}

function copyAugment (target, src, keys) {  // src为拦截器
  for (let i = 0; i < keys.length; i++) {
    const key = keys[i]
    def(target, key, src[key])
  }
}
复制代码

__proto__这个属性并非全部浏览器都有的,笔者以前也一直觉得这是一个通用属性,原来IE11才开始有这个属性,经过'__protp__' in {}也能够快速判断当前浏览浏览器是否IE10以上?确实用过,好用!

是否有__proto__属性处理方法也不相同,若是有的的话,直接在protoAugment方法内使用拦截器覆盖;若是没有__proto__属性,那就在当前调用数组下挂载拦截器里的异变数组方法。

实现原理都是根据原型链的特性,再数组使用原生方法以前加一个拦截器,拦截器内定义的都是能够改变数组自身的异变方法,若是拦截器内没有就向一层去找。

接下来分析第二句,也是整个数组方法实现的核心:

augment(value, arrayMethods, arrayKeys)

----------------------------------------------------------------------------

const arrayProto = Array.prototype  // 数组原型,有全部数组原生方法
const arrayMethods = Object.create(arrayProto)  // 建立空对象拦截器

const methodsToPatch = [  // 七个数组使用会改变自身的方法
  'push','pop','shift','unshift','splice','sort','reverse'
]

methodsToPatch.forEach(function (method) {  // 往拦截器下挂载异变方法

  const original = arrayProto[method]  // 过滤出七个数组原生原始方法
  
  def(arrayMethods, method, function mutator (...args) {  // 不定参数
  
    const result = original.apply(this, args)  // 借用原生方法,this就是调用的数组
    
    const ob = this.__ob__  // 以前Observer类下挂载的__ob__
    
    let inserted  // 临时保存数组新增的值
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    if (inserted) {
      ob.observeArray(inserted)  // 执行Observer类中的observeArray方法
    }
    ob.dep.notify()  // 触发手动依赖收集器内的依赖
    
    return result  // 返回数组执行结果
  })
})

const arrayKeys = Object.getOwnPropertyNames(arrayMethods) 
// 获取拦截器内挂载好的七个方法key的数组集合,用于没有__proto__的状况

复制代码

首先获取数组的全部原生方法,从中过滤出七个调用能够改变自身的方法,而后建立拦截器在它下面挂载七个通过异变的方法,这个异变方法的使用效果和原生方法是一致的,由于就是使用apply借用的,将执行后的结果保存给result,好比:

const arr = [1, 2, 3]
const result = arr.push(4)
复制代码

这个时候arr就变成了[1,2,3,4]result保存的就是新数组的长度,既然模仿就模仿的像一点。

接下来的赋值const ob = this.__ob__,以前定义的__ob__不只仅是标记位,保存的也是Observer类的实例。

有三个操做数组的方法是会添加新值的,使用inserted变量保存新添的值。若是是使用splice方法,就将前面两个表示位置的参数截取掉。而后使用observeArray方法将新添加的参数包装为响应式的。

最后通知手动依赖管理器内收集到的依赖派发更新,返回数组执行后的结果。

最后执行第三句:

this.observeArray(value)

observeArray(items) {
  for (let i = 0, i < items.length; i++) {
    observe(items[i])
  }
}
复制代码

将数组内的是数组或对象的每一项都包装成响应式的。因此当数组再使用方法时,首先会去arrayMethods拦截器内查找是不是异变方法,不是的话才去调用数组原生方法:

export default {
  data() {
    return {
      list: [1, 2, 3]
    }
  },
  methods: {
    changeArr1() {
      this.list.push(4)  // 调用拦截器里的异变方法
    },
    changeArr2() {
      this.list = this.list.concat(5) 
      // 调用原生方法,由于拦截器里没有,必须从新赋值由于不会改变自身
    }
  }
}
复制代码

至此数组响应式系统相关的也讲解完毕,整个响应式系统也分析完了。

数组响应式总结:数组的依赖收集仍是在get方法里,不过依赖的存放位置会有不一样,不是在defineReactive方法的dep里,而是在Observer类中的dep里,依赖的更新是在拦截器里的数组异变方法最后手动更新的。

一样数组响应式也是否是完美的,它也有缺点:

export default {
  data() {
    return {
      list: [1, 2, 3]
    }
  },
  methods: {
    changeListItem() {  // 改变数组某一项
      this.list[1] = 5
    },
    changeListLength() {  // 改变数组长度
      this.list.length = 0
    }
  }
}
复制代码

以上两种方式都改变了数组,但响应式是没法监听到的,由于不会触发set也没用使用数组方法去改变。不过你们还记得咱们以前介绍的手动依赖管理器么?咱们能够手动去通知它更新依赖而后触发视图变动~

export default {
  data() {
    return {
      list: [1, 2, 3],
      info: { name: 'cc' }
    }
  },
  methods: {
    changeListItem() {  // 改变数组某一项
      this.list[1] = 5
      this.list.__ob__.dep.notify()  // 手动通知
    },
    changeListLength() {  // 改变数组长度
      this.list.length = 0
      this.list.__ob__.dep.notify()  // 手动通知
    },
    changeInfo() {
      this.info.sex = 'man'
      this.info.__ob__.dep.notify()  // 对象也能够
    }
  }
}
复制代码

常规的对象增长属性是不会被感知到的,也可使用手动通知的形式触发依赖,知道这个原理仍是很cool的~

官方填坑

上面的奇技淫巧并不被推荐使用,咱们仍是介绍下官方推荐的弥补响应式不足的两个API$set$delete,其实它们只是处理一些状况,都不知足的最后仍是调了一下手动依赖管理器来实现,只是进行了简单的二次封装。

this.$set || Vue.set

function set(target, key, val) {
  if(Array.isArray(target)) {  // 数组
    target.length = Math.max(target.length, key)  // 最大值为长度
    target.splice(key, 1, val)  // 移除一位,异变方法派发更新
    return val
  }
  
  if(key in target && !(key in Object.prototype)) {  // key属于target
    target[key] = val  // 赋值操做触发set
    return val
  }
  
  if(!target.__ob__) {  // 普通对象赋值操做
    target[key] = val
    return val
  }
  
  defineReactive(target.__ob__.value, key, val)  // 将新值包装为响应式
  
  target.__ob__.dep.notify()  // 手动触发通知
  
  return val
}
复制代码

首先判断target是不是数组,是数组的话第二个参数就是长度了,设置数组的长度,而后使用splice这个异变方法插入val。 而后是判断key是否属于target,属于的话就是赋值操做了,这个会触发set去派发更新。接下来若是target并非响应式数据,那就是普通对象,那就设置一个对应key吧。最后以上状况都不知足,说明是在响应式数据上新增了一个属性,把新增的属性转为响应式数据,而后通知手动依赖管理器派发更新。

this.$delete || Vue.delete

function del (target, key) {
  if (Array.isArray(target)) {  // 数组
    target.splice(key, 1)  // 移除指定下表
    return
  }
  
  if (!hasOwn(target, key)) {  // key不属于target,再见
    return
  }
  
  delete target[key]  // 删除对象指定key
  
  if (!target.__ob__) {  // 普通对象,再见
    return
  }
  target.__ob__.dep.notify()  // 手动派发更新
}
复制代码

this.$delete就更加简单了,首先若是是数组就使用异变方法splice移除指定下标值。若是target是对象但key不属于它,再见。而后删除制定key的值,若是target不是响应式对象,删除的就是普通对象一个值,删了就删了。不然通知手动依赖管理器派发更新视图。

最后按照惯例咱们仍是以一道vue可能会被问到的面试题做为本章的结束~

面试官微笑而又不失礼貌的问道:

  • 请简单描述下vue响应式系统?

怼回去:

  • 简单来讲就是使用Object.defineProperty这个API为数据设置getset。当读取到某个属性时,触发get将读取它的组件对应的render watcher收集起来;当重置赋值时,触发set通知组件从新渲染页面。若是数据的类型是数组的话,还作了单独的处理,对能够改变数组自身的方法进行重写,由于这些方法不是经过从新赋值改变的数组,不会触发set,因此要单独处理。响应系统也有自身的不足,因此官方给出了$set$delete来弥补。

下一篇:Vue原理解析(八):一块儿搞明白使人头疼的diff算法

顺手点个赞或关注呗,找起来也方便~

参考:

Vue.js源码全方位深刻解析

Vue.js深刻浅出

Vue.js组件精讲

剖析 Vue.js 内部运行机制

相关文章
相关标签/搜索