深入浅出vue源码一(变化检测章)

532 阅读6分钟

前言


我们都知道Vue是MVVM框架的一种,它的最大特点就是数据驱动视图。那么什么是数据驱动试图呢? 这里我们可以简单的把数据看为状态,把视图看做UI, UI不可能一成不变的,它应该是动态变化的,所以得出状态变化视图随之变化就可以称为数据驱动试图。我们用以下数学公式来描述:

UI = Render(State)

在这里UI代表用户界面,State代表状态,Vue充当了Render角色。Vue发现state改变之后,经过一系列的加工最终呈现到用户UI上。那么第一个问题来了,Vue怎么知道state什么时候变化了呢?

一. 什么是变化检测


Vue是怎么知道state变化了呢?这就得出了变化检测这个概念,即状态追踪,当state发生改变,去通知视图进行更新。 变化检测是出现很久的一个词,在其它MVVM框架中,React使用对比虚拟节点来完成变化检测,Angular使用脏数据检查流程来完成变化检测。那么接下来我就来通过源码解析来分析Vue是怎么完成变化检测的机制,我们这儿解析的版本为2.6.x版本

二. Object的变化检测


我们知道MVVM框架的核心是数据驱动视图,知道了数据什么时候变化,然后通知视图图去更新,那么问题便迎刃而解了。在Vue 2中使用的是Obeject.defineProperty来检测数据的变化。

1. 使Object对象变为可检测。

let myTeslaCar = {
  name: '我的小车车',
  age: 1,
  description: '2016年 黑色 Tesla'
}
Object.defineProperty(myTeslaCar, "name", {
  set(newVal) {
    console.log('name属性被设置了值',newVal)
  },
  get () {
    console.log('name属性被读取了')
  }
})
let carName = myTeslaCar.name
myTeslaCar.name = '我的tesla Model3'

输出结果如下:

image.png

接下来:我们让myTeslaCar的每一个属性变为可检测,我们创建一个Observe类,完整代码如下所示。

function def(object, key ,val) {
  Object.defineProperty(object,key, {
    configurable: true,
    writable: true,
    enumerable: true,
    value: val
  })
}
// 对应vue源码位置: src/core/observer/index.js
/** Observer类通过递归的方式把一个对象的所有属性都变为可监测对象 */
class Observer {
  constructor(value) {
    this.value = value;
    //给value新增一个‘__ob__'属性,值位当前Observe实例。
    // 可以理解为打上了一个标记,标记它为响应式了,避免重复操作
    def(value,'__ob__',this);
    if(Array.isArray(value)) {
      // 当val为数组的时的逻辑
    } else {
      this.walk(value);
    }
  }

  /**
   * @description 遍历每一个属性,然后转为可检测 
   * @param {Object} obj 
   */
  walk(obj) {
    const keys = Object.keys(obj).filter(key => key!== '__ob__');
    for(let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i]);
    }
  }
}

/**
   * @description: 将一个对象的key转为可检测
   * @param {object} obj
   * @param {string} key
   * @param {*} val
   */
 function defineReactive(obj, key, val) {
    // 如果只传了obj和key,那么val=obj[key]
    if(arguments.length === 2) {
      val = obj[key]
    }
    // 如果val是对象,就递归
    if(typeof val === 'object') {
      new Observer(val);
    }
    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get() {
        console.log(`${key}属性被读取了`)
        return val;
      },
      set(newVal) {
        if(val === newVal){
          return
        }
        console.log(`${key}属性被设置新值${newVal}`)
        val = newVal;
      }
    })
}

我们创建了一个ObServer类,并将将一个Object转换成了可检测的Object. 我们给Value添加了一个__ob__属性标记此对象以及转换成响应式的了避免重复操作;然后只有object类型才会调用walk将每一个属性转换成getter/setter的形式来检测属性变化。在defineReactive中若果子属性还是一个对象,那么使用new Observer(val)来递归子属性,这样我们就可以将object中的所有属性(包括子属性)转换成getter/setter形式。也就是说,只要将对象传入Observe,就能将对象的所有属性变化可检测的,响应的object

