Vue源碼解析03-異步更新隊列

Vue 源碼解析03-異步更新隊列

前言

這篇文章分析了Vue更新過程中使用的異步更新隊列的相關代碼。通過對異步更新隊列的研究和學習,加深對Vue更新機制的理解

什麼是異步更新隊列

先看看下面的例子:

    <div id="app">
        <div id="div" v-if="isShow">被隱藏的內容</div>
        <input @click="getDiv" value="按鈕" type="button">
    </div>
  <script>

    let vm = new Vue({
        el: '#app',
        data: {
        //控制是否顯示#div
        isShow: false
        },
        methods:{
        getDiv: function () {
            this.isShow=true
            var content = document.getElementById('div').innerHTML;
            console.log('content',content)
        }
        }
    })
</script>
  • 上面的例子是,點擊按鈕顯示被隱藏的div,同時打印div內部html的內容。
  • 按照我們一般的認知,應該是點擊按鈕能夠顯示div並且在控制檯看到div的內部html的內容。

但是實際執行的結果確是,div可以顯示出來,但是打印結果的時候會報錯,錯誤原因就是innerHTML爲null,也就是div不存在。
只有當我們再次點擊按鈕的時候纔會打印出div裏面的內容。這就是Vue的異步更新隊列的結果

異步更新隊列的概念

Vue的dom更新是異步的,當數據發生變化時Vue不是立刻去更新dom,而是開啓一個隊列,並緩衝在同一個事件中循環發生的所有數據變化。
在緩衝時,會去除重複的數據,避免多餘的計算和dom操作。在下一個事件循環tick中,刷新隊列並執行已去重的工作。

  • 所以上面的代碼報錯是因爲當執行this.isShow=true時,div還未被創建出來,知道下次Vue事件循環時纔開始創建

  • 查重機制降低了Vue的開銷

  • 異步更新隊列實現的選擇:由於瀏覽器的差異,Vue會根據當前環境選擇Promise.then或者MuMutationObserver,如果兩者都不支持,則會用setImmediate或者setTimeout代替

異步更新隊列解析

異步隊列源碼入口

通過之前對Vue數據響應式的分析我們知道,當Vue數據發生變化時,會觸發dep的notify()方法,該方法通知觀察者watcher去更新dom,我們先看一下這的源碼

  • from src/core/observer/dep.js
    //直接看核心代碼
    notify () {
        //這是Dep的notify方法,Vue的會對data數據進行數據劫持,該方法被放到data數據的set方法中最後執行
        //也就是通知更新操作
        // stabilize the subscriber list first
        const subs = this.subs.slice()
        if (process.env.NODE_ENV !== 'production' && !config.async) {
        subs.sort((a, b) => a.id - b.id)
        }
        for (let i = 0, l = subs.length; i < l; i++) {
        // !!!核心:通知watcher進行數據更新
        //這裏的subs[i]其實是Dep維護的一個watcher數組,所以我們下面是執行的watcher中的update方法
        subs[i].update()
        }
  }
  • 上面的代碼簡單來說就是dep通知watcher盡心更新操作,我們看一下watcher相關的代碼
    from :src/core/observer/watcher.js
    //這裏只展示部分核心代碼
    //watcher的update方法
    update () {
        /* istanbul ignore else */
        //判斷是否存在lazy和sync屬性
        if (this.lazy) {
            this.dirty = true
        } else if (this.sync) {
        this.run()
        } else {
        //核心:將當前的watcher放到一個隊列中
        queueWatcher(this)
        }
    }
  • 上面watcher的update更新方法簡單來說就是調用了一個queueWatcher方法,這個方法其實是將當前的watcher實例放入到一個隊列中,以便完成後面的異步更新隊列操作

異步隊列入隊

