今天就跟大家聊聊有關(guān)如何從Javascript事件循環(huán)看出Vue.nextTick的原理和執(zhí)行機(jī)制,可能很多人都不太了解,為了讓大家更加了解,小編給大家總結(jié)了以下內(nèi)容,希望大家根據(jù)這篇文章可以有所收獲。
創(chuàng)新互聯(lián)公司成立以來(lái)不斷整合自身及行業(yè)資源、不斷突破觀念以使企業(yè)策略得到完善和成熟,建立了一套“以技術(shù)為基點(diǎn),以客戶(hù)需求中心、市場(chǎng)為導(dǎo)向”的快速反應(yīng)體系。對(duì)公司的主營(yíng)項(xiàng)目,如中高端企業(yè)網(wǎng)站企劃 / 設(shè)計(jì)、行業(yè) / 企業(yè)門(mén)戶(hù)設(shè)計(jì)推廣、行業(yè)門(mén)戶(hù)平臺(tái)運(yùn)營(yíng)、重慶APP開(kāi)發(fā)公司、手機(jī)網(wǎng)站開(kāi)發(fā)、微信網(wǎng)站制作、軟件開(kāi)發(fā)、成都服務(wù)器托管等實(shí)行標(biāo)準(zhǔn)化操作,讓客戶(hù)可以直觀的預(yù)知到從創(chuàng)新互聯(lián)公司可以獲得的服務(wù)效果。
拋磚引玉
Vue 的特點(diǎn)之一就是響應(yīng)式,但是有些時(shí)候數(shù)據(jù)更新了,我們看到頁(yè)面上的 DOM 并沒(méi)有立刻更新。如果我們需要在 DOM 更新之后再執(zhí)行一段代碼時(shí),可以借助 nextTick 實(shí)現(xiàn)。
我們先來(lái)看一個(gè)例子
export default { data() { return { msg: 0 } }, mounted() { this.msg = 1 this.msg = 2 this.msg = 3 }, watch: { msg() { console.log(this.msg) } } }
這里的結(jié)果是只輸出一個(gè) 3,而非依次輸出 1,2,3。這是為什么呢?
vue 的官方文檔是這樣解釋的:
Vue 異步執(zhí)行 DOM 更新。只要觀察到數(shù)據(jù)變化,Vue 將開(kāi)啟一個(gè)隊(duì)列,并緩沖在同一事件循環(huán)中發(fā)生的所有數(shù)據(jù)改變。如果同一個(gè)watcher 被多次觸發(fā),只會(huì)被推入到隊(duì)列中一次。這種在緩沖時(shí)去除重復(fù)數(shù)據(jù)對(duì)于避免不必要的計(jì)算和 DOM 操作上非常重要。然后,在下一個(gè)的事件循環(huán)“tick”中,Vue 刷新隊(duì)列并執(zhí)行實(shí)際 (已去重的) 工作。Vue 在內(nèi)部嘗試對(duì)異步隊(duì)列使用原生的Promise.then和 MessageChannel,如果執(zhí)行環(huán)境不支持,會(huì)采用setTimeout(fn, 0)代替。 |
假如有這樣一種情況,mounted鉤子函數(shù)下一個(gè)變量 a 的值會(huì)被++循環(huán)執(zhí)行 1000 次。每次++時(shí),都會(huì)根據(jù)響應(yīng)式觸發(fā)setter->Dep->Watcher->update->run。如果這時(shí)候沒(méi)有異步更新視圖,那么每次++都會(huì)直接操作 DOM 一次,這是非常消耗性能的。所以 Vue 實(shí)現(xiàn)了一個(gè)queue隊(duì)列,在下一個(gè) Tick(或者是當(dāng)前 Tick 的微任務(wù)階段)的時(shí)候會(huì)統(tǒng)一執(zhí)行queue中Watcher的run。同時(shí),擁有相同 id 的Watcher不會(huì)被重復(fù)加入到該queue中去,所以不會(huì)執(zhí)行 1000 次Watcher的run。最終的結(jié)果是直接把 a 的值從 1 變成 1000,大大提升了性能。
在 vue 中,數(shù)據(jù)監(jiān)測(cè)都是通過(guò)Object.defineProperty來(lái)重寫(xiě)里面的 set 和 get 方法實(shí)現(xiàn)的,vue 更新 DOM 是異步的,每當(dāng)觀察到數(shù)據(jù)變化時(shí),vue 就開(kāi)始一個(gè)隊(duì)列,將同一事件循環(huán)內(nèi)所有的數(shù)據(jù)變化緩存起來(lái),等到下一次 eventLoop,將會(huì)把隊(duì)列清空,進(jìn)行 DOM 更新。
想要了解 vue.nextTick 的執(zhí)行機(jī)制,我們先來(lái)了解一下 javascript 的事件循環(huán)。
js 事件循環(huán)
js 的任務(wù)隊(duì)列分為同步任務(wù)和異步任務(wù),所有的同步任務(wù)都是在主線(xiàn)程里執(zhí)行的。異步任務(wù)可能會(huì)在 macrotask 或者 microtask 里面,異步任務(wù)進(jìn)入 Event Table 并注冊(cè)函數(shù)。當(dāng)指定的事情完成時(shí),Event Table 會(huì)將這個(gè)函數(shù)移入 Event Queue。主線(xiàn)程內(nèi)的任務(wù)執(zhí)行完畢為空,會(huì)去 Event Queue 讀取對(duì)應(yīng)的函數(shù),進(jìn)入主線(xiàn)程執(zhí)行。上述過(guò)程會(huì)不斷重復(fù),也就是常說(shuō)的 Event Loop(事件循環(huán))。
1. macro-task(宏任務(wù)):
每次執(zhí)行棧執(zhí)行的代碼就是一個(gè)宏任務(wù)(包括每次從事件隊(duì)列中獲取一個(gè)事件回調(diào)并放到執(zhí)行棧中執(zhí)行)。瀏覽器為了能夠使得 js 內(nèi)部(macro)task與 DOM 任務(wù)能夠有序執(zhí)行,會(huì)在一個(gè)(macro)task執(zhí)行結(jié)束后,在下一個(gè)(macro)task執(zhí)行開(kāi)始前,對(duì)頁(yè)面進(jìn)行重新渲染。宏任務(wù)主要包含:
script(整體代碼)
setTimeout / setInterval
setImmediate(Node.js 環(huán)境)
I/O
UI render
postMessage
MessageChannel
2. micro-task(微任務(wù)):
可以理解是在當(dāng)前 task 執(zhí)行結(jié)束后立即執(zhí)行的任務(wù)。也就是說(shuō),在當(dāng)前 task 任務(wù)后,下一個(gè) task 之前,在渲染之前。所以它的響應(yīng)速度相比 setTimeout(setTimeout 是 task)會(huì)更快,因?yàn)闊o(wú)需等渲染。也就是說(shuō),在某一個(gè) macrotask 執(zhí)行完后,就會(huì)將在它執(zhí)行期間產(chǎn)生的所有 microtask 都執(zhí)行完畢(在渲染前)。microtask 主要包含:
process.nextTick(Node.js 環(huán)境)
Promise
Async/Await
MutationObserver(html5 新特性)
3. 小結(jié)
先執(zhí)行主線(xiàn)程
遇到宏隊(duì)列(macrotask)放到宏隊(duì)列(macrotask)
遇到微隊(duì)列(microtask)放到微隊(duì)列(microtask)
主線(xiàn)程執(zhí)行完畢
執(zhí)行微隊(duì)列(microtask),微隊(duì)列(microtask)執(zhí)行完畢
執(zhí)行一次宏隊(duì)列(macrotask)中的一個(gè)任務(wù),執(zhí)行完畢
執(zhí)行微隊(duì)列(microtask),執(zhí)行完畢
依次循環(huán)。。。
Vue.nextTick 源碼
vue 是采用雙向數(shù)據(jù)綁定的方法驅(qū)動(dòng)數(shù)據(jù)更新的,雖然這樣能避免直接操作 DOM,提高了性能,但有時(shí)我們也不可避免需要操作 DOM,這時(shí)就該 Vue.nextTick(callback)出場(chǎng)了,它接受一個(gè)回調(diào)函數(shù),在 DOM 更新完成后,這個(gè)回調(diào)函數(shù)就會(huì)被調(diào)用。不管是 vue.nextTick 還是vue.prototype.\$nextTick 都是直接用的nextTick這個(gè)閉包函數(shù)。
export const nextTick = (function () { const callbacks = [] let pending = false let timerFunc function nextTickHandler () { pending = false const copies = callbacks.slice(0) callbacks.length = 0 for (let i = 0; i < copies.length; i++) { copies[i]() } } ... })()
使用數(shù)組callbacks保存回調(diào)函數(shù),pending表示當(dāng)前狀態(tài),使用函數(shù)nextTickHandler 來(lái)執(zhí)行回調(diào)隊(duì)列。在該方法內(nèi),先通過(guò)slice(0)保存了回調(diào)隊(duì)列的一個(gè)副本,通過(guò)設(shè)置 callbacks.length = 0清空回調(diào)隊(duì)列,最后使用循環(huán)執(zhí)行在副本里的所有函數(shù)。
if (typeof Promise !== 'undefined' && isNative(Promise)) { var p = Promise.resolve() var logError = err => { console.error(err) } timerFunc = () => { p.then(nextTickHandler).catch(logError) if (isIOS) setTimeout(noop) } } else if (typeof MutationObserver !== 'undefined' && (isNative(MutationObserver) || MutationObserver.toString() === '[object MutationObserverConstructor]')) { var counter = 1 var observer = new MutationObserver(nextTickHandler) var textNode = document.createTextNode(String(counter)) observer.observe(textNode, { characterData: true }) timerFunc = () => { counter = (counter + 1) % 2 textNode.data = String(counter) } } else { timeFunc = () => {
隊(duì)列控制的最佳選擇是microtask,而microtask的最佳選擇是Promise。但如果當(dāng)前環(huán)境不支持 Promise,就檢測(cè)到瀏覽器是否支持 MO,是則創(chuàng)建一個(gè)文本節(jié)點(diǎn),監(jiān)聽(tīng)這個(gè)文本節(jié)點(diǎn)的改動(dòng)事件,以此來(lái)觸發(fā)nextTickHandler(也就是 DOM 更新完畢回調(diào))的執(zhí)行。此外因?yàn)榧嫒菪詥?wèn)題,vue 不得不做了microtask向macrotask 的降級(jí)方案。
為讓這個(gè)回調(diào)函數(shù)延遲執(zhí)行,vue 優(yōu)先用promise來(lái)實(shí)現(xiàn),其次是 html5 的 MutationObserver,然后是setTimeout。前兩者屬于microtask,后一個(gè)屬于 macrotask。下面來(lái)看最后一部分。
return function queueNextTick(cb?: Function, ctx?: Object) { let _resolve callbacks.push(() => { if (cb) cb.call(ctx) if (_resolve) _resolve(ctx) }) if (!pending) { pending = true timerFunc() } if (!cb && typeof Promise !== 'undefined') { return new Promise(resolve => { _resolve = resolve }) } }
這就是我們真正調(diào)用的nextTick函數(shù),在一個(gè)event loop內(nèi)它會(huì)將調(diào)用 nextTick的cb 回調(diào)函數(shù)都放入 callbacks 中,pending 用于判斷是否有隊(duì)列正在執(zhí)行回調(diào),例如有可能在 nextTick 中還有一個(gè) nextTick,此時(shí)就應(yīng)該屬于下一個(gè)循環(huán)了。最后幾行代碼是 promise 化,可以將 nextTick 按照 promise 方式去書(shū)寫(xiě)(暫且用的較少)。
應(yīng)用場(chǎng)景
場(chǎng)景一、點(diǎn)擊按鈕顯示原本以 v-show = false 隱藏起來(lái)的輸入框,并獲取焦點(diǎn)。
<input id="keywords" v-if="showit"> showInput(){ this.showit = true document.getElementById("keywords").focus() }
以上的寫(xiě)法在第一個(gè) tick 里,因?yàn)楂@取不到輸入框,自然也獲取不到焦點(diǎn)。如果我們改成以下的寫(xiě)法,在 DOM 更新后就可以獲取到輸入框焦點(diǎn)了。
showsou(){ this.showit = true this.$nextTick(function () { // DOM 更新了 document.getElementById("keywords").focus() }) }
場(chǎng)景二、獲取元素屬,點(diǎn)擊獲取元素寬度。
<div id="app"> <p ref="myWidth" v-if="showMe">{{ message }}</p> <button @click="getMyWidth">獲取p元素寬度</button> </div> getMyWidth() { this.showMe = true; thisthis.message = this.$refs.myWidth.offsetWidth; //報(bào)錯(cuò) TypeError: this.$refs.myWidth is undefined this.$nextTick(()=>{ //dom元素更新后執(zhí)行,此時(shí)能拿到p元素的屬性 thisthis.message = this.$refs.myWidth.offsetWidth; }) }
看完上述內(nèi)容,你們對(duì)如何從Javascript事件循環(huán)看出Vue.nextTick的原理和執(zhí)行機(jī)制有進(jìn)一步的了解嗎?如果還想了解更多知識(shí)或者相關(guān)內(nèi)容,請(qǐng)關(guān)注創(chuàng)新互聯(lián)行業(yè)資訊頻道,感謝大家的支持。
名稱(chēng)欄目:如何從Javascript事件循環(huán)看出Vue.nextTick的原理和執(zhí)行機(jī)制
文章轉(zhuǎn)載:http://www.rwnh.cn/article32/gspopc.html
成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供電子商務(wù)、微信小程序、全網(wǎng)營(yíng)銷(xiāo)推廣、網(wǎng)站建設(shè)、網(wǎng)站制作、定制網(wǎng)站
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶(hù)投稿、用戶(hù)轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話(huà):028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來(lái)源: 創(chuàng)新互聯(lián)