vue 官網中是這樣描述 nextTick 的
在下次 DOM 更新回圈結束之后執行延遲回呼,在修改資料之后立即使用這個方法,可以獲取更新后的 DOM,
在學習 nextTick 是如何實作之前,我們要先了解下 JavaScript 的執行機制
JavaScript 執行機制
瀏覽器是多執行緒的,例如GUI渲染執行緒、JS引擎執行緒、事件監聽執行緒等,,,
javascript 執行機制就是借用瀏覽器的多執行緒機制,再基于 Event Loop 事件回圈機制實作的,其實作了單執行緒異步效果
Event Loop 步驟大致如下:
- 瀏覽器加載頁面時,除了開辟堆疊記憶體外,還會創建兩個佇列
- Web API:任務監聽佇列,監測異步任務是否可以執行
- Task Queue:任務佇列,分為異步宏任務佇列和異步微任務佇列
- 當主執行緒自上而下執行代碼程序中,如果遇到異步代碼,則把異步任務放到 Web API 中去監聽
- 瀏覽器會開辟新的執行緒去監聽是否可以執行
- 不會阻礙主執行緒的渲染,它會繼續向下執行同步代碼
- 當異步任務被監測為可以執行了(有了運行結果),也不會立即去執行,而是在 task queue 中放置一個事件,排隊等待執行
- 根據微任務還是宏任務,放在不同的佇列中
- 誰先進來排隊的,誰在各自隊伍的最前面
- 執行堆疊中的所有同步任務執行完畢,主執行緒空閑下來,此時會去 task queue 中把正在排隊的事件,按照順序取出來,進入主執行緒執行
- 微任務優先級比較高,當執行堆疊為空時,先去執行微任務佇列中的事件,直到微任務佇列為空,才會去執行宏任務佇列中的事件
- 上述程序會不斷重復,也就是常說的事件回圈(Event Loop)
task 又分為宏任務(macro task)和微任務(micro task)兩大類,在瀏覽器環境中
- 常見的 macro task 有 script(整體代碼)、
setTimeout/setInterval/setImmediate、XMLHttpRequest/fetch,DOM事件(如滑鼠點擊、滾動頁面、放大縮小等),渲染事件(決議 DOM、計算布局、繪制) - 常見的 micro task 有
Promise.then/catch/finally、async/await、MutationObserver
需要注意的是!!!如果處理微任務的程序中有新的微任務添加進來了,添加的速度一直比執行快,則永遠執行微任務
下面的代碼永遠不會列印宏任務輸出
function macroFn(){
setTimeout(() => {
console.log('>>>>MA')
},0)
}
function microFn(){
Promise.resolve().then(() => {
console.log('mi')
microFn()
})
}
macroFn()
microFn()
nextTick實作原理
vue2.7 原始碼中,有一個單獨的檔案src/core/util/next-tick.js去維護 nextTick,有興趣的同學可以自行去觀看
vue2.7 原始碼中,nextTick并沒有直接使用某個 API ,而是采用了優雅降級的方案去實作異步更新
在內部會嘗試使用原生的Promise.then (IE不支持)、MutationObserver 和 setImmediate (高版本IE專享),如果執行環境還不支持的話,則會采用 setTimeout(fn, 0)
需要注意的是,我們維護了一個 callbacks,用于存盤 nextTick 回呼
這樣就保證了在同一個 tick 內多次呼叫 nextTick,只需創建一個異步任務,就可以依次執行 callbacks 中的所有 nextTick 回呼,而不是去開啟多個異步任務去處理,
let callbacks = [] // 存盤 nextTick 回呼
let waiting = false // 防抖
// 按照順序依次執行 callbacks 中的方法
function flushCallbacks() {
let cbs = callbacks.slice(0)
waiting = false
callbacks = []
cbs.forEach(cb => cb())
}
let timerFunc;
if (Promise) {
timerFunc = () => {
Promise.resolve().then(flushCallbacks)
}
}else if(MutationObserver){
let observer = new MutationObserver(flushCallbacks); // 這里傳入的回呼是異步執行的
let textNode = document.createTextNode(1);
observer.observe(textNode,{
characterData:true
});
timerFunc = () => {
textNode.textContent = 2;
}
}else if(setImmediate){
timerFunc = () => {
setImmediate(flushCallbacks);
}
}else{
timerFunc = () => {
setTimeout(flushCallbacks);
}
}
export function nextTick(cb) {
callbacks.push(cb) // 維護 nextTick 中的 cakllback 方法
if (!waiting) {
timerFunc()
waiting = true
}
}
異步更新
vue 內部的異步更新渲染也使用了 nextTick
在 Watcher 類的 update 更新方法中,我們呼叫了 queueWatcher 異步佇列更新方法,該方法在 vue2.7原始碼中的 src/core/util/scheduler.js 檔案中維護
import { queueWatcher } from './scheduler'
class Watcher {
...
// 重新渲染
update() {
console.log('watcher-update')
queueWatcher(this) // watcher 異步更新
}
}
src/core/util/scheduler.js
import { nextTick } from '../util/next-tick'
/**
* @name QueueWatcher,內部 watcher 異步更新
* @decs 把當前的 watcher 暫存起來,在一個tick周期內,不管我們的 update 執行多少次,只會執行一輪重繪操作
*/
let queue = []
let has = {}
let pending = false // 防抖
function flushSchedulerQueue() {
let flushQueue = queue.slice(0)
queue = []
has = {}
pending = false
flushQueue.forEach(q => q.run()) // 在重繪的程序中可能還有新的 watcher,重新放到 queue 中
}
// 在一個tick周期內,不管我們的 update 執行多少次,只會執行一輪重繪操作
export function queueWatcher(watcher) {
const id = watcher.id
if (!has[id]) {
queue.push(watcher)
has[id] = true
if (!pending) {
nextTick(flushSchedulerQueue)
pending = true
}
}
}
常見問題
1. nexTick 是異步還是同步?
這個不能一概而論,nextTick 內部既有同步代碼又有異步代碼,
例如 維護 callbacks 佇列是同步任務;執行佇列中的方法是異步任務
2. nextTick 回呼的執行是微任務還是宏任務?
針對 vue2.7 來說,nextTick并沒有直接使用某個 API ,而是采用了優雅降級的方案去實作異步更新,
在內部會嘗試使用原生的Promise.then (微任務)、MutationObserver (微任務) 和 setImmediate (宏任務),如果執行環境還不支持的話,則會采用 setTimeout (宏任務)
可以理解為 99% 的場景下都是微任務,只有在不支持 Promise 和 MutationObserver API的瀏覽器中,才會是宏任務,例如 IE9 、IE10
3. 為什么要封裝 nextTick?而不是使用某個具體的 API?
優雅降級,盡量使用微任務,盡可能縮短渲染周期
保證統一性,nextTick 可以暴露給用戶,保證用戶在修改資料之后立即使用這個方法,可以獲取更新后的 DOM
this.name = 'libc'
this.$nextTick(()=>{
console.log(document.querySelector('.user').innerHTML)
});
參考檔案
這一次,徹底弄懂 JavaScript 執行機制
人間不正經生活手冊轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/549589.html
標籤:其他
下一篇:Echarts中的Map學習記錄
