vue源碼之批量異步更新策略的深入解析
vue異步更新源碼中會(huì)有涉及事件循環(huán)、宏任務(wù)、微任務(wù)的概念,所以先了解一下這幾個(gè)概念。
一、事件循環(huán)、宏任務(wù)、微任務(wù)
1.事件循環(huán)Event Loop:瀏覽器為了協(xié)調(diào)事件處理、腳本執(zhí)行、網(wǎng)絡(luò)請(qǐng)求和渲染等任務(wù)而定制的工作機(jī)制。
2.宏任務(wù)Task: 代表一個(gè)個(gè)離散的、獨(dú)立的工作單位。瀏覽器完成一個(gè)宏任務(wù),在下一個(gè)宏任務(wù)開始執(zhí)行之前,會(huì)對(duì)頁(yè)面重新渲染。主要包括創(chuàng)建文檔對(duì)象、解析HTML、執(zhí)行主線JS代碼以及各種事件如頁(yè)面加載、輸入、網(wǎng)絡(luò)事件和定時(shí)器等。
3.微任務(wù):微任務(wù)是更小的任務(wù),是在當(dāng)前宏任務(wù)執(zhí)行結(jié)束后立即執(zhí)行的任務(wù)。如果存在微任務(wù),瀏覽器會(huì)在完成微任務(wù)之后再重新渲染。微任務(wù)的例子有Promise回調(diào)函數(shù)、DOM變化等。
執(zhí)行過(guò)程:執(zhí)行完宏任務(wù) => 執(zhí)行微任務(wù) => 頁(yè)面重新渲染 => 再執(zhí)行新一輪宏任務(wù)

