自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

從Javascript 事件循環(huán)看 Vue.nextTick 的原理和執(zhí)行機(jī)制

開發(fā) 前端
Vue 的特點(diǎn)之一就是響應(yīng)式,但是有些時(shí)候數(shù)據(jù)更新了,我們看到頁面上的 DOM 并沒有立刻更新。如果我們需要在 DOM 更新之后再執(zhí)行一段代碼時(shí),可以借助 nextTick 實(shí)現(xiàn)。

拋磚引玉

Vue 的特點(diǎn)之一就是響應(yīng)式,但是有些時(shí)候數(shù)據(jù)更新了,我們看到頁面上的 DOM 并沒有立刻更新。如果我們需要在 DOM 更新之后再執(zhí)行一段代碼時(shí),可以借助 nextTick 實(shí)現(xiàn)。

[[323732]]

我們先來看一個(gè)例子

  1. export default { 
  2.   data() { 
  3.     return { 
  4.       msg: 0 
  5.     } 
  6.   }, 
  7.   mounted() { 
  8.     this.msg = 1 
  9.     this.msg = 2 
  10.     this.msg = 3 
  11.   }, 
  12.   watch: { 
  13.     msg() { 
  14.       console.log(this.msg) 
  15.     } 
  16.   } 

這里的結(jié)果是只輸出一個(gè) 3,而非依次輸出 1,2,3。這是為什么呢?

vue 的官方文檔是這樣解釋的:

Vue 異步執(zhí)行 DOM 更新。只要觀察到數(shù)據(jù)變化,Vue 將開啟一個(gè)隊(duì)列,并緩沖在同一事件循環(huán)中發(fā)生的所有數(shù)據(jù)改變。如果同一個(gè)watcher 被多次觸發(fā),只會被推入到隊(duì)列中一次。這種在緩沖時(shí)去除重復(fù)數(shù)據(jù)對于避免不必要的計(jì)算和 DOM 操作上非常重要。然后,在下一個(gè)的事件循環(huán)“tick”中,Vue 刷新隊(duì)列并執(zhí)行實(shí)際 (已去重的) 工作。Vue 在內(nèi)部嘗試對異步隊(duì)列使用原生的Promise.then和 MessageChannel,如果執(zhí)行環(huán)境不支持,會采用setTimeout(fn, 0)代替。

假如有這樣一種情況,mounted鉤子函數(shù)下一個(gè)變量 a 的值會被++循環(huán)執(zhí)行 1000 次。每次++時(shí),都會根據(jù)響應(yīng)式觸發(fā)setter->Dep->Watcher->update->run。如果這時(shí)候沒有異步更新視圖,那么每次++都會直接操作 DOM 一次,這是非常消耗性能的。所以 Vue 實(shí)現(xiàn)了一個(gè)queue隊(duì)列,在下一個(gè) Tick(或者是當(dāng)前 Tick 的微任務(wù)階段)的時(shí)候會統(tǒng)一執(zhí)行queue中Watcher的run。同時(shí),擁有相同 id 的Watcher不會被重復(fù)加入到該queue中去,所以不會執(zhí)行 1000 次Watcher的run。最終的結(jié)果是直接把 a 的值從 1 變成 1000,大大提升了性能。

在 vue 中,數(shù)據(jù)監(jiān)測都是通過Object.defineProperty來重寫里面的 set 和 get 方法實(shí)現(xiàn)的,vue 更新 DOM 是異步的,每當(dāng)觀察到數(shù)據(jù)變化時(shí),vue 就開始一個(gè)隊(duì)列,將同一事件循環(huán)內(nèi)所有的數(shù)據(jù)變化緩存起來,等到下一次 eventLoop,將會把隊(duì)列清空,進(jìn)行 DOM 更新。

想要了解 vue.nextTick 的執(zhí)行機(jī)制,我們先來了解一下 javascript 的事件循環(huán)。

js 事件循環(huán)

js 的任務(wù)隊(duì)列分為同步任務(wù)和異步任務(wù),所有的同步任務(wù)都是在主線程里執(zhí)行的。異步任務(wù)可能會在 macrotask 或者 microtask 里面,異步任務(wù)進(jìn)入 Event Table 并注冊函數(shù)。當(dāng)指定的事情完成時(shí),Event Table 會將這個(gè)函數(shù)移入 Event Queue。主線程內(nèi)的任務(wù)執(zhí)行完畢為空,會去 Event Queue 讀取對應(yīng)的函數(shù),進(jìn)入主線程執(zhí)行。上述過程會不斷重復(fù),也就是常說的 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í)行,會在一個(gè)(macro)task執(zhí)行結(jié)束后,在下一個(gè)(macro)task執(zhí)行開始前,對頁面進(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ù)。也就是說,在當(dāng)前 task 任務(wù)后,下一個(gè) task 之前,在渲染之前。所以它的響應(yīng)速度相比 setTimeout(setTimeout 是 task)會更快,因?yàn)闊o需等渲染。也就是說,在某一個(gè) macrotask 執(zhí)行完后,就會將在它執(zhí)行期間產(chǎn)生的所有 microtask 都執(zhí)行完畢(在渲染前)。microtask 主要包含:

  • process.nextTick(Node.js 環(huán)境)
  • Promise
  • Async/Await
  • MutationObserver(html5 新特性)