observer类位于源码的src/core/observer/index.js中。

现在我们重新定义一个myCar2对象:

const myCar2 = {
  name: '我的小车车',
  age: 1,
  description: '2016年 黑色 Tesla',
  infomation: {
    color: 'black',
    date: '2016-9.10'
  }
}
const carObserver = new Observer(myCar2);
console.log(carObserver)
console.log(carObserver.value.name)

运行结果如下:

image.png

这样myCar2的所有对象就变为响应式的了。

2. 依赖收集

2.1 什么是依赖收集?

在上一章中,我们完成了第一步,我们将一个对象转换成了响应式的可检测对象。知道了数据变化的时机,我们就能通知视图去更新变化。那么问题来了,我们究竟怎样通知视图去变化呢?视图那么大,我们该通知谁去变化?总不能一个数据变化,就把整个视图都更新一遍?这样是显然是不合理的。

这个时候你会想到,那么谁用到了对应的状态,就更新谁呗! 对了,就是这个思想,现在我们换一种优雅的说法:我们把谁用到了数据改为谁依赖了状态,我们把每一个状态创建一个依赖数组(因为一个状态可能被用于多处),当一个状态发生了变化,我们就去对应的依赖数组去通知每一个依赖,告诉它们你们依赖的状态变化呢,你们改更新啦!这一过程就是依赖收集。

2.2 何时收集依赖,何时通知依赖更新?

明白了依赖收集这个概念之后,那我们来思考这个问题,那到底何时收集依赖?何时更新依赖呢? 你是不是想到了gettersetter呢?对的,就是这两个关键点。其实,谁用到了状态,就相当于谁读取了状态。这样我们就应该在getter中收集依赖,然后在状态变化时在setter中通知对应依赖变化。总结一句话如下:

getter中收集依赖,setter中通知依赖更新

2.3 把依赖收集到哪里?

了解了依赖收集和怎样收集依赖和何时更新依赖之后,我们来思考下,把依赖收集到哪儿呢?

在2.1中我们说到可以用一个依赖数组来保存依赖,谁依赖了状态就把谁放进对应状态的依赖数组。但是单单使用一个数组来管理依赖的话,功能好像并不完善并且代码的耦合性也过于高。更好的做法是,我们应该扩展管理依赖的功能,对每一个数据都建立一个依赖管理器,把这个数据的所有依赖都管理起来。Vue中使用了Dep类来管理依赖,我们来看如下简版依赖管理类Dep

/*
 * @Description: 依赖管理类Dep
 */
    export default class Dep {
      constructor() {
        this.subs = []
      }

      addSub(sub) {
        this.subs.push(sub)
      }

      removeSub(sub) {
        remove(this.subs, sub)
      }

      depend() {
        if(window.target) {
          this.addSub(window.target)
        }
      }
    }

    /** 删除数组中的给定item */
    export function remove(arr, item) {
      if(arr.length) {
        const index = arr.indexOf(item)
        if(index > -1) {
          return arr.splice(index, 1)
        }
      }
    }

我们定义了一个简易的Dep类,然后添加增删依赖的方法,使用depend添加依赖,使用notify方法通知依赖更新。现在我们就可以在getter中进行依赖收集,在setter中通知依赖更新了。看defineReactive中的setter和getter。

function defineReactive(obj, key, val) {
    // 如果只传了obj和key,那么val=obj[key]
    if(arguments.length === 2) {
      val = obj[key]
    }
    // 如果val是对象,就递归
    if(typeof val === 'object') {
      new Observer(val);
    }
    const dep = new Dep(); // 实例化一个依赖管理器
    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get() {
        console.log(`${key}属性被读取了`)
        // 收集依赖
        dep.depend();
        return val;
      },
      set(newVal) {
        if(val === newVal){
          return
        }
        console.log(`${key}属性被设置新值${newVal}`)
        val = newVal;
        dep.notify(); // 通知依赖更新
      }
    })
}

