通过断点跟踪探索Vue响应原理


基于版本2.5.2源码分析

响应原理

  1. Object.defineProperty()
  2. 观察者模式

响应原理的探索

文章对于Vue原理的探索基于下面的测试工程,结合浏览器断点跟踪堆栈的方式阅读源码。

测试工程代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
<template>
  <div id="app">
    {{ num }}
    <button @click="click">+1</button>
  </div>
</template>

<script>
export default {
  name: 'app',
  data () {
    return {
      num: 1
    }
  },
  methods: {
    click () {
      this.num += 1
    }
  }
}
</script>

运行效果

test_project

Vue对观察者模式的实现

核心三个类:ObserverDepWatcher

开始时,需要通过Observer类将数据转换成可被观察的对象。

Observer类的构造方法里通过this.walk()和this.observeArray()方法,间接地调用了Object.defineProperty(),Object.defineProperty()参数中的getter方法会使用一个new Dep()的闭包实例对象,该实例主要用于依赖收集(即收集Watcher实例)。

所以每个可被观察的对象都与一个Dep类实例绑定在一起,一对一关系

例如Vue会对this.num转换成一个可被观察的对象,将一个闭包的Dep实例注入到this.num的getter方法里。当this.num被读取值时,并且有Watcher实例,就会收集依赖,将该Watcher实例与闭包的Dep实例相互引用。这样,只要当this.num的setter方法被调用时,就可通过闭包的Dep实例通知所有的Watcher实例执行一个回调。

Dep类实例和Watcher实例关系是多对多

这三个类之间的通信过程如下:

  1. 将数据A通过Observer类转换成一个可被观察的对象。
  2. 改写数据A的getter和setter方法,并在getter方法里,嵌入一个闭包对象Dep类实例。
  3. 对数据A感兴趣的数据B创建一个Watcher类实例,把数据A的读取方式(可能function或字符串)当成参数传进Watcher类。
  4. Watcher类构造实例时,先将自己放到全局的Dep.target上,然后读取数据A,触发数据A的getter方法。
  5. 数据A getter方法里检查到Dep.target有值,收集依赖Watcher实例。
  6. 那以后数据A只要有值被更新,即调用setter方法,就能通知到所有依赖的Watcher实例。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Observer 类
export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that has this object as root $data

  // Observer的构造器
  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      const augment = hasProto
        ? protoAugment
        : copyAugment
      augment(value, arrayMethods, arrayKeys)
      this.observeArray(value)
    } else {
      this.walk(value) // 内部调用defineReactive方法
    }
  }
}
 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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// defineReactive 方法
