1. 程式人生 > 程式設計 >Vue.js原理分析之nextTick實現詳解

Vue.js原理分析之nextTick實現詳解

前言

tips:第一次發技術文章,篇幅比較簡短,主要採取文字和關鍵程式碼表現的形式,希望幫助到大家。(若有不正確還請多多指正)

nextTick作用和用法

用法:nextTick接收一個回撥函式作為引數,它的作用是將回調延遲到下一次DOM更新之後執行,如果沒有提供回撥函式引數且在支援Promise的環境中,nextTick將返回一個Promise。
適用場景:開發過程中,開發者需要在更新完資料之後,需要對新DOM做一些操作,其實我們當時無法對新DOM進行操作,因為這時候還沒有重新渲染,這時候nextTick就派上了用場。

nextTick實現原理

下面我們介紹下nextTick工作原理:

首先我們應該瞭解到更新完資料(狀態)之後,DOM更新這個動作並不是同步進行的,而是非同步的。Vue.js中有一個佇列,每當需要渲染時,會將Watcher推送到這個佇列中,等下一次事件迴圈中再讓Watcher觸發渲染流程。這裡我們可能會有兩個疑問:

**1.為什麼更新DOM是非同步的?**

我們知道從Vue2.0開始使用虛擬DOM進行渲染,變化偵測只發送到元件級別,元件內部則通過虛擬DOM的diff(比對)而進行區域性渲染,而在同一次事件迴圈中元件假如收到兩份通知,元件是否會進行兩次渲染呢?事實上一次事件迴圈元件會在所有狀態修改完畢之後只進行一次渲染操作。

**2.什麼是事件迴圈?**

javascript是單執行緒指令碼語言,它具有非阻塞特性,之所以非阻塞是由於在處理非同步程式碼時,主執行緒會掛起這個任務,當非同步任務處理完畢之後會根據一定的規則去執行非同步任務的回撥,非同步任務分巨集任務(macrotast)和微任務(microtast),它們會被分配到不同的佇列中,當執行棧所有任務執行完畢之後,會先檢查微任務佇列中是否有事件存在,優先執行微任務佇列事件對應的回撥,直至為空。然後再執行巨集任務佇列中事件的回撥。無限重複這個過程,形成一個無限迴圈就叫做事件迴圈。

常見微任務包括:Promise 、MutationObserver、Object.observer、process.nextTick等

常見巨集任務包括:setTimeout、setInterval、setImmediate、MessageChannel、requestAnimation、UI互動事件等

微任務如何註冊?

nextTick會將回調新增到非同步任務佇列中延遲執行,在執行回撥前,反覆呼叫nextTick,Vue並不會反覆新增到任務佇列中,只會向任務佇列新增一個任務,多次使用nextTick只會將回調新增到回撥列表快取起來,當任務觸發時,會清空回撥列表並依次執行所有回撥 ,具體程式碼如下:

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]()
  }
}
let microTimerFunc
const p = Promise.resolve()
microTimerFunc = () => { //註冊微任務
  p.then(flushCallbacks)
}

export function nextTick(cb,ctx){
  callbacks.push(()=>{
    if(cb){
      cb.call(ctx)
    }
  })
  if(!pending){
    pending = true //將pending設定為true,保證任務在依次事件迴圈中不會重複新增
    microTimerFunc()
  }
}

由於微任務優先順序太高,可能在某些場景下需要使用到巨集任務,所以Vue提供了可以強制使用巨集任務的方法withMacroTask。具體實現如下:

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]()
  }
}
let microTimerFunc
//新增程式碼
let macroTimerFunc = function(){
  ...
}

let useMacroTask = false
const p = Promise.resolve()
microTimerFunc = () => { //註冊微任務
  p.then(flushCallbacks)
}

//新增程式碼
export function withMacroTask(fn){
  return fn._withTask || fn._withTask = function()=>{
    useMacroTask = true
    const res = fn.apply(null,arguments)
    useMacroTask = false
    return res
  }
}

