從 4 個要點說透 Vue nextTick

前言


nextTick 在 Vue 中是一個很出名的工具函數,我們在實際運用的時候也經常會用到,那麼它實際上到底有什麼樣的作用,Vue 中又是如何設計的,我們在日常中有什麼場景是可以借鑑的。

我們以 Vue 最新的 v2.6.14 版本來分析,鏈接 https://github.com/vuejs/vue/blob/v2.6.14/src/core/util/next-tick.js

正文分析

What

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

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

How

先來看下 nextTick 全部的代碼,把 flow 相關去掉,加上我們自己的註釋:

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(異步)執行這些任務。

那根據這個核心實現,在不考慮兼容性和異常的情況下,我們可以實現一個極簡版本的 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 行,但是功能很核心也很強大,我們可以像這樣使用:

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 中的話,官網也給到了大家答案,詳情 https://cn.vuejs.org/v2/guide/reactivity.html#%E5%BC%82%E6%AD%A5%E6%9B%B4%E6%96%B0%E9%98%9F%E5%88%97。

如果簡化來理解的話就是:爲了更好的性能,將更新 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 庫 https://github.com/caolan/async 簡直就是把異步玩到了極致,裏邊有很多很好的實現思路以及技巧,感興趣的也是可以深入瞭解的。

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

數組循環

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

這麼做的原因,其實是考慮了一種特殊情況,如果某一個 callback 執行的時候,又一次調用了 nextTick,進而更新了 callbacks,那這個時候的執行就不是我們所期望的了。所以需要先拷貝一份原有的,即使在 cb 中更新了 callbacks 也不影響我們的循環和執行,符合預期。

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

同時這個問題還可以有很多的延伸,針對於數組循環,正向循環和逆向循環有啥區別嗎,是不是都一樣;以及我們用 for 循環和用數組本身的 forEach 會有啥不一樣嗎;還有 for 循環的終止條件,我們寫 i < array.length 和 const len = array.length; i < len 有啥區別沒有?

Promise

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

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

希望你去深入學習和理解它,做到精通 Promise!

其他小 Tips

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/vXPfiOM3QevqMVqGlKevQA