Vue源碼閱讀(四): Vue的異步更新隊列

關於數據響應化,問一個常見的問題:html

下面示例代碼中的兩個輸出console.log(p1.innerHTML),分別是什麼?爲何?html5

<!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 麼?那顯然是不合理的。批量、異步的操做才更優雅。express

咱們想要去源碼看看,Vue 更新 DOM 的批量與異步操做,究竟是如何作的呢?bash

首先界定一個界限:咱們不會立馬深刻到虛擬 DOM 的生成與頁面更新的 patch 算法中去,只是想要看看這個批量與異步的過程,解決剛剛提到的問題。app

源碼

從以前的筆記內容可知:數據響應的核心方法defineReactive()中,當數據發生變化的時候,會調用Dep.notify()方法,通知對應的Watcher執行updateComponent()操做,繼而從新渲染執行更新頁面。異步

讓咱們從Dep的notify()方法提及。async

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呢?oop

帶着這個問題,查看同一文件夾下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.im/post/5b498d…

jakearchibald.com/2015/tasks-…

相關文章
相關標籤/搜索