3. 依赖到底是谁?

通过上一章的学习,你现在应该了解了什么是收集依赖,以及收集依赖和通知依赖更新的时机。那么到底依赖是谁呢? 虽然我们说谁用到了状态谁就是依赖,但这只是我们的口述,我们需要知道真正的依赖在代码中时怎么实现的。

在Vue2中,其实还实现了一个Watcher类,它就是我们上文所描述的谁,换句话说: 谁用到了状态,谁就是依赖,就为谁创建一个Watch实例。在数据发生变化时,我们不是直接通知依赖,而是通知依赖的Watch实例,由Watch实例来通知视图更新。 下面是Watch类的简版实现:

export default class Watcher {
  constructor (vm,expOrFn,cb) {
    this.vm = vm;
    this.cb = cb;
    this.getter = parsePath(expOrFn)
    this.value = this.get()
  }
  get () {
    window.target = this;
    const vm = this.vm
    let value = this.getter.call(vm, vm)
    window.target = undefined;
    return value
  }
  update () {
    const oldValue = this.value
    this.value = this.get()
    this.cb.call(this.vm, this.value, oldValue)
  }
}

/**
 * Parse simple path.
 * 把一个形如'data.a.b.c'的字符串路径所表示的值,从真实的data对象中取出来
 * 例如:
 * data = {a:{b:{c:2}}}
 * parsePath('a.b.c')(data)  // 2
 */
const bailRE = /[^\w.$]/
export function parsePath (path) {
  if (bailRE.test(path)) {
    return
  }
  const segments = path.split('.')
  return function (obj) {
    for (let i = 0; i < segments.length; i++) {
      if (!obj) return
      obj = obj[segments[i]]
    }
    return obj
  }
}

谁用到了状态谁就是依赖,我们就为每一个依赖创建一个Watch实例,在Watch实例初始化过程中读取了数据,首先它会将自己挂载到全局唯一的地方window.target(vue2源码使用的是Dep.target)。 由于读取了数据,将会触发该数据的getter收集依赖, 然后将会通过Dep.depend取到当前正在读取数据的依赖(即Watch实例, window.target)并存入依赖数组,然后在Watch的get方法中将window.target释放掉。

当数据发生改变时,会在当前数据的setter中通过dep.notify()通知依赖更新,然后在dep.notify()中遍历依赖然后调用Watchupdate方法,从而更新视图。

简单来说:

Watch会将自己挂载到全局唯一的位置上,然后读取数据时将会通过getter收集依赖,取得是全局上面挂载的这个值(如本例的window.target)并添加到依赖组里。收集好依赖之后,Dep会通知所有的依赖更新,调用Watch的update来达到更新视图。

为了便于理解,可以参照下图:

graph LR
Data(数据) --> Getter(getter) --3.取到当前正在读取数据的依赖--> Global(window.target)
Data(数据) --> Setter(setter)
Getter(getter) -.2.读取数据触发getter返回数据.-> Wather(Wather) 
Wather(Wather) --1.挂载到全局唯一的位置--> Global(window.target)
Global --4.添加到依赖数组--> Dep(Dep)

4. 不足之处

  1. Object.defineProperty有一点不足就是:向对象添加新的一对key/value时,它是无法进行检测的,即会出现一个问题,当我们添加或删除一个key/value时,无法知道状态变化了,无法通知依赖,无法进行视图更新。这也就是我们开发时常发现数据变更但视图没有更新的原因之一。
  2. Object.defineProperty是无法拦截到数组的大部分操作的,Vue2的解决方案是重写了数组中的常使用的几个方法,这一点我们将在数组的变化检测章详细说明。

当然Vue2也注意到了这一点,提供了全局API来解决这个问题,分别是Vue.setVue.delete,这两个API将在全局API章节详细解析实现原理。

5.总结

