vue $nextTick 原理詳解


一.nextTick定義

二.為什么使用 nextTick

Vue 在更新 DOM 時是異步執行的。

只要偵聽到數據變化,Vue 將開啟一個隊列,並緩沖在同一事件循環中發生的所有數據變更。如果同一個 watcher 被多次觸發,只會被推入到隊列中一次。在下一個的事件循環“tick”中,Vue 刷新隊列並執行實際 (已去重的) 工作。

在事件環境中的數據變化完成,在進行渲染[視圖更新],可以避免DOM的頻繁變動,從而避免了因此帶來的瀏覽器卡頓,大幅度提升性能.

三.nextTick的理解和應用場景

  • 在鈎子函數created()里面想要獲取dom的內容或者操作dom

 

  • 在視圖data更新后,基於新的視圖操作該dom

在下次 DOM 更新循環結束之后執行延遲回調。在修改數據之后立即使用這個方法,獲取更新后的 DOM

 

 

//改變數據 
vm.message = 'changed' 
//想要立即使用更新后的DOM。這樣不行,因為設置message后DOM還沒有更新 
console.log(vm.$el.textContent) 
// 並不會得到'changed' //這樣可以,nextTick里面的代碼會在DOM更新后執行 
Vue.nextTick(function(){     
  console.log(vm.$el.textContent) 
//可以得到'changed' 
})

 

 

四.事件隊列(非阻塞,單線程)

1.JavaScript的運行機制

  1. 所有同步任務都在主線程上執行,形成一個[執行棧]

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

  3. 一旦"執行棧"中的所有同步任務執行完畢,系統就會讀取"任務隊列",看看里面有哪些事件對應的異步任務,於是結束等待狀態,進入執行棧,開始執行(執行順序,首先執行完當前隊列的微任務,再去執行當前隊列的宏任務)。

  4. 主線程不斷重復上面的第三步。

2.概括

調用棧中的同步任務都執行完畢,棧內被清空了,就代表主線程空閑了,這個時候就會去任務隊列中按照順序讀取一個任務放入到棧中執行。每次棧內被清空,都會去讀取任務隊列有沒有任務,有就讀取執行,一直循環讀取-執行的操作。

 

 

3.MacroTask(宏任務)/ MicroTask(微任務)

 

MacroTask(宏任務):script、setTimeout、setInterval、setImmediate(瀏覽器暫時不支持,只有IE10支持,具體可見MDN)。
MicroTask(微任務):Process.nextTick(Node獨有)、Promise、Object.observe、MutationObserver(具體使用方式查看這里)

 

4.示例練習

 

*** 判斷其輸出
setTimeout(()=>{ 
  console.log("setTimeout1"); 
  Promise.resolve().then(data => { console.log(222); }); 
}); 
setTimeout(()=>{ console.log("setTimeout2"); }); 
Promise.resolve().then(data=>{ console.log(111); })

*** 判斷其輸出
console.log('script start'); 
setTimeout(function () { 
  console.log('setTimeout---0'); 
}, 0); 
setTimeout(function () { 
  console.log('setTimeout---200'); 
  setTimeout(function () { 
    console.log('inner-setTimeout---0'); 
  }); 
  Promise.resolve().then(function () { 
    console.log('promise5'); 
  }); 
}, 200); 
Promise.resolve().then(function () { 
  console.log('promise1'); 
}).then(function () { 
  console.log('promise2'); 
}); 
Promise.resolve().then(function () { 
  console.log('promise3'); 
}); 
console.log('script end');

 

五.源碼解析

 

/* @flow */
/* globals MutationObserver */
import { noop } from 'shared/util'
import { handleError } from './error'
import { isIE, isIOS, isNative } from './env'

/** 是否是微任務 */
export let isUsingMicroTask = false

/** 用來存儲所有需要執行的回調函數 */
const callbacks = []

/*
 * 表示狀態,判斷是否有正在執行的回調函數。
 * 如果代碼中 timerFunc 函數被推送到任務隊列中去則不需要重復推送。 
 */
let pending = false

/** 用來執行callbacks里面存儲的所有回調函數 */
function flushCallbacks () {
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}

/**  保存需要被執行的函數。 */
let timerFunc

/**判斷是否支出 Promise 原生調用 */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
/** 判斷是否支持 MutationObserver */
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||
  // PhantomJS and iOS 7.x
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  let counter = 1
  const observer = new MutationObserver(flushCallbacks)
  const textNode = document.createTextNode(String(counter))
  observer.observe(textNode, {
    characterData: true
  })
  timerFunc = () => {
    counter = (counter + 1) % 2
    textNode.data = String(counter)
  }
  isUsingMicroTask = true
/** 判斷是否支持 setImmediate */
} 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(flushCallbacks)
  }
} else {
  // Fallback to setTimeout.
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

/**
 * @params cb 回調
 * @params ctx 執行上下文 
 */
export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  if (!pending) {
    pending = true
    timerFunc()
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

 

以上代碼表明在vue中處理異步延遲調用的方式主要如下:

  • 判斷是否支持原生Promise;

 

** 創建MutationObserver示例observer ,傳參回調為 flushCallbacks

** 創建文本節點textNode,並通過observer 監聽 textNode 的內容變化

** 設置timerFunc函數,以觸發textNode 的變化,觸發 flushCallbacks

  • 判斷是否支持setImmediate;

  • 以上全部不支持,則通過setTimeout(fn, 0)模擬異步延遲調用。

 

Vue.prototype.$nextTick = function (fn) {
  return nextTick(fn, this)
};

 

參考文獻


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM