派发更新

通过上一节分析我们了解了响应式数据依赖收集过程,收集的目的就是为了当我们修改数据的时候,可以对相关的依赖派发更新,那么这一节我们来详细分析这个过程。

我们先来回顾一下 setter 部分的逻辑:

  1. /**
  2. * Define a reactive property on an Object.
  3. */
  4. export function defineReactive (
  5. obj: Object,
  6. key: string,
  7. val: any,
  8. customSetter?: ?Function,
  9. shallow?: boolean
  10. ) {
  11. const dep = new Dep()
  12. const property = Object.getOwnPropertyDescriptor(obj, key)
  13. if (property && property.configurable === false) {
  14. return
  15. }
  16. // cater for pre-defined getter/setters
  17. const getter = property && property.get
  18. const setter = property && property.set
  19. if ((!getter || setter) && arguments.length === 2) {
  20. val = obj[key]
  21. }
  22. let childOb = !shallow && observe(val)
  23. Object.defineProperty(obj, key, {
  24. enumerable: true,
  25. configurable: true,
  26. // ...
  27. set: function reactiveSetter (newVal) {
  28. const value = getter ? getter.call(obj) : val
  29. /* eslint-disable no-self-compare */
  30. if (newVal === value || (newVal !== newVal && value !== value)) {
  31. return
  32. }
  33. /* eslint-enable no-self-compare */
  34. if (process.env.NODE_ENV !== 'production' && customSetter) {
  35. customSetter()
  36. }
  37. if (setter) {
  38. setter.call(obj, newVal)
  39. } else {
  40. val = newVal
  41. }
  42. childOb = !shallow && observe(newVal)
  43. dep.notify()
  44. }
  45. })
  46. }

setter 的逻辑有 2 个关键的点,一个是 childOb = !shallow && observe(newVal),如果 shallow 为 false 的情况,会对新设置的值变成一个响应式对象;另一个是 dep.notify(),通知所有的订阅者,这是本节的关键,接下来我会带大家完整的分析整个派发更新的过程。

过程分析

当我们在组件中对响应的数据做了修改,就会触发 setter 的逻辑,最后调用 dep.notify() 方法,它是 Dep 的一个实例方法,定义在 src/core/observer/dep.js 中:

  1. class Dep {
  2. // ...
  3. notify () {
  4. // stabilize the subscriber list first
  5. const subs = this.subs.slice()
  6. for (let i = 0, l = subs.length; i < l; i++) {
  7. subs[i].update()
  8. }
  9. }
  10. }

这里的逻辑非常简单,遍历所有的 subs,也就是 Watcher 的实例数组,然后调用每一个 watcherupdate 方法,它的定义在 src/core/observer/watcher.js 中:

  1. class Watcher {
  2. // ...
  3. update () {
  4. /* istanbul ignore else */
  5. if (this.computed) {
  6. // A computed property watcher has two modes: lazy and activated.
  7. // It initializes as lazy by default, and only becomes activated when
  8. // it is depended on by at least one subscriber, which is typically
  9. // another computed property or a component's render function.
  10. if (this.dep.subs.length === 0) {
  11. // In lazy mode, we don't want to perform computations until necessary,
  12. // so we simply mark the watcher as dirty. The actual computation is
  13. // performed just-in-time in this.evaluate() when the computed property
  14. // is accessed.
  15. this.dirty = true
  16. } else {
  17. // In activated mode, we want to proactively perform the computation
  18. // but only notify our subscribers when the value has indeed changed.
  19. this.getAndInvoke(() => {
  20. this.dep.notify()
  21. })
  22. }
  23. } else if (this.sync) {
  24. this.run()
  25. } else {
  26. queueWatcher(this)
  27. }
  28. }
  29. }

这里对于 Watcher 的不同状态,会执行不同的逻辑,computedsync 等状态的分析我会之后抽一小节详细介绍,在一般组件数据更新的场景,会走到最后一个 queueWatcher(this) 的逻辑,queueWatcher 的定义在 src/core/observer/scheduler.js 中:

  1. const queue: Array<Watcher> = []
  2. let has: { [key: number]: ?true } = {}
  3. let waiting = false
  4. let flushing = false
  5. /**
  6. * Push a watcher into the watcher queue.
  7. * Jobs with duplicate IDs will be skipped unless it's
  8. * pushed when the queue is being flushed.
  9. */
  10. export function queueWatcher (watcher: Watcher) {
  11. const id = watcher.id
  12. if (has[id] == null) {
  13. has[id] = true
  14. if (!flushing) {
  15. queue.push(watcher)
  16. } else {
  17. // if already flushing, splice the watcher based on its id
  18. // if already past its id, it will be run next immediately.
  19. let i = queue.length - 1
  20. while (i > index && queue[i].id > watcher.id) {
  21. i--
  22. }
  23. queue.splice(i + 1, 0, watcher)
  24. }
  25. // queue the flush
  26. if (!waiting) {
  27. waiting = true
  28. nextTick(flushSchedulerQueue)
  29. }
  30. }
  31. }