首先,我们通过Object.defineProperty实现了对属性的变化检测,并且封装了Observer类实现了对Object的所有属性都转换成getter/setter的形式来完成动态检测。

然后我们学习了什么是收集依赖,知道了在getter中收集依赖,在setter中通知依赖更新。然后封装了Dep依赖管理类来管理依赖收集。

最后,我们为每一个依赖都创建了Wacth实例,当数据变化时,我们去通知Watch实例,由Watch实例去做更新操作。

整体流程如下:

1.通过Observer类将对象转换成getter/setter形式来追踪状态变化。

2.当外界使用Watch实例读取数据就会触发getter将Watch实例添加到Dep中去。

3.当数据发生变化时,将会由setter通知依赖(Watch)去更新变化。

4.当Watch收到通知时,会通知外界,外界收到通知后可能会更新视图,也有可能调用用户设置的回调函数。

三. 数组的变化检测

上一章中我们介绍了对象的变化检测,那么这一章我们将来学习数组的变化检测。为什么Array的变化检测要轮出来讲,为什么和Object的是两套逻辑?

因为我们使用的是对象的原型上的defineProperty方法,所以Array是无法使用的,我们需要了解Vue是怎么设计数组的变化检测逻辑。 虽然对象的数组检测拥有另外的一套逻辑,但基本思想并没有改变:

在读取数据时收集依赖,在数据变化时通知所有依赖进行更新。

1. 在哪儿收集依赖

我们应该将使用到Array的地方收集作为依赖,那么问题来了,在哪儿收集依赖呢?

其实Array的收集方式和Object相同,都是在getter中收集。

有的同学就会问了,defineProperty不是无法监听到数组变化,那又要怎么监听到数据变化,那又怎么触发getter呢?

我们回想一下,我们定义数组时候的方式是不是如下方式:

export default {
    data () {
        return {
           ...,
           hobby: ['爱好1', '爱好2',...,'爱好n']
           ...,
        }
    },
}

有的同学是不是突然就发现了,我们的数据都是写到对象里面的,要取到这个hobby只要从对象读取一下就行了,这样就触发了hobby的getter,从而可以收集依赖了。如此得到标题的解:

Array收集依赖也是在getter中进行。

2. 让Array变为可检测。

上一节我们已经知道Array收集依赖也是通过getter进行,我们回想一下Object的变为可检测的流程,读取数据时应该在getter中收集依赖,数据变化时在setter中通知依赖更新。此时我们已经知道收集依赖通过getter进行,也就完成了一部分,即我们知道了Array何时被读取了,我们无法感知Array何时变化了。下面我们就围绕这一问题来进行分析:Array型数据发生变化时我们如何得知?

2.1 思路

Object的数据变化可以通过setter感知到,但是Array并没有setter,我们如何得知数据何时变化的了?

其实思考一下,Array改变,绝对调用了数组的方法,而改变原数组的方法就那么几个。我们能不能在不改变原有功能的情况下,重写一下这几个方法并扩展一下其功能,例如下面的例子:

    Array.prototype.newPush = function(value) {
     this.push(value)
     console.log(`数组被修改了`)
    }

浏览器运行结果如图所示:

image.png

2.2 拦截器

改变自身数组的方法有7个,分别是push,pop,shift,unshift, splice, sort, reverse基于我们的思路,我们需要实现一个拦截器arrayMethods,它拦截Array实例到原型对象这一层,Array实例实际调用的是我们拦截器里实现的方法。

// 源码位置:/src/core/observer/array.js
const arrayPrototype = Array.prototype
export const arrayMethods = Object.create(arrayPrototype)

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

arrayMap.forEach(function(method){
  const origin = arrayPrototype[method];
  Object.defineProperty(arrayMethods, method, {
    configurable: true,
    enumerable: true,
    writable: true,
    value: function mutator(...args) {
      const result = origin.apply(this, args)
      return result;
    }
  })
})

为了方便理解,调用流程如图所示:

