從 Vue3 原始碼中再談 nextTick

dustinny發表於2021-03-06

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

  定義:在下次DOM更新迴圈結束之後執行延遲迴調。在修改資料之後立即使用這個方法,獲取更新後的DOM

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

  下次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="親自試一試</a>

  那麼nextTick是怎麼做到的呢?為了後面的內容更好理解,這裡我們得從js的執行機制說起

  JS執行機制

  我們都知道JS是單執行緒語言,即指某一時間內只能幹一件事,有的同學可能會問,為什麼JS不能是多執行緒呢?多執行緒就能同一時間內幹多件事情了

  是否多執行緒這個取決於語言的用途,一個很簡單的例子,如果同一時間,一個新增了DOM,一個刪除了DOM,這個時候語言就不知道是該添還是該刪了,所以從應用場景來看JS只能是單執行緒

  單執行緒就意味著我們所有的任務都需要排隊,後面的任務必須等待前面的任務完成才能執行,如果前面的任務耗時很長,一些從使用者角度上不需要等待的任務就會一直等待,這個從體驗角度上來講是不可接受的,所以JS中就出現了非同步的概念

  概念

  同步在主執行緒上排隊執行的任務,只有前一個任務執行完畢,才能執行後一個任務

  非同步不進入主執行緒、而進入"任務佇列"(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="親自試一試</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)

  }

  }

  好了,實現全在上面了,好像還沒有解開我們的疑問,我們需要搞清楚queueJob及queuePostFlushCb是怎麼被呼叫的

  //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存在,相信大家心裡已經有答案了。

  總結

  nextTick是vue中的更新策略,也是效能最佳化手段,基於JS執行機制實現

  vue中我們改變資料時不會立即觸發檢視,如果需要實時獲取到最新的DOM,這個時候可以手動呼叫nextTick


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69995861/viewspace-2761553/,如需轉載,請註明出處,否則將追究法律責任。

相關文章