Vue - The Good Parts: nextTick

前言

nextTick 在 Vue 中是一個很出名的工具函數,咱們在實際運用的時候也常常會用到,那麼它實際上到底有什麼樣的做用,Vue 中又是如何設計的,咱們在平常中有什麼場景是能夠借鑑的。html

咱們以 Vue 最新的 v2.6.14 版原本分析,連接 github.com/vuejs/vue/b…前端

正文分析

What

nextTick 是個什麼東西,參考 Vue 2 的官方 API 文檔:cn.vuejs.org/v2/api/#Vue…vue

image2021-6-9_11-32-53.png

能夠看出是執行一個回調函數,咱們這裏能夠成爲一個任務,那在 Vue 中文檔已經講明白了,在下次 DOM 更新循環結束後執行這個任務(回調),這樣你就能夠取到更新後的 DOM 了。react

How

先來看下 nextTick 所有的代碼,把flow相關去掉,加上咱們本身的註釋:git

import { noop } from 'shared/util'
import { handleError } from './error'
import { isIE, isIOS, isNative } from './env'
 
// 是否使用的是 MicroTask,如 Promise MutationObserver
// 若是瀏覽器不支持 則會使用 MacroTask setImmediate setTimeout
// 相關進一步知識能夠參考 瀏覽器 eventloop 相關文章
export let isUsingMicroTask = false
// 儲存全部的 callback 隊列,能夠認爲是一個個任務
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]()
  }
}
 
// 實現異步的函數,從名字上看下一個 tick,即一個 timer
let timerFunc
 
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  // 原生 Promise 異步
  const p = Promise.resolve()
  timerFunc = () => {
    // 利用 promise.then 實現,一個 micro task 以後執行 flushCallbacks
    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
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||
  // PhantomJS and iOS 7.x
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  // 降級使用 MutationObserver
  // Use MutationObserver where native Promise is not available,
  // e.g. PhantomJS, iOS7, Android 4.4
  // (#6466 MutationObserver is unreliable in IE11)
  let counter = 1
  const observer = new MutationObserver(flushCallbacks)
  const textNode = document.createTextNode(String(counter))
  observer.observe(textNode, {
    characterData: true
  })
  timerFunc = () => {
    // 觸發textNode的改變,進而觸發MutationObserver的回調執行 flushCallbacks
    counter = (counter + 1) % 2
    textNode.data = String(counter)
  }
  isUsingMicroTask = true
} 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
    setImmediate(flushCallbacks)
  }
} else {
  // Fallback to setTimeout.
  timerFunc = () => {
    // 經典的 setTimeout
    setTimeout(flushCallbacks, 0)
  }
}
 
// 主實現
export function nextTick (cb, ctx) {
  let _resolve
  // 往 callbacks 隊列中添加一個一個任務
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  // 若是不是在等待中,即上一輪的callbacks任務隊列已經執行完畢
  // 那麼就進入等待狀態,從新進入新一輪的等待下一個timer而後執行新一輪存下來的callbacks任務隊列
  if (!pending) {
    pending = true
    timerFunc()
  }
  // nextTick的另外一種用法,nextTick().then()
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}
複製代碼

咱們能夠看出來,代碼雖然很少,可是處理的狀況仍是不少的,也有不少兼容性的處理。若是咱們來翻譯下,nextTick 最核心的實現就是:拿一個隊列存儲全部要執行的任務,在下一個tick(異步)執行這些任務github

那根據這個核心實現,在不考慮兼容性和異常的狀況下,咱們能夠實現一個極簡版本的 nextTick:小程序

let pending = false
const tasks = []
const flushCallbacks = () => {
  pending = false
  tasks.forEach(task => task())
  tasks.length = 0
}
 
const p = Promise.resolve()
const timerFunc = () => {
  p.then(flushCallbacks)
}
 
function nextTick(task) {
  tasks.push(task)
  if (!pending) {
    pending = true
    timerFunc()
  }
}
複製代碼

短短20行,可是功能很核心也很強大,咱們能夠像這樣使用:api

const task1 = () => console.log('1')
const task2 = () => console.log('2')
 
console.log('before')
nextTick(task1)
nextTick(task2)
console.log('after')
 
// 運行的結果:before after 1 2
複製代碼

這個時候,相信你已經更進一步理解了 nextTick:將須要異步執行的任務收集起來在下一個 tick 依次執行他們。數組

Why

那爲何須要 nextTick 呢,咱們不能直接執行這些任務嗎?在 Vue 中的話,官網也給到了你們答案,詳情 cn.vuejs.org/v2/guide/re…promise

