道理講再多,不如帶你真真切切的踩個坑。以下圖所示,一個門店後廚的統計頁面,在第一個「後廚出菜統計」模塊,要繪製每一個廚師出菜比例的餅圖。javascript
既然Vue是雙向數據綁定的,咱們直接把響應數據賦值給data後,視圖理應就會更新,可咱們緊接着經過DOM對象繪製圖表的時候,卻報錯找不到DOM元素。顯然Vue的數據賦值後,沒有當即去更新DOM。html
<template>
<div>
<p class="title">後廚出菜統計</p>
</div>
<div class="cook-item" v-for="(cook,index) in cookList" :key="'chef_list_'+cook.oid">
<!-- 出菜信息 -->
<div></div>
<!-- 出菜圖表 -->
<div style="flex: 1;background-color: #f8f8f8;border-radius: 6px;">
<canvas :id="'pie_' + cook.oid"></canvas>
</div>
</div>
</template>
<script> import F2 from '@antv/f2'; const axios = require('axios').default; // 動態構造餅圖 function makePieChart(list, containerId) { // .... const pieChart = new F2.Chart({ id: containerId, pixelRatio: window.devicePixelRatio, padding: [20, 'auto'] }); // ... pieChart.render(); } export default { name: 'cookReport', props: { storeCode: String, // 門店編碼 date: String // 統計日期 }, data() { return { cookList: [] } }, mounted() { // 掛載完成後,發起請求數據 axios.get(`${process.env.VUE_APP_BASE_URL}/report?date=${this.date}&storeCode=${this.storeCode}`).then(res => { // 賦值 this.cookList = res.data // 直接調用 this.cookList.forEach(cook => { makePieChart(cook.pieDataList, `pie_${cook.oid}`) }) }) } } </script>
複製代碼
F2圖表繪製報錯:vue
Uncaught (in promise) TypeError: Cannot read property 'currentStyle' of null
at getStyle (f2.js?e004:765)
at getWidth (f2.js?e004:769)
at Canvas._initCanvas (f2.js?e004:9848)
at new Canvas (f2.js?e004:9801)
at createCanvas (f2.js?e004:10031)
at Chart._initCanvas (f2.js?e004:10490)
at Chart._init (f2.js?e004:10567)
at new Chart (f2.js?e004:10606)
at makePieChart (StoreReport.vue?8e92:161)
at eval (StoreReport.vue?8e92:293)
複製代碼
這是由於Vue採起的是異步更新策略,咱們把圖表繪製的代碼寫在nextTick裏,能正常運行了。java
mounted() {
// 掛載完成後,發起請求數據
axios.get(`${process.env.VUE_APP_BASE_URL}/report?date=${this.date}&storeCode=${this.storeCode}`).then(res => {
// 賦值
this.cookList = res.data
// 等待DOM更新完成後,再調用
this.$nextTick(() => {
this.cookList.forEach(cook => {
makePieChart(cook.pieDataList, `pie_${cook.oid}`)
})
});
})
}
複製代碼
nextTick()
的本質在下次 DOM 更新循環結束以後執行延遲迴調。在修改數據以後當即使用這個方法,獲取更新後的 DOM。node
那麼問題來了,咱們不由要思考nextTick回調函數的本質是什麼?react
nextTick()
解答第一個問題,就不得不提事件循環機制。ios
衆所周知,事件循環(EventLoop)是JS協調異步程序的機制,異步程序中又分爲宏任務和微任務,宏任務一般是和瀏覽器級別的,好比定時器/輪詢器/XHR等;微任務一般是ECMAScript級別的,好比promise/await+async/mutationObserver/processe.nextTick等; 宏任務和微任務都是以隊列(queue)的數據結構存在,宿主環境輪詢檢查執行棧(CallStack)的清空狀況,而後逐個放入宏任務執行,當前宏任務完後執行緊接着會執行微任務。git
如今能夠回答第一個問題了,每執行一個宏任務,就是一個tick。而nextTick()
在語境上,能夠理解成"在下一個宏任務執行以前的回調函數"。github
nextTick()
經過MutataionObsever監聽DOM的更新接着解答第二個問題,DOM的增刪改查操做是同步執行的,對於DOM變化的監聽提供的MutationObsever API是異步的,而且屬於微任務。 一個簡單的MutationObsever示例:canvas
<body>
<div id="container"></div>
<script> // 廚師列表 let cookList = [{ name: '斯蒂芬周', oid: 10081 },{ name: '雞姐', oid: 10082 },{ name: '唐牛', oid: 10083 }]; // 模板容器 let $container = document.getElementById('container'); // 當容器內的chart DOM插入後,打印日誌 new MutationObserver(() => { console.log('大廚已就緒,能夠進行下一步圖表繪製工做!') }).observe($container, { childList: true, subtree: true }); // 遍歷數組,插入chart DIV cookList.forEach(cook => { let cookDiv = document.createElement('div'); cookDiv.setAttribute('id','cook_' + cook.oid); cookDiv.textContent = cook.name; $container.appendChild(cookDiv); }); </script>
</body>
複製代碼
nextTick()
本質就是往微任務隊列中追加執行函數接下來看第三個問題:若是像生命週期函數同樣,提早註冊nextTick是否可行呢?答案是否認的
mounted() {
// 先註冊nextTick
this.$nextTick(() => {
console.log('DOM ready')
});
axios.get(`${process.env.VUE_APP_BASE_URL}/report?date=${this.date}&storeCode=${this.storeCode}`).then(res => {
// 響應後對數據進行賦值
this.cookList = res.data
console.log('data assign')
})
}
複製代碼
輸出的結果:沒等數據賦值,nextTick()
就先執行了
> DOM ready
> data assign
複製代碼
結論:MutataionObsever
和nextTick
都是微任務,在隊列中按順序執行。因此在業務代碼中,數據賦值與nextTick()
老是成對出現,而且nextTick
必須在數據賦值後面,不能像鉤子函數同樣提早註冊。
nextTick
與process.nextTick()
的區別process.nextTick
是node.js的API,和Vue.nextTick
同樣,都是往微任務追加執行函數
nextTick
與jQuery.ready()
的區別當我和後端小哥用廚師圖表渲染的例子,講解nextTick
時,他當下反應是,「哦!這個我知道,和jQuery.ready()同樣嘛!」,我當時是一臉的黑人問號表情。這二者仍是不同的,jQuery.ready()
是jQuery實例掛載到window的回調函數,函數裏面就能夠經過jQuery來操做DOM了。而Vue.nextTick()
是響應數據綁定的視圖更新後的回調函數,函數裏面能夠操做DOM。
Vue.nextTick
與組件內this.nextTick
的區別當前組件和與全局Vue實例都指向src/core/util/next-tick.js
同一個nextTick()
函數
nextTick
與requestAnimationFrame()
的關係仍是要回到EventLoop機制:
Microtask
),若是有,則輪詢執行,直到清空微任務隊列(包含嵌套產生的微任務);可見,nextTick只是微任務隊列中普通的函數,就是按順序執行。而requestAnimationFrame()
取決於本次輪詢是否要進行更新渲染,在須要更新渲染前,調用執行。
/* @flow */
/* globals MutationObserver */
import { noop } from 'shared/util'
import { handleError } from './error'
import { isIE, isIOS, isNative } from './env'
// 標識-是否使用微任務執行回調函數
export let isUsingMicroTask = false
// 數組-回調函數列表
const callbacks = []
// 標識-是否有回調函數在執行中
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]()
}
}
// 定義主邏輯的timerFunc方法
let timerFunc
if (typeof Promise !== 'undefined' && isNative(Promise)) {
/** * 判斷是否支持Promise */
const p = Promise.resolve()
// 以Promise.then來處理flushCallbacks函數
timerFunc = () => {
p.then(flushCallbacks)
if (isIOS) setTimeout(noop)
}
// Promise屬於微任務,進行標識
isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
/** * 判斷是否支持MutationObsever */
let counter = 1
// 以MutationObsever的監聽來處理flushCallbacks函數
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)
}
// MutationObsever也屬於微任務,進行標識
isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
/** * 判斷是否支持setImmediate */
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
/** * 以上方法都不支持,則經過定時器的宏任務來處理 */
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
// 對外暴露nextTick方法,傳遞callback回調函數,以及執行環境
export function nextTick (cb?: Function, ctx?: Object) {
let _resolve
// 將回調函數包裝後,放入callbacks數組
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
if (!pending) {
// 有微任務在執行時,先設置pending爲true進行等待
pending = true
timerFunc()
}
// 若是支持Promise,對外返回Promise類型
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
複製代碼
next-tick.js
文件對外只暴露一個nextTick()
方法,方法的參數就是業務代碼中的回調函數;nextTick()
,傳入的回調函數都會放入一個callbacks
數組中,而後執行timerFun()
異步方法;timerFun()
對環境進行判斷,是否支持微任務對象Promise/MutationObsever,若是不支持則經過setImmediate/setTimeout宏任務包裝成異步方法,回調處理flushCallbacks()
;flushCallback()
如方法名所寫,作的就是按順序遍歷執行每一個回調函數,並清空數組;nextTick
爲何不直接放到下一個宏任務,而是優先放入微任務隊列?queueMicrotask()
也是往微任務隊列中追加執行函數,nextTick
爲何要用Promise來實現?