graph LR
Array(array) --> pop --> 拦截器[arrayMethods]
Array --> push --> 拦截器
Array --> shift --> 拦截器
Array --> unshift --> 拦截器
Array --> splice --> 拦截器
Array --> sort --> 拦截器
Array --> reverse --> 拦截器

拦截器--> Array.prototype

上述代码,我们首先创建了一个继承Array的原型对象的空拦截器对象arrayMethods。 然后使用defineProperty对改变原数组的7个方法逐个进行封装,array实例使用这7个方法时实际是使用了我们拦截器中同名方法,在同名方法中origin代表原始对应方法,这样我们拦截中的方法就可以扩展一些逻辑,比如说通知更新。

2.3 使用拦截器。

上一节中我们定义好了拦截器,但是我们还没有挂载到数组实例数组原型对象之间,挂载其实只要当数据类型为Array时,把数据的__proto__赋值为我们的拦截器arrayMethods就可以了。

下面我们看下添加了Array定义的Observe类代码:

class Observer {
  constructor(value) {
    this.value = value;
    //给value新增一个‘__ob__'属性,值位当前Observe实例。
    // 可以理解为打上了一个标记,标记它为响应式了,避免重复操作
    def(value,'__ob__',this);
    if(Array.isArray(value)) {
      // 当val为数组的时的逻辑
      const augment = hastProto ? protoToTarget : copyToSelf;
      augment(value, arrayIntercepter, arrayIntercepterKeys)
    } else {
      // 对象时变为可检测的逻辑
      this.walk(value);
    }
  }
  // 省略之前的代码...
}
/** 检测浏览器是否支持__proto__ */
export const hastProto = '__proto__' in {}

const arrayIntercepterKeys = Object.getOwnPropertyNames(arrayIntercepter)

/** 挂载src的原型到target,即src.__proto__ = target */
function protoToTarget(src,target) {
  src.__proto__ = target;
}

/**
 * @description: 复制目标上面的属性到自身上面
 * @param {object} src
 * @param {object} target
 * @param {string[]} keys
 */
function copyToSelf(src, target, keys) {
  for(let i =0 ; i < keys.length; i++) {
    const key = keys[i];
    def(src, key, target[key]);
  }
}

上述代码,我们首先检测浏览器是否支持__proto__属性,如果支持我们就调用protoToTarget将原型属性__proto__赋值为我们上文写的拦截器arrayIntercepter;如果不支持我们就调用copyToSelf方法把拦截器上面的定义的7个重写方法复制到value里面。

当拦截器生效之后,当数组发生变化之后,我们就可以在拦截器里面通知变化了,也就是说我们知道数组何时发生变化了,这样我们也就完成了Array的变化检测。

3. 数组的依赖收集

3.1 数组的依赖收集在哪儿

我们知道Observer类完成了给数据添加了getter/setter,所以依赖应该也应该在Observer进行收集,源码是如下完成的:

export class Observer {
  constructor(value) {
    this.value = value;
    //给value新增一个‘__ob__'属性,值位当前Observe实例。
    // 可以理解为打上了一个标记,标记它为响应式了,避免重复操作
    def(value,'__ob__',this);
    // 实例化一个依赖管理器,用来收集数组依赖
    this.dep = new Dep();
    if(Array.isArray(value)) {
      // 当val为数组的时的逻辑
      const augment = hastProto ? protoToTarget : copyToSelf;
      augment(value, arrayIntercepter, arrayIntercepterKeys)
    } else {
      // 对象时变为可检测的逻辑
      this.walk(value);
    }
  }
}

源码是在实例上添加了一个dep依赖管理器来收集数组的依赖。

3.2 怎么收集依赖

在上文,我们有提到数组的依赖也应该在getter中收集,也在Observe类中添加了一个依赖管理器来收集数组的依赖,那么我们怎么在getter里面进行收集依赖呢?

