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ì)頁面重新渲染。主要包括創(chuàng)建文檔對(duì)象、解析HTML、執(zhí)行主線JS代碼以及各種事件如頁面加載、輸入、網(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í)行過程:執(zhí)行完宏任務(wù) => 執(zhí)行微任務(wù) => 頁面重新渲染 => 再執(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ì)列,頁面渲染,進(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ì)列,頁面渲染,進(jìn)入下一個(gè)宏任務(wù)setTimeout,重復(fù)上述類似流程,打印出9,11,10,12
二、Vue異步批量更新過程
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è)過程相當(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)過一系列的轉(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 來實(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)文章
解決element-ui的table表格控件表頭與內(nèi)容列不對(duì)齊問題
這篇文章主要介紹了解決element-ui的table表格控件表頭與內(nèi)容列不對(duì)齊問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-07-07
vue實(shí)現(xiàn)移動(dòng)端圖片上傳功能
這篇文章主要為大家詳細(xì)介紹了vue實(shí)現(xiàn)移動(dòng)端圖片上傳功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-12-12
解決vue-router 切換tab標(biāo)簽關(guān)閉時(shí)緩存問題
這篇文章主要介紹了解決vue-router 切換tab標(biāo)簽關(guān)閉時(shí)緩存問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-07-07
VUE的history模式下除了index外其他路由404報(bào)錯(cuò)解決辦法
在本篇文章里小編給大家分享的是關(guān)于VUE的history模式下除了index外其他路由404報(bào)錯(cuò)解決辦法,對(duì)此有需要的朋友們可以學(xué)習(xí)下。2019-08-08
說說如何在Vue.js中實(shí)現(xiàn)數(shù)字輸入組件的方法
這篇文章主要介紹了說說如何在Vue.js中實(shí)現(xiàn)數(shù)字輸入組件的方法,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2019-01-01
vue + typescript + video.js實(shí)現(xiàn) 流媒體播放 視頻監(jiān)控功能
視頻才用流媒體,有后臺(tái)實(shí)時(shí)返回?cái)?shù)據(jù), 要支持flash播放, 所以需安裝對(duì)應(yīng)的flash插件。這篇文章主要介紹了vue + typescript + video.js 流媒體播放 視頻監(jiān)控,需要的朋友可以參考下2019-07-07