任務(wù)執(zhí)行順序例子:
//第一個(gè)宏任務(wù)進(jìn)入主線程
console.log('1');
//丟到宏事件隊(duì)列中
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')
})
//丟到宏事件隊(duì)列中
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
解析:
第一個(gè)宏任務(wù)
- 第一個(gè)宏任務(wù)進(jìn)入主線程,打印1
- setTimeout丟到宏任務(wù)隊(duì)列
- process.nextTick丟到微任務(wù)隊(duì)列
- new Promise直接執(zhí)行,打印7
- Promise then事件丟到微任務(wù)隊(duì)列
- setTimeout丟到宏任務(wù)隊(duì)列
第一個(gè)宏任務(wù)執(zhí)行完,開始執(zhí)行微任務(wù)
- 執(zhí)行process.nextTick,打印6
- 執(zhí)行Promise then事件,打印8
微任務(wù)執(zhí)行完,清空微任務(wù)隊(duì)列,頁(yè)面渲染,進(jìn)入下一個(gè)宏任務(wù)setTimeout
- 執(zhí)行打印2
- process.nextTick丟到微任務(wù)隊(duì)列
- new Promise直接執(zhí)行,打印4
- Promise then事件丟到微任務(wù)隊(duì)列
第二個(gè)宏任務(wù)執(zhí)行完,開始執(zhí)行微任務(wù)
- 執(zhí)行process.nextTick,打印3
- 執(zhí)行Promise then事件,打印5
微任務(wù)執(zhí)行完,清空微任務(wù)隊(duì)列,頁(yè)面渲染,進(jìn)入下一個(gè)宏任務(wù)setTimeout,重復(fù)上述類似流程,打印出9,11,10,12
二、Vue異步批量更新過(guò)程
1.解析:當(dāng)偵測(cè)到數(shù)據(jù)變化,vue會(huì)開啟一個(gè)隊(duì)列,將相關(guān)的watcher存入隊(duì)列,將回調(diào)函數(shù)存入callbacks隊(duì)列,異步執(zhí)行回調(diào)函數(shù),遍歷watcher隊(duì)列進(jìn)行渲染。
異步:Vue 在更新 DOM 時(shí)是異步執(zhí)行的,只要偵聽到數(shù)據(jù)變化,vue將開啟一個(gè)隊(duì)列,并緩沖 在同一事件循環(huán)中發(fā)生的所有數(shù)據(jù) 的變更。
批量:如果同一個(gè)watcher被多次觸發(fā),只會(huì)被推入到隊(duì)列中一次。去重可以避免不必要的計(jì)算和DOM操作。然后在下一個(gè)的事件循環(huán)“tick”中,vue刷新隊(duì)列執(zhí)行實(shí)際工作。
異步策略:Vue的內(nèi)部對(duì)異步隊(duì)列嘗試使用原生的Promise.then、MutationObserver和 setImmediate,如果執(zhí)行環(huán)境不支持,則會(huì)采用 setTimeout(fn, 0) 代替。即會(huì)先嘗試使用微任務(wù)方式,不行再用宏任務(wù)方式。
異步批量更新流程圖:
三、vue批量異步更新源碼
異步更新:整個(gè)過(guò)程相當(dāng)于將臭襪子放到盆子里,最后一起洗。
1.當(dāng)一個(gè)Data更新時(shí),會(huì)依次執(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)行異步,則不會(huì)在調(diào)度程序中對(duì)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入隊(duì),放到后面一起更新。
core/oberver/watcher.js:
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
//立即執(zhí)行渲染
this.run()
} else {
// watcher入隊(duì)操作,后面一起執(zhí)行渲染
queueWatcher(this)
}
}
(4)執(zhí)行queueWatcher(this): watcher進(jìn)行去重等操作后,添加到隊(duì)列中,調(diào)用nextTick(flushSchedulerQueue)執(zhí)行異步隊(duì)列,傳入回調(diào)函數(shù)flushSchedulerQueue。
core/oberver/scheduler.js:
function queueWatcher (watcher: Watcher) {
// has 標(biāo)識(shí),判斷該watcher是否已在,避免在一個(gè)隊(duì)列中添加相同的 Watcher
const id = watcher.id
if (has[id] == null) {
has[id] = true
// flushing 標(biāo)識(shí),處理 Watcher 渲染時(shí),可能產(chǎn)生的新 Watcher。
if (!flushing) {
// 將當(dāng)前 Watcher 添加到異步隊(duì)列
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)識(shí),讓所有的 Watcher 都在一個(gè) tick 內(nèi)進(jìn)行更新。
if (!waiting) {
waiting = true
if (process.env.NODE_ENV !== 'production' && !config.async) {
flushSchedulerQueue()
return
}
// 執(zhí)行異步隊(duì)列,并傳入回調(diào)
nextTick(flushSchedulerQueue)
}
}
}
(5)執(zhí)行nextTick(cb): 將傳進(jìn)去的 flushSchedulerQueue 函數(shù)處理后添加到callbacks隊(duì)列中,調(diào)用timerFunc啟動(dòng)異步執(zhí)行任務(wù)。
core/util/next-tick.js:
function nextTick (cb?: Function, ctx?: Object) {
let _resolve
// 此處的callbacks就是隊(duì)列(回調(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
// 啟動(dòng)異步執(zhí)行任務(wù),此方法會(huì)根據(jù)瀏覽器兼容性,選用不同的異步策略
timerFunc()
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
(6)timerFunc():根據(jù)瀏覽器兼容性,選用不同的異步方式去執(zhí)行flushCallbacks。由于宏任務(wù)耗費(fèi)的時(shí)間是大于微任務(wù)的,所以先選用微任務(wù)的方式,都不行時(shí)再使用宏任務(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 {
// 實(shí)在不行再使用setTimeout的異步方式
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
(7)flushCallbacks:異步執(zhí)行callbacks隊(duì)列中所有函數(shù)
core/util/next-tick.js:
// 循環(huán)callbacks隊(duì)列,執(zhí)行里面所有函數(shù)flushSchedulerQueue,并清空隊(duì)列
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隊(duì)列,執(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)過(guò)一系列的轉(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 時(shí)是異步執(zhí)行的,所以在修改data之后,并不能立刻獲取到修改后的DOM元素。為了獲取到修改后的 DOM元素,可以在數(shù)據(jù)變化之后立即使用 Vue.nextTick(callback)。
2.為什么 Vue.$nextTick 能夠獲取更新后的 DOM?
因?yàn)閂ue.$nextTick其實(shí)就是調(diào)用 nextTick 方法,在異步隊(duì)列中執(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時(shí)是異步進(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時(shí)是異步進(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隊(duì)列,
// 壓根沒有觸發(fā)watcher.update以及以后的一系列操作,所以也就沒有執(zhí)行到最后的watcher.run()實(shí)行渲染
// 所以此處DOM并未更新
console.log('nextTick:test.innerHTML:' + test.innerHTML);
})
this.foo = 'foo1';
// vue在更新DOM時(shí)是異步進(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 來(lái)實(shí)現(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隊(duì)列,
// 壓根沒有觸發(fā)watcher.update以及以后的一系列操作,所以也就沒有執(zhí)行到最后的watcher.run()實(shí)行渲染
// 所以此處DOM并未更新
console.log('nextTick:test.innerHTML:' + test.innerHTML);
})
this.foo = 'foo1';
// vue在更新DOM時(shí)是異步進(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隊(duì)列,
// 壓根沒有觸發(fā)watcher.update以及以后的一系列操作,所以也就沒有執(zhí)行到最后的watcher.run()實(shí)行渲染
// 所以此處DOM并未更新
console.log('nextTick:test.innerHTML:' + test.innerHTML);
})
this.foo = 'foo1';
// vue在更新DOM時(shí)是異步進(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)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
如何利用vue-cli監(jiān)測(cè)webpack打包與啟動(dòng)時(shí)長(zhǎng)
這篇文章主要給大家介紹了關(guān)于如何利用vue-cli監(jiān)測(cè)webpack打包與啟動(dòng)時(shí)長(zhǎng)的相關(guān)資料,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2022-02-02
vue-cli+webpack項(xiàng)目打包到服務(wù)器后,ttf字體找不到的解決操作
這篇文章主要介紹了vue-cli+webpack項(xiàng)目打包到服務(wù)器后,ttf字體找不到的解決操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-08-08
vue3使用別名報(bào)錯(cuò)問(wèn)題的解決辦法(vetur插件報(bào)錯(cuò)問(wèn)題)
最近在寫一個(gè)購(gòu)物網(wǎng)站使用vue,使用中遇到了問(wèn)題,下面這篇文章主要給大家介紹了關(guān)于vue3使用別名報(bào)錯(cuò)問(wèn)題的解決辦法,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-07-07
利用vueJs實(shí)現(xiàn)圖片輪播實(shí)例代碼
本篇文章主要介紹了利用vueJs實(shí)現(xiàn)圖片輪播實(shí)例代碼,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-06-06
解決vue項(xiàng)目,npm run build后,報(bào)路徑錯(cuò)的問(wèn)題
這篇文章主要介紹了解決vue項(xiàng)目,npm run build后,報(bào)路徑錯(cuò)的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-08-08
詳解vue3+element-plus實(shí)現(xiàn)動(dòng)態(tài)菜單和動(dòng)態(tài)路由動(dòng)態(tài)按鈕(前后端分離)
本文需要使用axios,路由,pinia,安裝element-plus,并且本文vue3是基于js而非ts的,這些環(huán)境如何搭建不做描述,需要讀者自己完成,感興趣的朋友跟隨小編一起看看吧2023-11-11
利用vue + element實(shí)現(xiàn)表格分頁(yè)和前端搜索的方法
眾所周知Element 是一套 Vue.js 后臺(tái)組件庫(kù),它能夠幫助你更輕松更快速地開發(fā)后臺(tái)項(xiàng)目。下面這篇文章主要給大家介紹了關(guān)于利用vue + element實(shí)現(xiàn)表格分頁(yè)和前端搜索的相關(guān)資料,需要的朋友可以參考借鑒,下面來(lái)一起看看吧。2017-12-12
Vue調(diào)用PC攝像頭實(shí)現(xiàn)拍照功能
這篇文章主要為大家詳細(xì)介紹了Vue調(diào)用PC攝像頭實(shí)現(xiàn)拍照功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-09-09
vue 百度地圖(vue-baidu-map)繪制方向箭頭折線實(shí)例代碼詳解
這篇文章主要介紹了vue 百度地圖(vue-baidu-map)繪制方向箭頭折線,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-04-04