我们思考一下:我们是不是只要在getter拿到Observer类的实例中的dep就可以了,那另一个问题又来了;我们怎么拿到Observer的实例呢?

哎,有的同学是不是已经想到了我们的标记属性__ob__, 是的,就是利用这个属性,它存储的值就是Observer实例本身,下面我们看看源码中的实现方法。

    function defineReactive(obj, key, val) {
    // ...省略这部分的代码...
    const childOb = observer(val); // 拿到val的Observer实例
    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get() {
        console.log(`${key}属性被读取了`)
        // 收集依赖
        dep.depend();
        if(childOb) {
          // 数组本身的依赖进行收集。
          childOb.__ob__.dep.depend();
          if(Array.isArray(value)) {
            // 数组的每一项进行依赖收集
            dependArray(value);
          }
        }
        return val;
      }
      // ...省略这部分的代码...
     })
  })
  /** 
 * @description 数组的每一项依赖进行收集
 */
function dependArray(arrayData) {
  if(!Array.isArray(arrayData)) {
    return;
  }
  for(let i = 0; i < arrayData.length; i++) {
    const e = arrayData[i];
    e && e.__ob__ && e.__ob__.dep.depend();
    if(Array.isArray(e)) {
      dependArray(e);
    }
  }
}
function isObject (obj) {
  return obj !== null && typeof obj === 'object'
}

/**
 * @description 返回value的Observer实例,如果__ob__没有就通过Observer变为响应式
 * @param {object} value 
 */
