[关闭]
@frank-shaw 2019-11-30T10:18:07.000000Z 字数 4004 阅读 1341

Vue源码阅读(四): Vue的异步更新队列

vue 源码 异步更新


关于数据响应化,问一个常见的问题:

下面示例代码中的两个输出console.log(p1.innerHTML),分别是什么?为什么?

  1. <!DOCTYPE html>
  2. <html>
  3. <body>
  4. <div id="demo">
  5. <h1>异步更新</h1>
  6. <p id="p1">{{foo}}</p>
  7. </div>
  8. <script>
  9. const app = new Vue({
  10. el: '#demo',
  11. data: { foo: '' },
  12. mounted() {
  13. setInterval(() => {
  14. this.foo = 't1'
  15. this.foo = 't2'
  16. this.foo = 't3'
  17. console.log(p1.innerHTML) //此时,页面的展示值?
  18. this.$nextTick(() => {
  19. console.log(p1.innerHTML) //此时,页面的展示值?
  20. })
  21. }, 1000);
  22. }
  23. });
  24. </script>
  25. </body>
  26. </html>

这个问题的第一问“是什么”,并不复杂。难的是"为什么"。该问题的本质涉及到Vue的异步更新问题。

首先,需要明确的是:Vue的更新DOM的操作是异步的,批量的。之所以这么做的缘由也很简单:更新DOM的操作是昂贵的,消耗较大。如上面的展示例子所示,Vue内部会连续更新三次DOM么?那显然是不合理的。批量、异步的操作才更优雅。

我们想要去源码看看,Vue更新DOM的批量与异步操作,到底是如何做的呢?

首先界定一个界限:我们不会立马深入到虚拟DOM的生成与页面更新的patch算法中去,只是想要看看这个批量与异步的过程,解决刚刚提到的问题。

源码

从之前的笔记内容可知:数据响应的核心方法defineReactive()中,当数据发生变化的时候,会调用Dep.notify()方法,通知对应的Watcher执行updateComponent()操作,继而重新渲染执行更新页面。

让我们从Dep的notify()方法说起。

  1. export default class Dep {
  2. static target: ?Watcher;
  3. id: number;
  4. subs: Array<Watcher>;
  5. ...//省略
  6. notify () {
  7. // stabilize the subscriber list first
  8. const subs = this.subs.slice()
  9. for (let i = 0, l = subs.length; i < l; i++) {
  10. subs[i].update()
  11. }
  12. }
  13. }