3. 小結(jié)

  • 先執(zhí)行主線程
  • 遇到宏隊(duì)列(macrotask)放到宏隊(duì)列(macrotask)
  • 遇到微隊(duì)列(microtask)放到微隊(duì)列(microtask)
  • 主線程執(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)出場了,它接受一個(gè)回調(diào)函數(shù),在 DOM 更新完成后,這個(gè)回調(diào)函數(shù)就會被調(diào)用。不管是 vue.nextTick 還是vue.prototype.\$nextTick 都是直接用的nextTick這個(gè)閉包函數(shù)。

  1. export const nextTick = (function () { 
  2.   const callbacks = [] 
  3.   let pending = false 
  4.   let timerFunc 
  5.  
  6.   function nextTickHandler () { 
  7.     pending = false 
  8.     const copies = callbacks.slice(0) 
  9.     callbacks.length = 0 
  10.     for (let i = 0; i < copies.length; i++) { 
  11.       copies[i]() 
  12.     } 
  13.   } 
  14.  ... 
  15. })() 

使用數(shù)組callbacks保存回調(diào)函數(shù),pending表示當(dāng)前狀態(tài),使用函數(shù)nextTickHandler 來執(zhí)行回調(diào)隊(duì)列。在該方法內(nèi),先通過slice(0)保存了回調(diào)隊(duì)列的一個(gè)副本,通過設(shè)置 callbacks.length = 0清空回調(diào)隊(duì)列,最后使用循環(huán)執(zhí)行在副本里的所有函數(shù)。

  1. if (typeof Promise !== 'undefined' && isNative(Promise)) { 
  2.   var p = Promise.resolve() 
  3.   var logError = err => { 
  4.     console.error(err) 
  5.   } 
  6.   timerFunc = () => { 
  7.     p.then(nextTickHandler).catch(logError) 
  8.     if (isIOS) setTimeout(noop) 
  9.   } 
  10. } else if (typeof MutationObserver !== 'undefined' && (isNative(MutationObserver) || MutationObserver.toString() === '[object MutationObserverConstructor]')) { 
  11.   var counter = 1 
  12.   var observer = new MutationObserver(nextTickHandler) 
  13.   var textNode = document.createTextNode(String(counter)) 
  14.   observer.observe(textNode, { 
  15.     characterData: true 
  16.   }) 
  17.   timerFunc = () => { 
  18.     counter = (counter + 1) % 2 
  19.     textNode.data = String(counter) 
  20.   } 
  21. } else { 
  22.   timeFunc = () => { 

隊(duì)列控制的最佳選擇是microtask,而microtask的最佳選擇是Promise。但如果當(dāng)前環(huán)境不支持 Promise,就檢測到瀏覽器是否支持 MO,是則創(chuàng)建一個(gè)文本節(jié)點(diǎn),監(jiān)聽這個(gè)文本節(jié)點(diǎn)的改動(dòng)事件,以此來觸發(fā)nextTickHandler(也就是 DOM 更新完畢回調(diào))的執(zhí)行。此外因?yàn)榧嫒菪詥栴},vue 不得不做了microtask向macrotask 的降級方案。

