vue源碼之批量異步更新策略的深入解析
vue異步更新源碼中會有涉及事件循環(huán)、宏任務(wù)、微任務(wù)的概念,所以先了解一下這幾個概念。
一、事件循環(huán)、宏任務(wù)、微任務(wù)
1.事件循環(huán)Event Loop:瀏覽器為了協(xié)調(diào)事件處理、腳本執(zhí)行、網(wǎng)絡(luò)請求和渲染等任務(wù)而定制的工作機(jī)制。
2.宏任務(wù)Task: 代表一個個離散的、獨(dú)立的工作單位。瀏覽器完成一個宏任務(wù),在下一個宏任務(wù)開始執(zhí)行之前,會對頁面重新渲染。主要包括創(chuàng)建文檔對象、解析HTML、執(zhí)行主線JS代碼以及各種事件如頁面加載、輸入、網(wǎng)絡(luò)事件和定時器等。
3.微任務(wù):微任務(wù)是更小的任務(wù),是在當(dāng)前宏任務(wù)執(zhí)行結(jié)束后立即執(zhí)行的任務(wù)。如果存在微任務(wù),瀏覽器會在完成微任務(wù)之后再重新渲染。微任務(wù)的例子有Promise回調(diào)函數(shù)、DOM變化等。
執(zhí)行過程:執(zhí)行完宏任務(wù) => 執(zhí)行微任務(wù) => 頁面重新渲染 => 再執(zhí)行新一輪宏任務(wù)
任務(wù)執(zhí)行順序例子:
//第一個宏任務(wù)進(jìn)入主線程 console.log('1'); //丟到宏事件隊列中 setTimeout(function() { console.log('2'); process.nextTick(function() { console.log('3'); }) new Promise(function(resolve) { console.log('4'); resolve(); }).then(function() { console.log('5') }) }) //微事件1 process.nextTick(function() { console.log('6'); }) //主線程直接執(zhí)行 new Promise(function(resolve) { console.log('7'); resolve(); }).then(function() { //微事件2 console.log('8') }) //丟到宏事件隊列中 setTimeout(function() { console.log('9'); process.nextTick(function() { console.log('10'); }) new Promise(function(resolve) { console.log('11'); resolve(); }).then(function() { console.log('12') }) }) // 1,7,6,8,2,4,3,5,9,11,10,12
解析:
第一個宏任務(wù)
- 第一個宏任務(wù)進(jìn)入主線程,打印1
- setTimeout丟到宏任務(wù)隊列
- process.nextTick丟到微任務(wù)隊列
- new Promise直接執(zhí)行,打印7
- Promise then事件丟到微任務(wù)隊列
- setTimeout丟到宏任務(wù)隊列
第一個宏任務(wù)執(zhí)行完,開始執(zhí)行微任務(wù)
- 執(zhí)行process.nextTick,打印6
- 執(zhí)行Promise then事件,打印8
微任務(wù)執(zhí)行完,清空微任務(wù)隊列,頁面渲染,進(jìn)入下一個宏任務(wù)setTimeout
- 執(zhí)行打印2
- process.nextTick丟到微任務(wù)隊列
- new Promise直接執(zhí)行,打印4
- Promise then事件丟到微任務(wù)隊列
第二個宏任務(wù)執(zhí)行完,開始執(zhí)行微任務(wù)
- 執(zhí)行process.nextTick,打印3
- 執(zhí)行Promise then事件,打印5
微任務(wù)執(zhí)行完,清空微任務(wù)隊列,頁面渲染,進(jìn)入下一個宏任務(wù)setTimeout,重復(fù)上述類似流程,打印出9,11,10,12
二、Vue異步批量更新過程
1.解析:當(dāng)偵測到數(shù)據(jù)變化,vue會開啟一個隊列,將相關(guān)的watcher存入隊列,將回調(diào)函數(shù)存入callbacks隊列,異步執(zhí)行回調(diào)函數(shù),遍歷watcher隊列進(jìn)行渲染。
異步:Vue 在更新 DOM 時是異步執(zhí)行的,只要偵聽到數(shù)據(jù)變化,vue將開啟一個隊列,并緩沖 在同一事件循環(huán)中發(fā)生的所有數(shù)據(jù) 的變更。
批量:如果同一個watcher被多次觸發(fā),只會被推入到隊列中一次。去重可以避免不必要的計算和DOM操作。然后在下一個的事件循環(huán)“tick”中,vue刷新隊列執(zhí)行實際工作。
異步策略:Vue的內(nèi)部對異步隊列嘗試使用原生的Promise.then、MutationObserver和 setImmediate,如果執(zhí)行環(huán)境不支持,則會采用 setTimeout(fn, 0) 代替。即會先嘗試使用微任務(wù)方式,不行再用宏任務(wù)方式。
異步批量更新流程圖:
三、vue批量異步更新源碼
異步更新:整個過程相當(dāng)于將臭襪子放到盆子里,最后一起洗。
1.當(dāng)一個Data更新時,會依次執(zhí)行以下代碼:
(1)觸發(fā)Data.set()
(2)調(diào)用dep.notify():遍歷所有相關(guān)的Watcher,調(diào)用watcher.update()。
core/oberver/index.js:
notify () { const subs = this.subs.slice() // 如果未運(yùn)行異步,則不會在調(diào)度程序中對sub進(jìn)行排序 if (process.env.NODE_ENV !== 'production' && !config.async) { // 排序,確保它們按正確的順序執(zhí)行 subs.sort((a, b) => a.id - b.id) } // 遍歷相關(guān)watcher,并調(diào)用watcher更新 for (let i = 0, l = subs.length; i < l; i++) { subs[i].update() } }
(3)執(zhí)行watcher.update(): 判斷是立即更新還是異步更新。若為異步更新,調(diào)用queueWatcher(this),將watcher入隊,放到后面一起更新。
core/oberver/watcher.js:
update () { /* istanbul ignore else */ if (this.lazy) { this.dirty = true } else if (this.sync) { //立即執(zhí)行渲染 this.run() } else { // watcher入隊操作,后面一起執(zhí)行渲染 queueWatcher(this) } }
(4)執(zhí)行queueWatcher(this): watcher進(jìn)行去重等操作后,添加到隊列中,調(diào)用nextTick(flushSchedulerQueue)執(zhí)行異步隊列,傳入回調(diào)函數(shù)flushSchedulerQueue。
core/oberver/scheduler.js:
function queueWatcher (watcher: Watcher) { // has 標(biāo)識,判斷該watcher是否已在,避免在一個隊列中添加相同的 Watcher const id = watcher.id if (has[id] == null) { has[id] = true // flushing 標(biāo)識,處理 Watcher 渲染時,可能產(chǎn)生的新 Watcher。 if (!flushing) { // 將當(dāng)前 Watcher 添加到異步隊列 queue.push(watcher) } else { // 產(chǎn)生新的watcher就添加到排序的位置 let i = queue.length - 1 while (i > index && queue[i].id > watcher.id) { i-- } queue.splice(i + 1, 0, watcher) } // queue the flush // waiting 標(biāo)識,讓所有的 Watcher 都在一個 tick 內(nèi)進(jìn)行更新。 if (!waiting) { waiting = true if (process.env.NODE_ENV !== 'production' && !config.async) { flushSchedulerQueue() return } // 執(zhí)行異步隊列,并傳入回調(diào) nextTick(flushSchedulerQueue) } } }
(5)執(zhí)行nextTick(cb): 將傳進(jìn)去的 flushSchedulerQueue 函數(shù)處理后添加到callbacks隊列中,調(diào)用timerFunc啟動異步執(zhí)行任務(wù)。
core/util/next-tick.js:
function nextTick (cb?: Function, ctx?: Object) { let _resolve // 此處的callbacks就是隊列(回調(diào)數(shù)組),將傳入的 flushSchedulerQueue 方法處理后添加到回調(diào)數(shù)組 callbacks.push(() => { if (cb) { try { cb.call(ctx) } catch (e) { handleError(e, ctx, 'nextTick') } } else if (_resolve) { _resolve(ctx) } }) if (!pending) { pending = true // 啟動異步執(zhí)行任務(wù),此方法會根據(jù)瀏覽器兼容性,選用不同的異步策略 timerFunc() } // $flow-disable-line if (!cb && typeof Promise !== 'undefined') { return new Promise(resolve => { _resolve = resolve }) } }
(6)timerFunc():根據(jù)瀏覽器兼容性,選用不同的異步方式去執(zhí)行flushCallbacks。由于宏任務(wù)耗費(fèi)的時間是大于微任務(wù)的,所以先選用微任務(wù)的方式,都不行時再使用宏任務(wù)的方式,
core/util/next-tick.js:
let timerFunc // 支持Promise則使用Promise異步的方式執(zhí)行flushCallbacks if (typeof Promise !== 'undefined' && isNative(Promise)) { const p = Promise.resolve() timerFunc = () => { p.then(flushCallbacks) if (isIOS) setTimeout(noop) } isUsingMicroTask = true } else if (!isIE && typeof MutationObserver !== 'undefined' && ( isNative(MutationObserver) || 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 } else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) { timerFunc = () => { setImmediate(flushCallbacks) } } else { // 實在不行再使用setTimeout的異步方式 timerFunc = () => { setTimeout(flushCallbacks, 0) } }
(7)flushCallbacks:異步執(zhí)行callbacks隊列中所有函數(shù)
core/util/next-tick.js:
// 循環(huán)callbacks隊列,執(zhí)行里面所有函數(shù)flushSchedulerQueue,并清空隊列 function flushCallbacks () { pending = false const copies = callbacks.slice(0) callbacks.length = 0 for (let i = 0; i < copies.length; i++) { copies[i]() } }
(8)flushSchedulerQueue():遍歷watcher隊列,執(zhí)行watcher.run()
watcher.run():真正的渲染
function flushSchedulerQueue() { currentFlushTimestamp = getNow(); flushing = true; let watcher, id; // 排序,先渲染父節(jié)點(diǎn),再渲染子節(jié)點(diǎn) // 這樣可以避免不必要的子節(jié)點(diǎn)渲染,如:父節(jié)點(diǎn)中 v -if 為 false 的子節(jié)點(diǎn),就不用渲染了 queue.sort((a, b) => a.id - b.id); // do not cache length because more watchers might be pushed // as we run existing watchers // 遍歷所有 Watcher 進(jìn)行批量更新。 for (index = 0; index < queue.length; index++) { watcher = queue[index]; if (watcher.before) { watcher.before(); } id = watcher.id; has[id] = null; // 真正的更新函數(shù) watcher.run(); // in dev build, check and stop circular updates. if (process.env.NODE_ENV !== "production" && has[id] != null) { circular[id] = (circular[id] || 0) + 1; if (circular[id] > MAX_UPDATE_COUNT) { warn( "You may have an infinite update loop " + (watcher.user ? `in watcher with expression "${watcher.expression}"` : `in a component render function.`), watcher.vm ); break; } } } // keep copies of post queues before resetting state const activatedQueue = activatedChildren.slice(); const updatedQueue = queue.slice(); resetSchedulerState(); // call component updated and activated hooks callActivatedHooks(activatedQueue); callUpdatedHooks(updatedQueue); // devtool hook /* istanbul ignore if */ if (devtools && config.devtools) { devtools.emit("flush"); } }
(9)updateComponent():watcher.run()經(jīng)過一系列的轉(zhuǎn)圈,執(zhí)行updateComponent,updateComponent中執(zhí)行render(),讓組件重新渲染, 再執(zhí)行_update(vnode) ,再執(zhí)行 patch()更新界面。
(10)_update():根據(jù)是否有vnode分別執(zhí)行不同的patch。
四、Vue.nextTick(callback)
1.Vue.nextTick(callback)作用:獲取更新后的真正的 DOM 元素。
由于Vue 在更新 DOM 時是異步執(zhí)行的,所以在修改data之后,并不能立刻獲取到修改后的DOM元素。為了獲取到修改后的 DOM元素,可以在數(shù)據(jù)變化之后立即使用 Vue.nextTick(callback)。
2.為什么 Vue.$nextTick 能夠獲取更新后的 DOM?
因為Vue.$nextTick其實就是調(diào)用 nextTick 方法,在異步隊列中執(zhí)行回調(diào)函數(shù)。
Vue.prototype.$nextTick = function (fn: Function) { return nextTick(fn, this); };
3.使用 Vue.$nextTick
例子1:
<template> <p id="test">{{foo}}</p> </template> <script> export default{ data(){ return { foo: 'foo' } }, mounted() { let test = document.querySelector('#test'); this.foo = 'foo1'; // vue在更新DOM時是異步進(jìn)行的,所以此處DOM并未更新 console.log('test.innerHTML:' + test.innerHTML); this.$nextTick(() => { // nextTick回調(diào)是在DOM更新后調(diào)用的,所以此處DOM已經(jīng)更新 console.log('nextTick:test.innerHTML:' + test.innerHTML); }) } } </script> 執(zhí)行結(jié)果: test.innerHTML:foo nextTick:test.innerHTML:foo1
例子2:
<template> <p id="test">{{foo}}</p> </template> <script> export default{ data(){ return { foo: 'foo' } }, mounted() { let test = document.querySelector('#test'); this.foo = 'foo1'; // vue在更新DOM時是異步進(jìn)行的,所以此處DOM并未更新 console.log('1.test.innerHTML:' + test.innerHTML); this.$nextTick(() => { // nextTick回調(diào)是在DOM更新后調(diào)用的,所以此處DOM已經(jīng)更新 console.log('nextTick:test.innerHTML:' + test.innerHTML); }) this.foo = 'foo2'; // 此處DOM并未更新,且先于異步回調(diào)函數(shù)前執(zhí)行 console.log('2.test.innerHTML:' + test.innerHTML); } } </script> 執(zhí)行結(jié)果: 1.test.innerHTML:foo 2.test.innerHTML:foo nextTick:test.innerHTML:foo2
例子3:
<template> <p id="test">{{foo}}</p> </template> <script> export default{ data(){ return { foo: 'foo' } }, mounted() { let test = document.querySelector('#test'); this.$nextTick(() => { // nextTick回調(diào)是在觸發(fā)更新之前就放入callbacks隊列, // 壓根沒有觸發(fā)watcher.update以及以后的一系列操作,所以也就沒有執(zhí)行到最后的watcher.run()實行渲染 // 所以此處DOM并未更新 console.log('nextTick:test.innerHTML:' + test.innerHTML); }) this.foo = 'foo1'; // vue在更新DOM時是異步進(jìn)行的,所以此處DOM并未更新 console.log('1.test.innerHTML:' + test.innerHTML); this.foo = 'foo2'; // 此處DOM并未更新,且先于異步回調(diào)函數(shù)前執(zhí)行 console.log('2.test.innerHTML:' + test.innerHTML); } } </script> 執(zhí)行結(jié)果: 1.test.innerHTML:foo 2.test.innerHTML:foo nextTick:test.innerHTML:foo
4、 nextTick與其他異步方法
nextTick是模擬的異步任務(wù),所以可以用 Promise 和 setTimeout 來實現(xiàn)和 this.$nextTick 相似的效果。
例子1:
<template> <p id="test">{{foo}}</p> </template> <script> export default{ data(){ return { foo: 'foo' } }, mounted() { let test = document.querySelector('#test'); this.$nextTick(() => { // nextTick回調(diào)是在觸發(fā)更新之前就放入callbacks隊列, // 壓根沒有觸發(fā)watcher.update以及以后的一系列操作,所以也就沒有執(zhí)行到最后的watcher.run()實行渲染 // 所以此處DOM并未更新 console.log('nextTick:test.innerHTML:' + test.innerHTML); }) this.foo = 'foo1'; // vue在更新DOM時是異步進(jìn)行的,所以此處DOM并未更新 console.log('1.test.innerHTML:' + test.innerHTML); this.foo = 'foo2'; // 此處DOM并未更新,且先于異步回調(diào)函數(shù)前執(zhí)行 console.log('2.test.innerHTML:' + test.innerHTML); Promise.resolve().then(() => { console.log('Promise:test.innerHTML:' + test.innerHTML); }); setTimeout(() => { console.log('setTimeout:test.innerHTML:' + test.innerHTML); }); } } </script> 執(zhí)行結(jié)果: 1.test.innerHTML:foo 2.test.innerHTML:foo nextTick:test.innerHTML:foo Promise:test.innerHTML:foo2 setTimeout:test.innerHTML:foo2
例子2:
<template> <p id="test">{{foo}}</p> </template> <script> export default{ data(){ return { foo: 'foo' } }, mounted() { let test = document.querySelector('#test'); // Promise 和 setTimeout 依舊是等到DOM更新后再執(zhí)行 Promise.resolve().then(() => { console.log('Promise:test.innerHTML:' + test.innerHTML); }); setTimeout(() => { console.log('setTimeout:test.innerHTML:' + test.innerHTML); }); this.$nextTick(() => { // nextTick回調(diào)是在觸發(fā)更新之前就放入callbacks隊列, // 壓根沒有觸發(fā)watcher.update以及以后的一系列操作,所以也就沒有執(zhí)行到最后的watcher.run()實行渲染 // 所以此處DOM并未更新 console.log('nextTick:test.innerHTML:' + test.innerHTML); }) this.foo = 'foo1'; // vue在更新DOM時是異步進(jìn)行的,所以此處DOM并未更新 console.log('1.test.innerHTML:' + test.innerHTML); this.foo = 'foo2'; // 此處DOM并未更新,且先于異步回調(diào)函數(shù)前執(zhí)行 console.log('2.test.innerHTML:' + test.innerHTML); } } </script> 執(zhí)行結(jié)果: 1.test.innerHTML:foo 2.test.innerHTML:foo nextTick:test.innerHTML:foo Promise:test.innerHTML:foo2 setTimeout:test.innerHTML:foo2
總結(jié)
到此這篇關(guān)于vue源碼之批量異步更新策略的文章就介紹到這了,更多相關(guān)vue批量異步更新策略內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
解決element-ui的table表格控件表頭與內(nèi)容列不對齊問題
這篇文章主要介紹了解決element-ui的table表格控件表頭與內(nèi)容列不對齊問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-07-07解決vue-router 切換tab標(biāo)簽關(guān)閉時緩存問題
這篇文章主要介紹了解決vue-router 切換tab標(biāo)簽關(guān)閉時緩存問題,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-07-07VUE的history模式下除了index外其他路由404報錯解決辦法
在本篇文章里小編給大家分享的是關(guān)于VUE的history模式下除了index外其他路由404報錯解決辦法,對此有需要的朋友們可以學(xué)習(xí)下。2019-08-08說說如何在Vue.js中實現(xiàn)數(shù)字輸入組件的方法
這篇文章主要介紹了說說如何在Vue.js中實現(xiàn)數(shù)字輸入組件的方法,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2019-01-01vue + typescript + video.js實現(xiàn) 流媒體播放 視頻監(jiān)控功能
視頻才用流媒體,有后臺實時返回數(shù)據(jù), 要支持flash播放, 所以需安裝對應(yīng)的flash插件。這篇文章主要介紹了vue + typescript + video.js 流媒體播放 視頻監(jiān)控,需要的朋友可以參考下2019-07-07