这里引入了一个队列的概念,这也是 Vue 在做派发更新的时候的一个优化的点,它并不会每次数据改变都触发 watcher 的回调,而是把这些 watcher 先添加到一个队列里,然后在 nextTick 后执行 flushSchedulerQueue

这里有几个细节要注意一下,首先用 has 对象保证同一个 Watcher 只添加一次;接着对 flushing 的判断,else 部分的逻辑稍后我会讲;最后通过 wating 保证对 nextTick(flushSchedulerQueue) 的调用逻辑只有一次,另外 nextTick 的实现我之后会抽一小节专门去讲,目前就可以理解它是在下一个 tick,也就是异步的去执行 flushSchedulerQueue

接下来我们来看 flushSchedulerQueue 的实现,它的定义在 src/core/observer/scheduler.js 中。

  1. let flushing = false
  2. let index = 0
  3. /**
  4. * Flush both queues and run the watchers.
  5. */
  6. function flushSchedulerQueue () {
  7. flushing = true
  8. let watcher, id
  9. // Sort queue before flush.
  10. // This ensures that:
  11. // 1. Components are updated from parent to child. (because parent is always
  12. // created before the child)
  13. // 2. A component's user watchers are run before its render watcher (because
  14. // user watchers are created before the render watcher)
  15. // 3. If a component is destroyed during a parent component's watcher run,
  16. // its watchers can be skipped.
  17. queue.sort((a, b) => a.id - b.id)
  18. // do not cache length because more watchers might be pushed
  19. // as we run existing watchers
  20. for (index = 0; index < queue.length; index++) {
  21. watcher = queue[index]
  22. if (watcher.before) {
  23. watcher.before()
  24. }
  25. id = watcher.id
  26. has[id] = null
  27. watcher.run()
  28. // in dev build, check and stop circular updates.
  29. if (process.env.NODE_ENV !== 'production' && has[id] != null) {
  30. circular[id] = (circular[id] || 0) + 1
  31. if (circular[id] > MAX_UPDATE_COUNT) {
  32. warn(
  33. 'You may have an infinite update loop ' + (
  34. watcher.user
  35. ? `in watcher with expression "${watcher.expression}"`
  36. : `in a component render function.`
  37. ),
  38. watcher.vm
  39. )
  40. break
  41. }
  42. }
  43. }
  44. // keep copies of post queues before resetting state
  45. const activatedQueue = activatedChildren.slice()
  46. const updatedQueue = queue.slice()
  47. resetSchedulerState()
  48. // call component updated and activated hooks
  49. callActivatedHooks(activatedQueue)
  50. callUpdatedHooks(updatedQueue)
  51. // devtool hook
  52. /* istanbul ignore if */
  53. if (devtools && config.devtools) {
  54. devtools.emit('flush')
  55. }
  56. }

这里有几个重要的逻辑要梳理一下,对于一些分支逻辑如 keep-alive 组件相关和之前提到过的 updated 钩子函数的执行会略过。

  • 队列排序
    queue.sort((a, b) => a.id - b.id) 对队列做了从小到大的排序,这么做主要有以下要确保以下几点:

1.组件的更新由父到子;因为父组件的创建过程是先于子的,所以 watcher 的创建也是先父后子,执行顺序也应该保持先父后子。

2.用户的自定义 watcher 要优先于渲染 watcher 执行;因为用户自定义 watcher 是在渲染 watcher 之前创建的。