export function observer(value) {
  // 不是对象或者是虚拟节点就返回
  if(!isObject(value) || value instanceof VNode) {
    return;
  }
  let ob;
  if(value.hasOwnProperty('__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else {
    ob = new Observer(value);
  }
  return ob;
}

上述代码中,我们首先尝试判断传入的val是否拥有__ob__属性,我们在前文中有提及到,拥有此属性的对象就代表已经成为响应式的了,如果没有,就调用new Observer(val)转为响应式的数据并返回这个Observer实例;这样我们就可以拿到实例的依赖管理器,然后我们在getter中进行依赖收集,然后递归收集数组每一项的依赖。

Tips: typeof 数组或者null的结果为Object

3.3 怎么通知依赖更新

前文我们提及到数组的数据新增或减少是不会粗发setter的, 而改变数组自身的方法有7种,所以我们封装了数组实例与数组原型对象之间的拦截器,我们应该在拦截器中的同名方法中通知依赖。

那具体怎么做呢,首先要通知依赖,就要先访问到依赖。我们应该拿到响应式数据的Observer实例,即__ob__属性,拿到了之后,就可以访问到对应实例的依赖管理器,调用notify方法通知更新即可。下面我们看代码:

arrayMap.forEach(function(method){
  const origin = arrayPrototype[method];
  Object.defineProperty(arrayIntercepter, method, {
    configurable: true,
    enumerable: true,
    writable: true,
    value: function mutator(...args) {
      const result = origin.apply(this, args)
      // 拿到Observer实例
      const ob = this.__ob__;
      // 通知依赖更新
      ob.dep.notify(); 
      return result;
    }
  })
})

由于拦截器是挂载到原型属性上,所以this代表的就是数据value,拿到了Observer实例之后,你就可以访问到依赖管理器dep然后通过notify方法进行更新了。到这,数组的变化检测就已经完成了。

4. 实现深度检测

我们上文所说的都是增对数组自身的变化检测而言,对数组添加一个元素或者删除一个元素都是可以检测到的。但是如果数组的子元素变化了,以上操作就检测不到。而在Vue中,无论是Object数据还是Array数据都是深度检测,Object类型的数据我们已经在defineReactive中进行递归处理完成了。

那么Array类型的数据怎么实现深度检测呢?是不是有的同学就会想到,我们遍历每一项,然后通过new Observer不就得了。对的,其核心思想就是如此。我们看下代码:

tips: 深度检测即不但要检测自身的变化,还要检测到每一个子元素的数据变化。

export class Observer {
  constructor(value) {
    this.value = value;
    def(value,'__ob__',this);
    this.dep = new Dep();
    if(Array.isArray(value)) {
      const augment = hastProto ? protoToTarget : copyToSelf;
      augment(value, arrayIntercepter, arrayIntercepterKeys)
      // 将数组的每一项转换为可被检测的响应式数据
      this.observeArray(value);
    } else {
      this.walk(value);
    }
  }

  /**
   * @description: 将数组的每一项变为响应式数据
   * @param {Array} items
   */
  observeArray (items) {
    for(let i = 0 ; i < items.length; i++) {
      observer(items[i]);
    }
  }
  //... 省略其它定义代码
}

/**
 * @description 返回value的Observer实例,如果ob没有就通过Observer变为响应式
 * @param {Observer} value 
 */
export function observer(value) {
  if(!isObject(value) || value instanceof VNode) {
    return;
  }
  let ob;
  if(value.hasOwnProperty('__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else {
    ob = new Observer(value);
  }
  return ob;
}

上述代码会调用observerArray递归数组的每一项,然后调用observer将不是响应式的子元素转为响应式数据。

5. 新增元素的变化检测。

对于数组我们已经完成了深度检测,但是上述逻辑还会有一种问题,我们在新增一个元素时,新增的此元素并不是响应式的,我们还需将数组新增的元素转为响应式的。

我们思考一下,向数组添加元素的方法无非三种,push,unshift,和splice这三个,当调用这三个方法时,我们在拦截器添加转换为响应式数据的逻辑处理,将新增的元素通过Observer的observeArray方法转为响应式不就完成了?我们直接看代码:

arrayMap.forEach(function(method){
  const origin = arrayPrototype[method];
  def(arrayIntercepter, method, function mutator(...args) {
    const result = origin.apply(this, args)
      // 拿到Observer实例
      const ob = this.__ob__;
      let inserted;
      switch (method) {
        case 'push':
        case 'unshift': {
          // 如果是push和unshift第一项就是待插入的元素
          inserted = args;
          break;
        }
        case 'splice': {
          // splice第2个参数是待插入的元素
          inserted = args.splice(2);
        }
      }
      // 如果插入的元素存在,则调用observe函数将新增的元素转为响应式数据
      if(inserted) ob.observeArray(inserted); 
      // 通知依赖更新
      ob.dep.notify(); 
      return result;
  })
})

tips: inserted为数组,因为observeArray参数为数组。

6. 不足之处

以上对于数组的变化操作都是基于原型对象的拦截,但是我们日常使用的另一种方式是使用下标访问和操作,这就出现了一个问题,如果使用下标进行操作数组,那么变化检测就检测不到了,如下代码所示:

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

当然Vue也注意到了这点,它添加了两个全局API来解决这个问题,即Vue.setVue.delete,这两个API的实现原理我将会在全局API分析一节单独为大家仔细分析,这儿就不再阐述。

7. 总结

在这一章中我们可以很清楚的知道,数据的访问我们是很容易通过getter感知到,但数据变化我们是无从得知。如是我们通过了拦截器重写了变化数组的7个方法来感知数组的变化。其次,我们对数组的依赖收集和通知依赖更新进行了深层次的分析,我们了解到Vue不但对数组自身作了变化检测,也对数组的子元素和新增的元素作了 变化检测逻辑,我们也分析了实现原理,由浅至深的向大家阐述了Vue实现数据变化检测的核心思想和基本原理。以下是思维导图。

Vue源码学习记录.png

四. 结语

4.1 作者有言

如果大家发现文中有阐述不合理和表述错误时欢迎批评和指正;如果还有不明白的地方也可以咨询作者,不用不好意思,我将知无不言。

成为大佬之路没有捷径可言,只有热爱和不断的学习。

4.2 参考指南

1. vue.js技术揭秘(没找到合适的链接,可以找我要电子文档)
2. vue.js源码全方位解析