export function nextTick(cb,ctx){
  callbacks.push(()=>{
    if(cb){
      cb.call(ctx)
    }
  })
  if(!pending){
    pending = true //將pending設定為true,保證任務在依次事件迴圈中不會重複新增
    //修改程式碼
    if(useMacroTask){
      macroTimerFunc()
    }else{
      microTimerFunc()
    }
  }
}

上面提供了一個withMacroTask方法強制使用巨集任務,通過useMacroTask變數進行控制是否使用註冊巨集任務執行,withMacroTask實現很簡單,先將useMacroTask變數設定為true,然後執行回撥,回撥執行之後再改回false。

巨集任務是如何註冊?

註冊巨集任務優先使用setImmediate,但是存在相容性問題,只能在IE中使用,所以使用MessageChannel作為備選方案,若以上都不支援則最後會使用setTimeout。具體實現如下:

if(typeof setImmediate !== 'undefined' && isNative(setImmediate)){
  macroTimerFunc = ()=>{
    setImmediate(flushCallbacks)
  }
} else if(
  typeof MessageChannel !== 'undefined' && 
  (isNative(MessageChannel) || MessageChannel.toString() === '[Object MessageChannelConstructor]')
){
  const channel = new MessageChannel()
  const port = channel.port2
  channel.port1.onmessage = flushCallbacks
  macroTimerFunc = ()=>{
    port.postMessage(1)
  }
} else {
  macroTimerFunc = ()=>{
    setTimout(flushCallbacks,0)
  }
}

microTimerFunc的實現方法是通過Promise.then,但是並不是所有瀏覽器都支援Promise,當不支援的時候採取降級為巨集任務方式

if(typeof Promise !== 'undefined' && isNative(Promise)){
  const p = Promise.resolve()
  microTimerFunc = ()=>{
    p.then(flushCallbacks)
  }
} else {
  microTimerFunc = macroTimerFunc
}

若未提供回撥且環境支援Promise情況下,nextTick會返回一個Promise,具體實現如下:

export function nextTick(cb,ctx) {
  let _resolve
  callbacks.push(()=>{
    if(cb){
      cb.call(ctx)
    }else{
      _resolve(ctx)
    }
  })

  if(!pending){
    pending = true
    if(useMacroTask){
      macroTimerFunc()
    }else{
      microTimerFunc()
    }
  }

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

以上是nextTick執行原理的設計,完整程式碼如下:

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]()
  }
}
let microTimerFunc
let macroTimerFunc 
let useMacroTask = false

//註冊巨集任務
if(typeof setImmediate !== 'undefined' && isNative(setImmediate)){
  macroTimerFunc = ()=>{
    setImmediate(flushCallbacks)
  }
} else if(
  typeof MessageChannel !== 'undefined' && 
  (isNative(MessageChannel) || MessageChannel.toString() === '[Object MessageChannelConstructor]')
){
  const channel = new MessageChannel()
  const port = channel.port2
  channel.port1.onmessage = flushCallbacks
  macroTimerFunc = ()=>{
    port.postMessage(1)
  }
} else {
  macroTimerFunc = ()=>{
    setTimout(flushCallbacks,0)
  }
}

//微任務註冊
if(typeof Promise !== 'undefined' && isNative(Promise)){
  const p = Promise.resolve()
  microTimerFunc = ()=>{
    p.then(flushCallbacks)
  }
} else {//降級處理
  microTimerFunc = macroTimerFunc
}

export function withMacroTask(fn){
  return fn._withTask || fn._withTask = function()=>{
    useMacroTask = true
    const res = fn.apply(null,ctx){
  let _resolve
  callbacks.push(()=>{
    if(cb){
      cb.call(ctx)
    }else{
      _resolve(ctx)
    }
  })
  if(!pending){
    pending = true //將pending設定為true,保證任務在依次事件迴圈中不會重複新增
    //修改程式碼
    if(useMacroTask){
      macroTimerFunc()
    }else{
      microTimerFunc()
    }
  }

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

以上便是對nextTick的實現原理的全部介紹。

參考資料

Vue.js深入淺出

總結

到此這篇關於Vue.js原理分析之nextTick實現詳解的文章就介紹到這了,更多相關Vue.js原理之nextTick實現內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!