# Vue响应式原理

# 对象的响应式

初始化data选项,如果data不是对象,则报警告。调用observe(data),对data进行观察。

function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  // 如果data不是对象,报警告
  if (!isPlainObject(data)) {
    data = {}
    process.env.NODE_ENV !== 'production' && warn(
      'data functions should return an object:\n' +
      'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
      vm
    )
  }
  observe(data, true /* asRootData */)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

执行observe(value)时:

  1. 如果value不是对象,直接返回;
  2. 如果对象__ob__有值,则表示该对象已经被观察过了,直接返回ob(ob是Observer类的实例);
  3. new一个Observer(value)实例,并返回。
function observe (value: any, asRootData: ?boolean): Observer | void {
  // 1.如果不是对象直接return
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob: Observer | void
  // 2.如果已经观测过则直接返回上次观测的实例
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if (
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    // 3.如果可以观测(对象、或数组)就进行观测
    ob = new Observer(value)
  }
  // 4.如果是根数据  vmCount标注为1
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

初始化Observer时,执行其构造函数:

  1. 给实例添加dep属性:该属性主要用于数组操作,及对象属性的动态添加、删除的监控;
  2. 如果value是数组;
    1. 修改数组的原型为arrayMethod,即重写数组的push、pop、shift、unshift、splice、sort、reverse这7个方法,这些方法会修改数组本身。
    2. 执行observeArray(value),对数组的每一项进行观察。
  3. 如果value是一个普通对象
    1. 执行walk(value),循环value的每一个key,通过defineReactive(obj, keys[i]),重写getter、setter。
class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that have this object as root $data
  constructor (value: any) {
    this.value = value
    // 每个对象都一个自己的dep(主要用于数组的依赖收集)
    this.dep = new Dep()
    this.vmCount = 0
    // value.__ob__赋值为Observer的实例,即每个value都有个一个__ob__属性,指向value对应的Observer实例。
    def(value, '__ob__', this)
    // 1、如果是数组,重写数组方法(push,pop,shift,unshift,splice,sort,reverse)
    if (Array.isArray(value)) {
      if (hasProto) { // (__proto__ in {})
        // 1)将value的原型__proto__,指向arrayMethods,用于拦击数组方法(这7个方法会改变数组本身,所以需要拦截)
        protoAugment(value, arrayMethods)
      } else {
        // 2)或者定义value.method为arrayMethods[method]方法
        copyAugment(value, arrayMethods, arrayKeys)
      }
      // 对数组进行观察
      this.observeArray(value)
    } else {
      // 2、如果是对象,最终调用defineReactive将其变为响应式的
      this.walk(value)
    }
  }
  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }
  observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

# 数组原型的重写

在上面我们说过,当value是一个数组的时候,我们重写了push,pop,shift,unshift,splice,sort,reverse这7个方法。当我们通过以上方法对数组操作时,就可以监控到用户的操作。

  1. 通过arrayProto = Object.create(arrayProto)方法,创建一个指向数组原型的对象副本
  2. 给arrayMethods对象添加这7个方法;
    1. 其中push、unshift、slice可能会给数组添加新的数据。我们将新添加的数据存放到inserted数组中,此时对新添加的数据,通过observeArray(inserted),依次对数组内的每个对象进行监控。
    2. 注意,const ob = this.__ob__这段代码。this为当前数组对象value。在new Observer(value)阶段,给value添加了一个__ob__属性,指向value对应的Observer实例。因此此时this.__ob__代表的是value的Observer实例ob,它有自己的dep实例。
  3. 调用ob.dep.notify(),通知页面更新。
// array.js
import { def } from '../util/index'
const arrayProto = Array.prototype
// arrayMethod的隐式原型:__proto__, 指向Array.prototype
export const arrayMethods = Object.create(arrayProto)

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

/**
 * Intercept mutating methods and emit events
 */