若是簡化來理解的話就是:爲了更好的性能,將更新 DOM 操做存放在異步更新隊列中,在下一個 tick 統一進行更新 DOM 操做。

試想下,若是咱們每更新一次數據,Vue 就須要去更新一次 DOM 操做的話,得有多卡頓,由於平常咱們處理邏輯必定是這樣的:

const data = {
  title: 'hello',
  desc: 'world'
}
this.msg = data.title
this.context = data.desc
複製代碼

這個仍是一個局部場景,更別想說,咱們的整個 Vue 應用的數據更新,DOM 更新了。

因此 Vue 中就採用了異步更新隊列這種方式來進行優化,也就是依賴上邊咱們分析的 nextTick 所作的最核心的事情。

總結

nextTick 之中,咱們能夠從其中學到什麼或者咱們能夠進一步瞭解什麼呢?

隊列

看出這裏邊對於隊列的操做(固然,用數組模擬的,本質是同樣的):隊列裏添加任務,執行隊列裏的任務,清空隊列。

隊列是一個咱們十分經常使用的數據結構,上邊所提到的 eventloop,你會發現和 nextTick 本質是同樣的,只是變得更復雜了,存在多個隊列的狀況,須要處理。

異步

咱們知道了部分 timerFunc 的實現,相對應的也就是咱們須要知道,哪些 API 的操做是異步的,以及是哪一種異步處理(MacroTask、MicroTask),他們之間有什麼差別和使用的影響,咱們遇到異步場景的時候應該如何去選擇。

還有一個點,這裏用到了降級的方案 setTimeout,傳的第二個參數是 0,那麼這個時候的效果是啥樣的;setTimeout 還能夠有其餘的什麼用法,到底能夠有幾個參數,返回值是啥類型的,何時須要咱們手工去 clearTimeout。

相對應的延伸,就是大名鼎鼎的 eventloop 相關知識,也須要去區分瀏覽器環境和 Node.js 環境。

異步和隊列碰撞在一塊兒,能夠有不少火花。

咱們有不少時候時候都須要處理異步任務,而對於這些任務的處理,最合適的數據結構就是隊列了,例如大名鼎鼎的 async 庫 github.com/caolan/asyn… 簡直就是把異步玩到了極致,裏邊有不少很好的實現思路以及技巧,感興趣的也是能夠深刻了解的。

咱們的現實需求也同樣,例如,在小程序場景中,不能超出10個的併發請求,超出的請求會被取消掉,因此咱們須要對請求進行封裝一層,在mpx中是封裝爲了mpx-fetch,並且咱們還要求了高低優先級兩種請求,這種狀況,就須要咱們藉助於隊列來實現咱們的需求。

數組循環

flushCallbacks 中,咱們看到了一個技巧,正常咱們本身的簡單實現中,是直接便利 callbacks 而後執行的,而 Vue 中則不是,他是複製了一份新的,而後循環執行的。

這麼作的緣由,實際上是考慮了一種特殊狀況,若是某一個 callback 執行的時候,又一次調用了 nextTick,進而更新了 callbacks,那這個時候的執行就不是咱們所指望的了。因此須要先拷貝一份原有的,即便在 cb 中更新了 callbacks 也不影響咱們的循環和執行,符合預期。

這是一個很嚴謹的地方,咱們在實際場景中,也要有這種思考和意識。

同時這個問題還能夠有不少的延伸,針對於數組循環,正向循環和逆向循環有啥區別嗎,是否是都同樣;以及咱們用 for 循環和用數組自己的 forEach 會有啥不同嗎;還有 for 循環的終止條件,咱們寫 i < array.lengthconst len = array.length; i < len 有啥區別沒有?

Promise

Promise 是一個很好的東西,至關有用,咱們須要深刻理解並使用它。這裏有一個比較有意思的一個點是 nextTick 的返回值處理,應用到了一個技巧:外部如何更新 Promise 的狀態,即你所看到的 _resolve 這個變量的做用。

Promise,一個各大廠基本都在考察的,Promise有哪些規範,包含哪些定義,哪些API,如何實現一個 Promise。

但願你去深刻學習和理解它,作到精通 Promise!

其餘小Tips

  • isNative 的處理,他是如何判斷的
  • pending,防重
  • 錯誤如何處理
  • 如何考慮兼容和降級

滴滴前端技術團隊的團隊號已經上線,咱們也同步了必定的招聘信息,咱們也會持續增長更多職位,有興趣的同窗能夠一塊兒聊聊。

相關文章
相關標籤/搜索