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

784 阅读3分钟

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

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

<!DOCTYPE html>
<html>
<body>
    <div id="demo">
        <h1>异步更新</h1>
        <p id="p1">{{foo}}</p>
    </div>
    <script>
        const app = new Vue({
            el: '#demo',
            data: { foo: '' },
            mounted() {
                setInterval(() => {                    
                    this.foo = 't1'
                    this.foo = 't2'
                    this.foo = 't3'
                    console.log(p1.innerHTML) //此时,页面的展示值?
                    this.$nextTick(() => {
                        console.log(p1.innerHTML) //此时,页面的展示值?
                    })
                }, 1000);
            }
        });
    </script>
</body>
</html>

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

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

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

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

源码

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

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

export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<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()
    }
  }
}

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

import { queueWatcher } from './scheduler'

export default class Watcher {

  ...//省略
  update () {
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {//如果是同步
      this.run()
    } else {
      queueWatcher(this) //Watcher的入队操作
    }
  }

   //实际执行的更新方法,会被scheduler调用
  run () {
      if (this.active) {
      //this.get()是挂载时传入的updateComponent()方法
      const value = this.get()
      //如果是组件的Watcher,不会有返回值value,不会执行下一步
      //只有用户自定义Watcher才会进入if
      if (
        value !== this.value ||
        isObject(value) ||
        this.deep
      ) {
        const oldValue = this.value
        this.value = value
        if (this.user) {
          try {
            this.cb.call(this.vm, value, oldValue)
          } catch (e) {
            handleError(e, this.vm, `callback for watcher "${this.expression}"`)
          }
        } else {
          this.cb.call(this.vm, value, oldValue)
        }
      }
    }
  }

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

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

export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  //去重
  if (has[id] == null) {
    has[id] = true
    if (!flushing) {
      queue.push(watcher)
    } else {
      let i = queue.length - 1
      while (i > index && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(i + 1, 0, watcher)
    }
    if (!waiting) {
      waiting = true

      if (process.env.NODE_ENV !== 'production' && !config.async) {
        flushSchedulerQueue()
        return
      }
      //异步刷新队列
      nextTick(flushSchedulerQueue)
    }
  }
}

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

看了这个批量更新的操作,有人会问:多次数据响应化,只有第一次更新的 Watcher 才会进入队列,是不是意味着只有第一次的数据响应化才生效,而后几次的数据响应化无效了呢?

回答:并不是这样的,数据响应化一直都在进行,变化的数据也一直在变。需要明确其和批量更新队列之间的关联,发生在 Watcher 的 run() 方法上。当执行 run() 方法的时候,其获取的 data 是最新的 data。

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


export let isUsingMicroTask = false

const callbacks = []
let pending = false

function flushCallbacks () {
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}

//关于timerFunc的选取过程
let timerFunc

//优先选择Promise,因为Promise是基于微任务的
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
  
//次优选择MutationObserver,MutationObserver也是基于微任务的
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  let counter = 1
  const observer = new MutationObserver(flushCallbacks)
  const textNode = document.createTextNode(String(counter))
  observer.observe(textNode, {
    characterData: true
  })
  timerFunc = () => {
    counter = (counter + 1) % 2
    textNode.data = String(counter)
  }
  isUsingMicroTask = true
  
//如果以上两者都不行,那么选择setImmediate(),它是基于宏任务的
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  // 最无奈的选择,选择setTimeout
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

//nextTick: 按照特定异步策略timerFunc() 执行队列操作
export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  if (!pending) {
    pending = true
    timerFunc()
  }
}

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

juejin.cn/post/684490…

jakearchibald.com/2015/tasks-…


vue源码解读文章目录:

(一):Vue构造函数与初始化过程

(二):数据响应式与实现

(三):数组的响应式处理

(四):Vue的异步更新队列

(五):虚拟DOM的引入

(六):数据更新算法--patch算法

(七):组件化机制的实现

(八):计算属性与侦听属性

(九):编译过程的 optimize 阶段

Vue 更多系列:

Vue的错误处理机制

以手写代码的方式解析 Vue 的工作过程

Vue Router的手写实现