methodsToPatch.forEach(function (method) {
  // cache original method
  const original = arrayProto[method]
  // 重写数组的"push,pop,shift,unshift,splice,sort,reverse"这7个方法
  def(arrayMethods, method, function mutator (...args) {
    // apply,调用数组原来的方法
    const result = original.apply(this, args)
    // this就是我们的数组对象target,this.__ob__就是其Observer实例对象。
    const ob = this.__ob__
    let inserted
    switch (method) {
      case 'push':
        // push、unshift一样,关于switch语句的用法,可以参考https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/switch
      case 'unshift':
        // unshift,添加到数组头部
        inserted = args
        break
      case 'splice':
        // splice(start,deletCount, item1,item2),从第二个开始,是要添加的数据
        inserted = args.slice(2)
        break
    }
    // 如果inserted有数据,则对新添加的数组进行观察
    if (inserted) ob.observeArray(inserted)
    // notify change,重新渲染
    ob.dep.notify();
    // result:原生数组执行后的返回值
    return result
  })
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

# defineReactive响应式

通过Object.defineProperty,重写对象key的getter、setter。

  1. 如果value本身也是对象,则调用observe(value)方法,对value进行递归观察。
  2. 对key取值时,触发get方法,对key依赖的watcher进行收集
    1. 执行dep.depend(),对当前key的dep进行依赖收集。如果value是对象时,则此时childOb(子对象Observer实例)不为空,调用childOb.dep.depend()对子dep进行依赖收集。
    2. 如果value是数组,调用dependArray,循环数组的每一项,执行depend方法,递归收集数组的依赖。
function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()
  // val是对象(数组),则递归observe(val),返回子对象的Observer实例:childOb
  let childOb =  observe(val)
  // 3、重新定义getter、setter
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    // 依赖收集,每一个dep收集对应的watcher
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
         dep.depend();
        if (childOb) {  // 新设置的val是一个对象(数组),此时childOb存在
          childOb.dep.depend(); // childOb这个observer实例,去收集当前的watcher
          // 如果getter的执行结果value是一个数组,则对数组中的内容继续进行依赖收集
          if (Array.isArray(value)) {  // 递归收集数组中的依赖
            // 如果数组中还有数组,例如"[[20,21],1,2]", 当我们通过vm.arr[0].push(100)时,也能触发更新
            dependArray(value)
          }
        }
      }
      return value
    },
    // 派发更新,
    // 循环dep中的watchers,执行watcher.update -> queneWatcher -> nextTick(flushSchedulerQueue -> 依次执行watcher.run方法)
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val;
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      childOb = observe(newVal)
      // 每个key都有自己的dep
      dep.notify()
    }
  })
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
function dependArray (value: Array<any>) {
  for (let e, i = 0, l = value.length; i < l; i++) {
    e = value[i]
    e && e.__ob__ && e.__ob__.dep.depend()
    if (Array.isArray(e)) {
      dependArray(e)
    }
  }
}
1
2
3
4
5
6
7
8
9

# 响应式流程分析

下图就是Vue中,实现对象响应式的整个过程。
响应式原理.jpg
总结起来如下:

# 1、观察阶段

  1. 只有对象才可以被observe;
  2. observe(value)时,初始化一个Observer实例:
    1. value是数组:
      1. 则重写数组方法。当我们调用push,pop,shift,unshift,splice,sort,reverse这七个方法时,调用this.__ob__.dep.notify()通知页面更新。
      2. 调用observeArray(value),对数组内的每一项进行观察。因为数组内的项可能有对象
    2. 如果value是对象:
      1. 调用walk(value)循环对象的每一个key,对每个key调用defineReactive(obj, key)设置对象的响应式,即重写getter、setter。
        1. 因为obj[key]的值可能也是对象,因此需要对该值进行观察,即observe(obj[key]),并赋值为childOb;

# 2、依赖收集阶段

当我们对一个对象的key取值时(假设值为value),触发其getter,如果Dep.target不为空,则调用dep.depend()收集当前key依赖的watcher。因为childOb可能不为空(对象key对应的值可能也是对象),因此需要对该对象进行依赖收集,即childOb.dep.depend()。

  • 如果这个对象value是数组,则调用dependArray,循环数组的每一项进行依赖收集。因为这个数组内部可能还有嵌套的数组。

# 3、更新阶段

当我们对一个对象的key设置新的值value

  • 新值value可能也是对象,调用observe(value)对新设置的值进行观察;
  • 调用dep.notify,通知页面更新。


至此,对象的响应式处理完成,包括设置响应式、收集依赖、更新这3个阶段。