下面看看queueWatcher的邏輯 from src/core/observer/scheduler.js

    export function queueWatcher (watcher: Watcher) {
    const id = watcher.id
    //去重的操作,先判斷是否在當前隊列中存在,避免重複操作
    if (has[id] == null) {
        has[id] = true
        if (!flushing) {
        queue.push(watcher)
        } else {
        // if already flushing, splice the watcher based on its id
        // if already past its id, it will be run next immediately.
        let i = queue.length - 1
        while (i > index && queue[i].id > watcher.id) {
            i--
        }
        queue.splice(i + 1, 0, watcher)
        }
        // queue the flush
        if (!waiting) {
        waiting = true

        if (process.env.NODE_ENV !== 'production' && !config.async) {
            flushSchedulerQueue()
            return
        }
        // 啓動異步任務(刷新當前的計劃任務)
        nextTick(flushSchedulerQueue)
        }
    }
    }
  • 上面這段queueWatcher的代碼的主要作用就是對任務去重,然後啓動異步任務,進行跟新操作。接下來我們看一線nextTick裏面的操作

from src/core/util/next-tick.js

//cb:
export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  //callbacks:這個方法維護了一個回調函數的數組,將回調函數添家進數組
  callbacks.push(() => {
      //添加錯誤處理
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  if (!pending) {
    pending = true
    //啓動異步函數
    timerFunc()
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
  • 這裏的核心,其實就在timerFunc的函數上,該函數根據不同的運行時環境,調用不同的異步更新隊列,下面看一下代碼

from src/core/util/next-tick.js

    /**這部分邏輯就是根據環境來判斷timerFunc到底是使用什麼樣的異步隊列**/
    let timerFunc
    //首選微任務執行異步操作:Promise、MutationObserver
    //次選setImmediate最後選擇setTimeout
    // 根據當前瀏覽器環境選擇用什麼方法來執行異步任務
    if (typeof Promise !== 'undefined' && isNative(Promise)) {
        //如果當前環境支持Promise,則使用Promise執行異步任務
        const p = Promise.resolve()
        timerFunc = () => {
            //最終是執行的flushCallbacks方法
            p.then(flushCallbacks)
            //如果是IOS則回退,因爲IOS不支持Promise
            if (isIOS) setTimeout(noop)
        }
        //當前使用微任務執行
        isUsingMicroTask = true
    } else if (!isIE && typeof MutationObserver !== 'undefined' && (
        //如果當前瀏覽器支持MutationObserver則使用MutationObserver
        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
    } else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
        //如果支持setImmediate,則使用setImmediate
        timerFunc = () => {
            setImmediate(flushCallbacks)
        }
    } else {
        //如果上面的條件都不滿足,那麼最後選擇setTimeout方法來完成異步更新隊列
        timerFunc = () => {
            setTimeout(flushCallbacks, 0)
        }
    }
  • 從上面代碼可以看出,不論timerFunc使用的是什麼樣的異步更新隊列,最終執行的函數還是落在了flushCallbacks上面,那麼我們來看一看,這個方法到底是什麼

from src/core/util/next-tick.js

    function flushCallbacks () {
        pending = false
        //拷貝callbacks數組內容
        const copies = callbacks.slice(0)
        //清空callbacks
        callbacks.length = 0
        //遍歷執行
        for (let i = 0; i < copies.length; i++) {
            //執行回調方法
            copies[i]()
        }
    }
  • 上面的這個方法就是遍歷執行了我們nextTick維護的那個回調函數數組,其實就是將數組的方法依次添加進異步隊列進行執行。同時清空callbacks數組爲下次更新作準備。

上面這幾段代碼其實都是watcher的異步隊列更新中的入隊操作,通過queueWatcher方法中調用的nextTick(flushSchedulerQueue),我們知道,其實是將flushSchedulerQueue這個方法入隊

異步隊列的具體更新方法

所以下面我們看一下flushSchedulerQueue這個方法到底執行了什麼操作

from src/core/observer/scheduler.js

    /**我們這裏只粘貼跟本次異步隊列更新相關的核心代碼**/
    //具體的更新操作
function flushSchedulerQueue () {
    currentFlushTimestamp = getNow()
    flushing = true
    let watcher, id
    //重新排列queue數組,是爲了確保:
    //更新順序是從父組件到子組件
    //用戶的watcher先於render 的watcher執行(因爲用戶watcher先於render watcher創建)
    //當子組件的watcher在父組件的watcher執行時被銷燬,則跳過該子組件的watcher
    queue.sort((a, b) => a.id - b.id)
    //queue數組維護的一個watcher數組
    //遍歷queue數組,在queueWatcher方法中我們將傳入的watcher實例push到了該數組中
    for (index = 0; index < queue.length; index++) {
        watcher = queue[index]
        if (watcher.before) {
            watcher.before()
        }
        id = watcher.id
        //清空has對象裏面的"id"屬性(這個id屬性之前在queueWatcher方法裏面查重的時候用到了)
        has[id] = null
        //核心:最終執行的其實是watcher的run方法
        watcher.run()
        //下面是一些警告提示,可以先忽略
        if (process.env.NODE_ENV !== 'production' && has[id] != null) {
            circular[id] = (circular[id] || 0) + 1
            if (circular[id] > MAX_UPDATE_COUNT) {
                warn(
                    'You may have an infinite update loop ' + (
                        watcher.user
                        ? `in watcher with expression "${watcher.expression}"`
                        : `in a component render function.`
                    ),
                    watcher.vm
                )
                break
            }
        }
    }
    //調用組件updated生命週期鉤子相關,先跳過
    const activatedQueue = activatedChildren.slice()
    const updatedQueue = queue.slice()

    resetSchedulerState()

    callActivatedHooks(activatedQueue)
    callUpdatedHooks(updatedQueue)
        if (devtools && config.devtools) {
            devtools.emit('flush')
        }
}
  • 上面的一堆 flushSchedulerQueue 代碼,簡單來說就是排列了queue數組,然後遍歷該數組,執行watcher.run方法。所以,異步隊列更新當我們入隊完以後,真正執行的方法其實是watcher.run方法

下面我們來繼續看一下watcher.run方法,到底執行了什麼操作

from src/core/observer/watcher.js

    /**
   * Scheduler job interface.
   * Will be called by the scheduler.
   * 上面這段英文註釋 是官方註釋,從這我們看出該方法最終會被scheduler調用
   */
  run () {
    if (this.active) {
        //這裏調用了watcher的get方法
      const value = this.get()
      if (
        value !== this.value ||
        // Deep watchers and watchers on Object/Arrays should fire even
        // when the value is the same, because the value may
        // have mutated.
        isObject(value) ||
        this.deep
      ) {
        // set new value
        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)
        }
      }
    }
  }
  • 上述run方法最終要的操作就是調用了watcher的get方法,該方法我們在之前的源碼分析有講過,主要實現的功能是調用了data數據的get方法,獲取最新數據。

至此,Vue異步更新隊列的核心代碼我們就分析完了,爲了便於理清思路,我們來一張圖總結一下

關於Vue.$nextTick

我們都知道.nextTick.nextTick方法,其實這個 **nextTick** 方法就是直接調用的上面的nextTick方法

from src/core/instance/render.js

  Vue.prototype.$nextTick = function (fn: Function) {
    return nextTick(fn, this)
  }
  • 由上面的代碼我們可以看出,$nextTick是將我們傳入的回調函數加入到了異步更新隊列,所以它才能實現dom更新後回調

注意,$nextTick()是會將我們傳入的函數加入到異步更新隊列中的,但是這裏有個問題,如果我們想獲得dom更新後的數據,我們應該把該邏輯放到更新操作之後
因爲加入異步隊列先後的問題,如果我們在更新數據之前入隊的話 ,是獲取不到更新之後的數據的

總結

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-CSh0tuHO-1573463374607)(https://static.123drm.com/front/drm/courses_wx/userside/ceshi/Vue異步隊列更新.jpg)]

總結起來就是,當觸發數據更新通知時,dep通知watcher進行數據更新,這時watcher會將自己加入到一個異步的更新隊列中。然後更新隊列會將傳入的更新操作進行批量處理。
這樣就達到了多次更新同時完成,提升了用戶體驗,減少了瀏覽器的開銷,增強了性能。

發佈了18 篇原創文章 · 獲贊 2 · 訪問量 3503
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章