為讓這個(gè)回調(diào)函數(shù)延遲執(zhí)行,vue 優(yōu)先用promise來實(shí)現(xiàn),其次是 html5 的 MutationObserver,然后是setTimeout。前兩者屬于microtask,后一個(gè)屬于 macrotask。下面來看最后一部分。

  1. return function queueNextTick(cb?: Function, ctx?: Object) { 
  2.   let _resolve 
  3.   callbacks.push(() => { 
  4.     if (cb) cb.call(ctx) 
  5.     if (_resolve) _resolve(ctx) 
  6.   }) 
  7.   if (!pending) { 
  8.     pending = true 
  9.     timerFunc() 
  10.   } 
  11.   if (!cb && typeof Promise !== 'undefined') { 
  12.     return new Promise(resolve => { 
  13.       _resolve = resolve 
  14.     }) 
  15.   } 

這就是我們真正調(diào)用的nextTick函數(shù),在一個(gè)event loop內(nèi)它會將調(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 方式去書寫(暫且用的較少)。

應(yīng)用場景

場景一、點(diǎn)擊按鈕顯示原本以 v-show = false 隱藏起來的輸入框,并獲取焦點(diǎn)。

  1. <input id="keywords" v-if="showit"> 
  2.  
  3. showInput(){ 
  4.   this.showit = true 
  5.   document.getElementById("keywords").focus() 

以上的寫法在第一個(gè) tick 里,因?yàn)楂@取不到輸入框,自然也獲取不到焦點(diǎn)。如果我們改成以下的寫法,在 DOM 更新后就可以獲取到輸入框焦點(diǎn)了。

  1. showsou(){ 
  2.   this.showit = true 
  3.   this.$nextTick(function () { 
  4.     // DOM 更新了 
  5.     document.getElementById("keywords").focus() 
  6.   }) 

場景二、獲取元素屬,點(diǎn)擊獲取元素寬度。

  1. <div id="app"> 
  2.   <p ref="myWidth" v-if="showMe">{{ message }}</p> 
  3.   <button @click="getMyWidth">獲取p元素寬度</button> 
  4. </div> 
  5.  
  6. getMyWidth() { 
  7.   this.showMe = true
  8.   thisthis.message = this.$refs.myWidth.offsetWidth; 
  9.   //報(bào)錯(cuò) TypeError: this.$refs.myWidth is undefined 
  10.   this.$nextTick(()=>
  11.       //dom元素更新后執(zhí)行,此時(shí)能拿到p元素的屬性 
  12.     thisthis.message = this.$refs.myWidth.offsetWidth; 
  13.   }) 

 

責(zé)任編輯:趙寧寧 來源: 前端先鋒隊(duì)
相關(guān)推薦

2020-09-21 14:35:20

VuenextTick前端

2017-09-12 09:50:08

JavaScriptEvent LoopVue.js

2017-02-09 15:15:54

Chrome瀏覽器

2024-08-26 14:52:58

JavaScript循環(huán)機(jī)制

2016-09-06 21:23:25

JavaScriptnode異步

2021-10-15 09:56:10

JavaScript異步編程

2020-12-29 08:21:03

JavaScript微任務(wù)宏任務(wù)

2024-06-21 08:32:24

2021-01-18 08:24:51

JavaScriptMicrotask微任務(wù)

2017-07-27 16:31:11

2010-07-16 09:00:20

開源RedOffice紅旗2000

2015-09-21 14:20:35

2024-09-20 05:46:00

2022-04-25 09:03:16

JavaScript代碼

2017-06-29 09:15:36

推薦算法策略

2009-03-17 15:36:29

JavaScript循環(huán)事件

2021-12-08 07:55:41

EventLoop瀏覽器事件

2022-09-19 19:51:30

ReactuseEffect

2022-07-07 09:12:17

JavaScript線程循環(huán)機(jī)制

2010-01-28 10:29:44

點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號