• 派发更新
    • 过程分析
    • 总结

    派发更新

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

    我们先来回顾一下 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