3.如果一个组件在父组件的 watcher 执行期间被销毁,那么它对应的 watcher 执行都可以被跳过,所以父组件的 watcher 应该先执行。

  • 队列遍历
    在对 queue 排序后,接着就是要对它做遍历,拿到对应的 watcher,执行 watcher.run()。这里需要注意一个细节,在遍历的时候每次都会对 queue.length 求值,因为在 watcher.run() 的时候,很可能用户会再次添加新的 watcher,这样会再次执行到 queueWatcher,如下:
  1. export function queueWatcher (watcher: Watcher) {
  2. const id = watcher.id
  3. if (has[id] == null) {
  4. has[id] = true
  5. if (!flushing) {
  6. queue.push(watcher)
  7. } else {
  8. // if already flushing, splice the watcher based on its id
  9. // if already past its id, it will be run next immediately.
  10. let i = queue.length - 1
  11. while (i > index && queue[i].id > watcher.id) {
  12. i--
  13. }
  14. queue.splice(i + 1, 0, watcher)
  15. }
  16. // ...
  17. }
  18. }

可以看到,这时候 flushing 为 true,就会执行到 else 的逻辑,然后就会从后往前找,找到第一个待插入 watcher 的 id 比当前队列中 watcher 的 id 大的位置。把 watcher 按照 id的插入到队列中,因此 queue 的长度发送了变化。

  • 状态恢复
    这个过程就是执行 resetSchedulerState 函数,它的定义在 src/core/observer/scheduler.js 中。
  1. const queue: Array<Watcher> = []
  2. let has: { [key: number]: ?true } = {}
  3. let circular: { [key: number]: number } = {}
  4. let waiting = false
  5. let flushing = false
  6. let index = 0
  7. /**
  8. * Reset the scheduler's state.
  9. */
  10. function resetSchedulerState () {
  11. index = queue.length = activatedChildren.length = 0
  12. has = {}
  13. if (process.env.NODE_ENV !== 'production') {
  14. circular = {}
  15. }
  16. waiting = flushing = false
  17. }

逻辑非常简单,就是把这些控制流程状态的一些变量恢复到初始值,把 watcher 队列清空。

接下来我们继续分析 watcher.run() 的逻辑,它的定义在 src/core/observer/watcher.js 中。

  1. class Watcher {
  2. /**
  3. * Scheduler job interface.
  4. * Will be called by the scheduler.
  5. */
  6. run () {
  7. if (this.active) {
  8. this.getAndInvoke(this.cb)
  9. }
  10. }
  11. getAndInvoke (cb: Function) {
  12. const value = this.get()
  13. if (
  14. value !== this.value ||
  15. // Deep watchers and watchers on Object/Arrays should fire even
  16. // when the value is the same, because the value may
  17. // have mutated.
  18. isObject(value) ||
  19. this.deep
  20. ) {
  21. // set new value
  22. const oldValue = this.value
  23. this.value = value
  24. this.dirty = false
  25. if (this.user) {
  26. try {
  27. cb.call(this.vm, value, oldValue)
  28. } catch (e) {
  29. handleError(e, this.vm, `callback for watcher "${this.expression}"`)
  30. }
  31. } else {
  32. cb.call(this.vm, value, oldValue)
  33. }
  34. }
  35. }
  36. }

run 函数实际上就是执行 this.getAndInvoke 方法,并传入 watcher 的回调函数。getAndInvoke 函数逻辑也很简单,先通过 this.get() 得到它当前的值,然后做判断,如果满足新旧值不等、新值是对象类型、deep 模式任何一个条件,则执行 watcher 的回调,注意回调函数执行的时候会把第一个和第二个参数传入新值 value 和旧值 oldValue,这就是当我们添加自定义 watcher 的时候能在回调函数的参数中拿到新旧值的原因。

那么对于渲染 watcher 而言,它在执行 this.get() 方法求值的时候,会执行 getter 方法:

  1. updateComponent = () => {
  2. vm._update(vm._render(), hydrating)
  3. }

所以这就是当我们去修改组件相关的响应式数据的时候,会触发组件重新渲染的原因,接着就会重新执行 patch 的过程,但它和首次渲染有所不同,之后我们会花一小节去详细介绍。

总结

通过这一节的分析,我们对 Vue 数据修改派发更新的过程也有了认识,实际上就是当数据发生变化的时候,触发 setter 逻辑,把在依赖过程中订阅的的所有观察者,也就是 watcher,都触发它们的 update 过程,这个过程又利用了队列做了进一步优化,在 nextTick 后执行所有 watcherrun,最后执行它们的回调函数。nextTick 是 Vue 一个比较核心的实现了,下一节我们来重点分析它的实现。

原文: https://ustbhuangyi.github.io/vue-analysis/reactive/setters.html