<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          1000字帶你掌握nextTick背后的原理

          共 13352字,需瀏覽 27分鐘

           ·

          2021-06-03 15:48


          一、引言

          在開發(fā)過程中,我們經(jīng)常遇到這樣的問題:我明明已經(jīng)更新了數(shù)據(jù),為什么當我獲取某個節(jié)點的數(shù)據(jù)時,卻還是更新前的數(shù)據(jù)?在視圖更新之后,怎么基于新的視圖進行操作?

          舉一個簡單的場景:

          <template>
              <div>
                  <p ref="message">{{ msg }}</p>
                  <button @click="handleClick">updateMsg</button>
              </div>
          </template>
          <script>
          export default {
              name: 'index',
              data () {
                  return {
                      msg: 'hello'
                  }
              },
              methods: {
                  handleClick () {
                      this.msg = 'hello world';
                      console.log(this.$refs.message.innerText); // hello
                  }
              }
          }
          </script>

          運行上面代碼,可以看到,修改數(shù)據(jù)后并不會立即更新dom,dom的更新是異步的,無法通過同步代碼獲取。雖然此時this.msg已經(jīng)變了 但是dom節(jié)點的值沒有更新,也就是說,變的只是數(shù)據(jù),而視圖節(jié)點的值未更新。所以當這時去獲取節(jié)點的this.$refs.message.innerText時,拿到的還是原來的數(shù)據(jù)。那問題來了,我啥時候才能拿到更新的數(shù)據(jù)呢?????????

          答:如果我們需要獲取數(shù)據(jù)更新后的dom信息,比如動態(tài)獲取dom的寬高、位置等,就需要使用nextTick

          handleClick () {
              this.msg = 'hello world';
              this.$nextTick(() => {
                  console.log(this.$refs.message.innerText) // hello world
              })
          }


          如vue官網(wǎng)的描述:

          Vue 在更新 DOM 時是異步執(zhí)行的。只要偵聽到數(shù)據(jù)變化,Vue 將開啟一個隊列,并緩沖在同一事件循環(huán)中發(fā)生的所有數(shù)據(jù)變更。如果同一個 watcher 被多次觸發(fā),只會被推入到隊列中一次。這種在緩沖時去除重復(fù)數(shù)據(jù)對于避免不必要的計算和 DOM 操作是非常重要的。然后,在下一個的事件循環(huán)“tick”中,Vue 刷新隊列并執(zhí)行實際 (已去重的) 工作。Vue 在內(nèi)部對異步隊列嘗試使用原生的 Promise.thenMutationObserversetImmediate,如果執(zhí)行環(huán)境不支持,則會采用 setTimeout(fn, 0)代替。

          以上出現(xiàn)了事件循環(huán)的概念,其涉及到JS的運行機制,包括主線程的執(zhí)行棧異步隊列異步API事件循環(huán)的協(xié)作,我們接下來先簡單了解一下 JS 的運行機制。

          二、JS 運行機制

          JS 執(zhí)行是單線程的,它是基于事件循環(huán)的。事件循環(huán)大致分為以下幾個步驟:

          • 所有同步任務(wù)都在主線程上執(zhí)行,形成一個執(zhí)行棧(execution context stack)。
          • 主線程之外,還存在一個"任務(wù)隊列"(task queue)。只要異步任務(wù)有了運行結(jié)果,就在"任務(wù)隊列"之中放置一個事件。
          • 一旦"執(zhí)行棧"中的所有同步任務(wù)執(zhí)行完畢,系統(tǒng)就會讀取"任務(wù)隊列",看看里面有哪些事件。那些對應(yīng)的異步任務(wù),于是結(jié)束等待狀態(tài),進入執(zhí)行棧,開始執(zhí)行。
          • 主線程不斷重復(fù)上面的第三步。

          主線程的執(zhí)行過程就是一個 tick,而所有的異步結(jié)果都是通過 “任務(wù)隊列” 來調(diào)度。消息隊列中存放的是一個個的任務(wù)(task)。規(guī)范中規(guī)定 task 分為兩大類,分別是 macro taskmicro task,并且每個 macro task 結(jié)束后,都要清空所有的 micro task。執(zhí)行順序如下:

          for (macroTask of macroTaskQueue) {
              // 1. Handle current MACRO-TASK
              handleMacroTask();
                
              // 2. Handle all MICRO-TASK
              for (microTask of microTaskQueue) {
                  handleMicroTask(microTask);
              }
          }

          接下來,我們來了解一下macro taskmicro task 的重要概念。

          2.1 macro task

          宏任務(wù),稱為task

          • macro task作用是為了讓瀏覽器能夠從內(nèi)部獲取javascript / dom的內(nèi)容并確保執(zhí)行棧能夠順序進行。
          • macro task調(diào)度是隨處可見的,例如解析HTML,獲得鼠標點擊的事件回調(diào)等等。
          2.2 micro task

          微任務(wù),也稱job

          • micro task通常用于在當前正在執(zhí)行的腳本之后直接發(fā)生的事情,比如對一系列的行為做出反應(yīng),或者做出一些異步的任務(wù),而不需要新建一個全新的task。
          • 只要執(zhí)行棧沒有其他javascript在執(zhí)行,在每個task結(jié)束時,micro task隊列就會在回調(diào)后處理。在micro task期間排隊的任何其他micro task將被添加到這個隊列的末尾并進行處理。

          在瀏覽器環(huán)境中,常見的 macro task 有 setTimeout、MessageChannel、postMessage、setImmediate;常見的 micro task 有 MutationObsever 和 Promise.then。

          根據(jù) HTML Standard,在每個 task 運行完以后,UI 都會重渲染,那么在 micro task 中就完成數(shù)據(jù)更新,當前 task 結(jié)束就可以得到最新的 UI 了。反之如果新建一個 task 來做數(shù)據(jù)更新,那么渲染就會進行兩次。

          micro task的這一特性是做隊列控制的最佳選擇,vue進行DOM更新內(nèi)部也是調(diào)用nextTick來做異步隊列控制。而當我們自己調(diào)用nextTick的時候,它就在更新DOM的那個micro task后追加了我們自己的回調(diào)函數(shù),從而確保我們的代碼在DOM更新后執(zhí)行。

          比如一段時間內(nèi),你無意中修改了最初代碼片段中的 msg多次,其實只要最后一次修改后的值更新到DOM就可以了,假如是同步更新的,每次 msg 值發(fā)生變化,那么都要觸發(fā) setter->Dep->Watcher->update->patch ,這個過程非常消耗性能。

          接下來我們就從源碼分析vue中nextTick的實現(xiàn)。

          三、nextTick源碼解析及原理

          /* @flow */
          /* globals MutationObserver */

          import { noop } from 'shared/util'
          import { handleError } from './error'
          import { isIE, isIOS, isNative } from './env'

          export let isUsingMicroTask = false

          const callbacks = []
          let pending = false

          /**
           * 對所有callback進行遍歷,然后指向響應(yīng)的回調(diào)函數(shù)
           * 使用 callbacks 保證了可以在同一個tick內(nèi)執(zhí)行多次 nextTick,不會開啟多個異步任務(wù),而把這些異步任務(wù)都壓成一個同步任務(wù),在下一個 tick 執(zhí)行完畢。
          */

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

          // Here we have async deferring wrappers using microtasks.
          // In 2.5 we used (macro) tasks (in combination with microtasks).
          // However, it has subtle problems when state is changed right before repaint
          // (e.g. #6813, out-in transitions).
          // Also, using (macro) tasks in event handler would cause some weird behaviors
          // that cannot be circumvented (e.g. #7109, #7153, #7546, #7834, #8109).
          // So we now use microtasks everywhere, again.
          // A major drawback of this tradeoff is that there are some scenarios
          // where microtasks have too high a priority and fire in between supposedly
          // sequential events (e.g. #4521, #6690, which have workarounds)
          // or even between bubbling of the same event (#6566).

          let timerFunc

          // The nextTick behavior leverages the microtask queue, which can be accessed
          // via either native Promise.then or MutationObserver.
          // MutationObserver has wider support, however it is seriously bugged in
          // UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It
          // completely stops working after triggering a few times... so, if native
          // Promise is available, we will use it:
          /* istanbul ignore next, $flow-disable-line */
          /**
          * timerFunc 實現(xiàn)的就是根據(jù)當前環(huán)境判斷使用哪種方式實現(xiàn)
          * 就是按照 Promise.then和 MutationObserver以及setImmediate的優(yōu)先級來判斷,支持哪個就用哪個,如果執(zhí)行環(huán)境不支持,會采用setTimeout(fn, 0)代替;
          */

          // 判斷是否支持原生 Promise
          if (typeof Promise !== 'undefined' && isNative(Promise)) {
            const p = Promise.resolve()
            timerFunc = () => {
              p.then(flushCallbacks)
              // In problematic UIWebViews, Promise.then doesn't completely break, but
              // it can get stuck in a weird state where callbacks are pushed into the
              // microtask queue but the queue isn'
          t being flushed, until the browser
              // needs to do some other work, e.g. handle a timer. Therefore we can
              // "force" the microtask queue to be flushed by adding an empty timer.
              if (isIOS) setTimeout(noop)
            }
            isUsingMicroTask = true
            // 不支持 Promise的話,再判斷是否原生支持 MutationObserver
          else if (!isIE && typeof MutationObserver !== 'undefined' && (
            isNative(MutationObserver) ||
            // PhantomJS and iOS 7.x
            MutationObserver.toString() === '[object MutationObserverConstructor]'
          )) {
            // Use MutationObserver where native Promise is not available,
            // e.g. PhantomJS, iOS7, Android 4.4
            // (#6466 MutationObserver is unreliable in IE11)
            // 新建一個 textNode的DOM對象,使用 MutationObserver 綁定該DOM并傳入回調(diào)函數(shù),在DOM發(fā)生變化的時候會觸發(fā)回調(diào),該回調(diào)會進入主線程(比任務(wù)隊列優(yōu)先執(zhí)行)
            let counter = 1
            const observer = new MutationObserver(flushCallbacks)
            const textNode = document.createTextNode(String(counter))
            observer.observe(textNode, {
              characterData: true
            })
            timerFunc = () => {
              counter = (counter + 1) % 2
              // 此時便會觸發(fā)回調(diào)
              textNode.data = String(counter)
            }
            isUsingMicroTask = true
            // 不支持的 MutationObserver 的話,再去判斷是否原生支持 setImmediate
          else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
            // Fallback to setImmediate.
            // Technically it leverages the (macro) task queue,
            // but it is still a better choice than setTimeout.
            timerFunc = () => {
              setImmediate(flushCallbacks)
            }
          else {
            // Promise,MutationObserver, setImmediate 都不支持的話,最后使用 setTimeout(fun, 0)
            // Fallback to setTimeout.
            timerFunc = () => {
              setTimeout(flushCallbacks, 0)
            }
          }

          // 該函數(shù)的作用就是延遲 cb 到當前調(diào)用棧執(zhí)行完成之后執(zhí)行
          export function nextTick (cb?: Function, ctx?: Object) {
            // 傳入的回調(diào)函數(shù)會在callbacks中存起來
            let _resolve
            callbacks.push(() => {
              if (cb) {
                try {
                  cb.call(ctx)
                } catch (e) {
                  handleError(e, ctx, 'nextTick')
                }
              } else if (_resolve) {
                _resolve(ctx)
              }
            })
            // pending是一個狀態(tài)標記,保證timerFunc在下一個tick之前只執(zhí)行一次
            if (!pending) {
              pending = true
              /**
              * timerFunc 實現(xiàn)的就是根據(jù)當前環(huán)境判斷使用哪種方式實現(xiàn)
              * 就是按照 Promise.then和 MutationObserver以及setImmediate的優(yōu)先級來判斷,支持哪個就用哪個,如果執(zhí)行環(huán)境不支持,會采用setTimeout(fn, 0)代替;
              */
              timerFunc()
            }
            // 當nextTick不傳參數(shù)的時候,提供一個Promise化的調(diào)用
            // $flow-disable-line
            if (!cb && typeof Promise !== 'undefined') {
              return new Promise(resolve => {
                _resolve = resolve
              })
            }
          }

          先來看 nextTick函數(shù)。傳入的回調(diào)函數(shù)會在callbacks中存起來,根據(jù)一個狀態(tài)標記 pending 來判斷當前是否要執(zhí)行 timerFunc()

          timerFunc() 是根據(jù)當前環(huán)境判斷使用哪種方式實現(xiàn),按照 Promise.thenMutationObserver以及setImmediate的優(yōu)先級來判斷,支持哪個就用哪個,如果執(zhí)行環(huán)境不支持,就會降級為 setTimeout 0,盡管它有執(zhí)行延遲,可能造成多次渲染,算是沒有辦法的辦法了。timerFunc()函數(shù)中會執(zhí)行 flushCallbacks函數(shù)。

          flushCallbacks 的邏輯非常簡單,對 callbacks遍歷,然后執(zhí)行相應(yīng)的回調(diào)函數(shù)。

          Tips:這里使用callbacks 而不是直接在 nextTick 中執(zhí)行回調(diào)函數(shù)的原因是保證在同一個 tick 內(nèi)多次執(zhí)行 nextTick,不會開啟多個異步任務(wù),而把這些異步任務(wù)都壓成一個同步任務(wù),在下一個 tick 執(zhí)行完畢。

          nextTick不傳cb參數(shù)時,會提供一個Promise化的調(diào)用,比如:

          nextTick().then(() => {})

          這是因為nextTick中有這樣一段邏輯:

          if (!cb && typeof Promise !== 'undefined') {
              return new Promise(resolve => {
                  _resolve = resolve
              })
          }

          _resolve 函數(shù)執(zhí)行,就會跳到 then 的邏輯中。

          四、總結(jié)

          以上就是vue的nextTick方法的實現(xiàn)原理了,總結(jié)一下就是:

          1. vue用異步隊列的方式來控制DOM更新和nextTick回調(diào)先后執(zhí)行
          2. microtask因為其高優(yōu)先級特性,能確保隊列中的微任務(wù)在一次事件循環(huán)前被執(zhí)行完畢
          3. 因為兼容性問題,vue不得不做了microtaskmacrotask的降級方案

          通俗來講,原理就是使用宏任務(wù)微任務(wù)來完成事件調(diào)用的機制,讓自己的回調(diào)事件在一個eventloop的最后執(zhí)行。宏任務(wù)或微任務(wù)根據(jù)瀏覽器情況采取不同的api,在通俗一點 ,可以把nextTick想象成為setTimeout 你就是要把這個事件放到本次事件的循環(huán)末尾調(diào)用

          Vue是異步更新DOM的,在平常的開發(fā)過程中,我們可能會需要基于更新后的 DOM 狀態(tài)來做點什么,比如后端接口數(shù)據(jù)發(fā)生了變化,某些方法是依賴于更新后的DOM變化,這時我們就可以使用 Vue.nextTick(callback)方法。

          五、參考文獻

          • Vue.js 技術(shù)揭秘
          • 全面解析Vue.nextTick實現(xiàn)原理
          • 事件循環(huán):微任務(wù)與宏任務(wù)
          瀏覽 48
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  亚洲人妻av | 国产成人av 高清在线 | 欧美国产一级片 | 久久国产成人免费视频 | 狠狠躁夜夜躁人人爽天天高潮 |