可知,其内部是执行的是相关联的Watcher的update()方法。

  1. import { queueWatcher } from './scheduler'
  2. export default class Watcher {
  3. ...//省略
  4. update () {
  5. if (this.lazy) {
  6. this.dirty = true
  7. } else if (this.sync) {//如果是同步
  8. this.run()
  9. } else {
  10. queueWatcher(this) //Watcher的入队操作
  11. }
  12. }
  13. //实际执行的更新方法,会被scheduler调用
  14. run () {
  15. if (this.active) {
  16. //this.get()是挂载时传入的updateComponent()方法
  17. const value = this.get()
  18. //如果是组件的Watcher,不会有返回值value,不会执行下一步
  19. //只有用户自定义Watcher才会进入if
  20. if (
  21. value !== this.value ||
  22. isObject(value) ||
  23. this.deep
  24. ) {
  25. const oldValue = this.value
  26. this.value = value
  27. if (this.user) {
  28. try {
  29. this.cb.call(this.vm, value, oldValue)
  30. } catch (e) {
  31. handleError(e, this.vm, `callback for watcher "${this.expression}"`)
  32. }
  33. } else {
  34. this.cb.call(this.vm, value, oldValue)
  35. }
  36. }
  37. }
  38. }

看到这里,提问一哈:如果在同一时刻,组件实例中的data修改了多次,其对应的Watcher也会执行queueWatcher(this)多次,那么是否会在当前队列中存在多个同样的Watcher呢?

带着这个问题,查看同一文件夹下schedule.jsqueueWatcher()方法:

  1. export function queueWatcher (watcher: Watcher) {
  2. const id = watcher.id
  3. //去重
  4. if (has[id] == null) {
  5. has[id] = true
  6. if (!flushing) {
  7. queue.push(watcher)
  8. } else {
  9. let i = queue.length - 1
  10. while (i > index && queue[i].id > watcher.id) {
  11. i--
  12. }
  13. queue.splice(i + 1, 0, watcher)
  14. }
  15. if (!waiting) {
  16. waiting = true
  17. if (process.env.NODE_ENV !== 'production' && !config.async) {
  18. flushSchedulerQueue()
  19. return
  20. }
  21. //异步刷新队列
  22. nextTick(flushSchedulerQueue)
  23. }
  24. }
  25. }

代码中看到:每个Watcher都会有一个id标识,只有全新的Watcher才会入队。批量的过程我们看到了,将是将Watcher放入到队列里面去,然后批量操作更新。

看了这个批量更新的操作,有人会问:多次数据响应化,只有第一次更新的Watcher才会进入队列,是不是意味着只有第一次的数据响应化才生效,而后几次的数据响应化无效了呢?
回答:并不是这样的,数据响应化一直都在进行,变化的数据也一直在变。需要明确其和批量更新队列之间的关联,发生在Watcher的run()方法上。当执行run()方法的时候,其获取的data是最新的data。

讲了批量,那么异步的过程是怎样的呢?让我们来看看nextTick()函数内部,了解一些关于异步操作的知识点:

  1. export let isUsingMicroTask = false
  2. const callbacks = []
  3. let pending = false
  4. function flushCallbacks () {
  5. pending = false
  6. const copies = callbacks.slice(0)
  7. callbacks.length = 0
  8. for (let i = 0; i < copies.length; i++) {
  9. copies[i]()
  10. }
  11. }
  12. //关于timerFunc的选取过程
  13. let timerFunc
  14. //优先选择Promise,因为Promise是基于微任务的
  15. if (typeof Promise !== 'undefined' && isNative(Promise)) {
  16. const p = Promise.resolve()
  17. timerFunc = () => {
  18. p.then(flushCallbacks)
  19. if (isIOS) setTimeout(noop)
  20. }
  21. isUsingMicroTask = true
  22. //次优选择MutationObserver,MutationObserver也是基于微任务的
  23. } else if (!isIE && typeof MutationObserver !== 'undefined' && (
  24. isNative(MutationObserver) ||
  25. MutationObserver.toString() === '[object MutationObserverConstructor]'
  26. )) {
  27. let counter = 1
  28. const observer = new MutationObserver(flushCallbacks)
  29. const textNode = document.createTextNode(String(counter))
  30. observer.observe(textNode, {
  31. characterData: true
  32. })
  33. timerFunc = () => {
  34. counter = (counter + 1) % 2
  35. textNode.data = String(counter)
  36. }
  37. isUsingMicroTask = true
  38. //如果以上两者都不行,那么选择setImmediate(),它是基于宏任务的
  39. } else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  40. timerFunc = () => {
  41. setImmediate(flushCallbacks)
  42. }
  43. } else {
  44. // 最无奈的选择,选择setTimeout
  45. timerFunc = () => {
  46. setTimeout(flushCallbacks, 0)
  47. }
  48. }
  49. //nextTick: 按照特定异步策略timerFunc() 执行队列操作
  50. export function nextTick (cb?: Function, ctx?: Object) {
  51. let _resolve
  52. callbacks.push(() => {
  53. if (cb) {
  54. try {
  55. cb.call(ctx)
  56. } catch (e) {
  57. handleError(e, ctx, 'nextTick')
  58. }
  59. } else if (_resolve) {
  60. _resolve(ctx)
  61. }
  62. })
  63. if (!pending) {
  64. pending = true
  65. timerFunc()
  66. }
  67. }

关于宏任务与微任务,可以查看更多有意思的页面:

https://juejin.im/post/5b498d245188251b193d4059
https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/?utm_source=html5weekly

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注