從 Vue3 源碼中再談 nextTick

開始以前先看下官方對其的定義html

定義: 在下次 DOM 更新循環結束以後執行延遲迴調。在修改數據以後當即使用這個方法,獲取更新後的 DOMvue

看完是否是有一堆問號?咱們從中找出來產生問號的關鍵詞react

  • 下次 DOM 更新循環結束以後?
  • 執行延遲迴調?
  • 更新後的 DOM?

從上面三個疑問大膽猜測一下性能優化

  • vue 更新DOM是有策略的,不是同步更新
  • nextTick 能夠接收一個函數作爲入參
  • nextTick 後能拿到最新的數據

好了,問題都拋出來了,先來看一下如何使用多線程

import { createApp, nextTick } from 'vue'
const app = createApp({
  setup() {
    const message = ref('Hello!')
    const changeMessage = async newMessage => {
      message.value = newMessage
      // 這裏獲取DOM的value是舊值
      await nextTick()
      // nextTick 後獲取DOM的value是更新後的值
      console.log('Now DOM is updated')
    }
  }
})

<a href="https://vue3js.cn/run/nextTick" target="_blank">親自試一試</a>app

那麼 nextTick 是怎麼作到的呢?爲了後面的內容更好理解,這裏咱們得從 js 的執行機制提及異步

JS執行機制

咱們都知道 JS 是單線程語言,即指某一時間內只能幹一件事,有的同窗可能會問,爲何 JS 不能是多線程呢?多線程就能同一時間內幹多件事情了async

是否多線程這個取決於語言的用途,一個很簡單的例子,若是同一時間,一個添加了 DOM,一個刪除了 DOM, 這個時候語言就不知道是該添仍是該刪了,因此從應用場景來看 JS 只能是單線程函數

單線程就意味着咱們全部的任務都須要排隊,後面的任務必須等待前面的任務完成才能執行,若是前面的任務耗時很長,一些從用戶角度上不須要等待的任務就會一直等待,這個從體驗角度上來說是不可接受的,因此JS中就出現了異步的概念post

概念

  • 同步 在主線程上排隊執行的任務,只有前一個任務執行完畢,才能執行後一個任務
  • 異步 不進入主線程、而進入"任務隊列"(task queue)的任務,只有"任務隊列"通知主線程,某個異步任務能夠執行了,該任務纔會進入主線程執行

運行機制

  • (1)全部同步任務都在主線程上執行,造成一個執行棧(execution context stack)。

  • (2)主線程以外,還存在一個"任務隊列"(task queue)。只要異步任務有了運行結果,就在"任務隊列"之中放置一個事件。

  • (3)一旦"執行棧"中的全部同步任務執行完畢,系統就會讀取"任務隊列",看看裏面有哪些事件。那些對應的異步任務,因而結束等待狀態,進入執行棧,開始執行。

  • (4)主線程不斷重複上面的第三步

image.png

nextTick

如今咱們回來vue中的nextTick

實現很簡單,徹底是基於語言執行機制實現,直接建立一個異步任務,那麼nextTick天然就達到在同步任務後執行的目的

const p = Promise.resolve()
export function nextTick(fn?: () => void): Promise<void> {
  return fn ? p.then(fn) : p
}

<a href="https://vue3js.cn/run/nextTick-demo-1.html" target="_blank">親自試一試</a>

看到這裏,有的同窗可能又會問,前面咱們猜測的 DOM 更新也是異步任務,那他們的這個執行順序如何保證呢?

別急,在源碼中nextTick還有幾個兄弟函數,咱們接着往下看

queueJob and queuePostFlushCb

queueJob 維護job列隊,有去重邏輯,保證任務的惟一性,每次調用去執行 queueFlush queuePostFlushCb 維護cb列隊,被調用的時候去重,每次調用去執行 queueFlush

const queue: (Job | null)[] = []
export function queueJob(job: Job) {
  // 去重 
  if (!queue.includes(job)) {
    queue.push(job)
    queueFlush()
  }
}

export function queuePostFlushCb(cb: Function | Function[]) {
  if (!isArray(cb)) {
    postFlushCbs.push(cb)
  } else {
    postFlushCbs.push(...cb)
  }
  queueFlush()
}

queueFlush

開啓異步任務(nextTick)處理 flushJobs

function queueFlush() {
  // 避免重複調用flushJobs
  if (!isFlushing && !isFlushPending) {
    isFlushPending = true
    nextTick(flushJobs)
  }
}

flushJobs

處理列隊,先對列隊進行排序,執行queue中的job,處理完後再處理postFlushCbs, 若是隊列沒有被清空會遞歸調用flushJobs清空隊列

function flushJobs(seen?: CountMap) {
  isFlushPending = false
  isFlushing = true
  let job
  if (__DEV__) {
    seen = seen || new Map()
  }

  // Sort queue before flush.
  // This ensures that:
  // 1. Components are updated from parent to child. (because parent is always
  //    created before the child so its render effect will have smaller
  //    priority number)
  // 2. If a component is unmounted during a parent component's update,
  //    its update can be skipped.
  // Jobs can never be null before flush starts, since they are only invalidated
  // during execution of another flushed job.
  queue.sort((a, b) => getId(a!) - getId(b!))

  while ((job = queue.shift()) !== undefined) {
    if (job === null) {
      continue
    }
    if (__DEV__) {
      checkRecursiveUpdates(seen!, job)
    }
    callWithErrorHandling(job, null, ErrorCodes.SCHEDULER)
  }
  flushPostFlushCbs(seen)
  isFlushing = false
  // some postFlushCb queued jobs!
  // keep flushing until it drains.
  if (queue.length || postFlushCbs.length) {
    flushJobs(seen)
  }
}

好了,實現全在上面了,好像尚未解開咱們的疑問,咱們須要搞清楚 queueJobqueuePostFlushCb 是怎麼被調用的

//  renderer.ts
function createDevEffectOptions(
  instance: ComponentInternalInstance
): ReactiveEffectOptions {
  return {
    scheduler: queueJob,
    onTrack: instance.rtc ? e => invokeArrayFns(instance.rtc!, e) : void 0,
    onTrigger: instance.rtg ? e => invokeArrayFns(instance.rtg!, e) : void 0
  }
}

// effect.ts
const run = (effect: ReactiveEffect) => {
  ...

  if (effect.options.scheduler) {
    effect.options.scheduler(effect)
  } else {
    effect()
  }
}

看到這裏有沒有恍然大悟的感受?原來當響應式對象發生改變後,執行 effect 若是有 scheduler 這個參數,會執行這個 scheduler 函數,而且把 effect 當作參數傳入

繞口了,簡單點就是 queueJob(effect),嗯,清楚了,這也是數據發生改變後頁面不會當即更新的緣由

effect傳送門

爲何要用nextTick

一個例子讓你們明白

{{num}}
for(let i=0; i<100000; i++){
	num = i
}

若是沒有 nextTick 更新機制,那麼 num 每次更新值都會觸發視圖更新,有了nextTick機制,只須要更新一次,因此爲何有nextTick存在,相信你們內心已經有答案了。

總結

nextTickvue 中的更新策略,也是性能優化手段,基於JS執行機制實現

vue 中咱們改變數據時不會當即觸發視圖,若是須要實時獲取到最新的DOM,這個時候能夠手動調用 nextTick

本文由博客一文多發平臺 OpenWrite 發佈!

相關文章
相關標籤/搜索