/**
 * Define a reactive property on an Object.
 */
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  // 闭包的Dep类实例
  const dep = new Dep()

  // 省略部分代码

  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      // Dep.target 不为null,即有Watcher实例
      if (Dep.target) {
        // 这里就是收集依赖的地方
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      // 通知所有依赖,执行回调
      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
// Dep 类
export default class Dep {
   target: ?Watcher;
  id: number;
  subs: Array<Watcher>;

  // 省略部分代码

  // 与Watcher实例互相引用
  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

  // 调用所有Watcher的update方法,触发Watcher的回调
  notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}
 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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
// Watcher 类
export default class Watcher {
  // 省略部分代码

  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: Object
  ) {
    // 省略部分代码

    // parse expression for getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = function () {}
        process.env.NODE_ENV !== 'production' && warn(
          `Failed watching path: "${expOrFn}" ` +
          'Watcher only accepts simple dot-delimited paths. ' +
          'For full control, use a function instead.',
          vm
        )
      }
    }
    this.value = this.lazy
      ? undefined
      : this.get()
  }

  /**
   * Evaluate the getter, and re-collect dependencies.
   */
  // 每次new Watcher()时调用此方法
  get () {
    // 将自身的Watcher实例赋值到全局的Dep.target,等待下面调用this.getter.call()时,如果触发数据getter方法,就可以自动收集依赖
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm)
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`)
      } else {
        throw e
      }
    } finally {
      // "touch" every property so they are all tracked as
      // dependencies for deep watching
      if (this.deep) {
        traverse(value)
      }
      popTarget()
      this.cleanupDeps()
    }
    return value
  }

  /**
   * Add a dependency to this directive.
   */
  // 此方法会被Dep类实例触发,与其互相引用
  addDep (dep: Dep) {
    const id = dep.id
    if (!this.newDepIds.has(id)) {
      this.newDepIds.add(id)
      this.newDeps.push(dep)
      if (!this.depIds.has(id)) {
        dep.addSub(this)
      }
    }
  }

  // 省略部分代码
}

断点调试的方式跟踪源码

在源码中按Object.defineProperty()搜索,可以定位到主要的函数。根据响应原理,使用defineProperty()的方法一定会设置getter和setter方法

vm.$data里的数据如何被监听变化的?

先在defineReactive()方法里的getter方法设置断点,命中后,可看到getter方法的返回值符合预期,是初始化值1。并且代码运行至936行,进行依赖收集,监听num=1的变化。

1

跟踪堆栈的内容,可以发现是Vue将template块转换成render函数后,在读取_vm.num值时触发到getter方法的。

2

前提,需要先对vm.$data下所有的字段调用defineProperty()才能触发依赖的收集。同样通过断点,发现这个过程是通过调用src/core/instance/state.js的initData方法

3

4

如果对num的值进行更新,就会相应触发setter方法。并在setter方法的最后,会通过dep.notify()通知所有依赖num值的依赖方。

5

computed属性如何被监听的?

  1. 先加一个computed属性
1
2
3
4
5
6
  computed: {
    plusNum () {
      return this.num + 1
    }
  }
  1. 在template里使用plusNum
1
2
3
4
5
6
7
<template>
  <div id="app">
    {{ num }}
    {{ plusNum }}
    <button @click="click">+1</button>
  </div>
</template>

最后在defineComputed()和createComputedGetter()这两个方法里加个断点。

6

7

computed里的字段通过initComputed()自动初始化,初始化时,在defineComputed()方法里调用仍然是通过defineProperty()进行设置getter和setter方法。只是computed属性字段比data特殊,setter方法不需要设置。

通常computed属性字段会依赖vm.$data的属性字段。例如this.plusNum就依赖了this.num。

Vue做法是this.num值被更新时(触发this.num的setter方法里的dep.notify()),会通过Watcher类的update方法将dirty值设置为True。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
export default class Watcher {
  update () {
    /* istanbul ignore else */
    if (this.lazy) {
      // computed属性字段在初始化Watcher实例时会将lazy赋值为True
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }
}

每次this.plusNum被取值时(触发getter方法),会检查watcher.dirty,True的话,表明依赖有更新,需要重新计算。

watch是如何实现的?

首先对测试工程代码进行修改

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<template>
  <div id="app">
      <!-->删掉{{ num }}的引用</-->
    <button @click="click">+1</button>
  </div>
</template>

<script>
export default {
  data () {
    return {
      num: 1,
      plusNum: 0  // 新增属性plusNum
    }
  },
  watch: {
    // 新增num的watch方法
    num (newVal) {
      this.plusNum = this.num + 1
    }
  }
}
</script>

Vue对watch功能的实现思路是在初始化方法initWatch()中,调用了实例方法的vue.$watch,$watch里会new一个Watcher用于监听,在Watcher的构造方法里,通过调用this.get()会触发到this.num的getter(),getter()里会把这儿new出来的Watcher实例当成依赖收集起来。只要this.num的setter()被触发,就会通过dep.notify()触发到Watcher实例的回调方法。

9

大概就是这样子,watch功能就实现了。这个过程通过堆栈可看出来。

8

总结

  1. vm.$data里的数据通过initData()更新,把每个数据转换成可观察对象。
  2. vm.computed里的数据通过defineComputed()方法重写getter方法,getter方法内部进行依赖收集,依赖更新时,不实时更新vm.computed,而是设置Watcher实例的dirty字段,只有在真正读取vm.computed值时,才进行重新计算。
  3. vm.watch里的数据调用vm.$watch,收集依赖,依赖更新值时,触发watch数据定义的方法。