前言
js是一個單線程的語言(非阻塞),最初的目的是為了和瀏覽器交互纷妆,也就是事件的輸入輸出流亦镶,計算機(jī)根據(jù)人類的指令做出不同的反應(yīng)結(jié)果,但是在JS執(zhí)行的過程環(huán)境中 我們有 幾個特殊的 “單詞” setTimeout
灼狰、setInterval
宛瞄、 Promise
、另外在 Node中還有 process.nextTick
交胚。那么他們的執(zhí)行順序到底是怎么樣的呢份汗,瀏覽器不應(yīng)該是按照他們書寫的順序從上往下執(zhí)行嗎?
那你又有疑問了蝴簇,既然是單線程的杯活,在某個特定的時刻只有特定的代碼能夠被執(zhí)行,并阻塞其它的代碼熬词。
那不行啊旁钧,我們總不能一直等著啊,前端需要調(diào)用后端接口取數(shù)據(jù)互拾,這個過程是需要響應(yīng)時間的歪今,那執(zhí)行這個代碼的時候瀏覽器也等著?答案是否定的颜矿。
其實還有其他很多類線程(應(yīng)該叫做任務(wù)隊列)寄猩,比如進(jìn)行ajax請求、監(jiān)控用戶事件骑疆、定時器田篇、讀寫文件的線程(例如在NodeJS中)等等。
這些我們稱之為異步事件封断,當(dāng)異步事件發(fā)生時斯辰,將他們放入執(zhí)行隊列,等待當(dāng)前代碼執(zhí)行完成坡疼。就不會長時間阻塞主線程彬呻。
等主線程的代碼執(zhí)行完畢,然后再讀取任務(wù)隊列柄瑰,返回主線程繼續(xù)處理闸氮。如此循環(huán)這就是事件循環(huán)機(jī)制。
JS 在執(zhí)行的過程中會產(chǎn)生執(zhí)行環(huán)境教沾,這些執(zhí)行環(huán)境會被順序的加入到執(zhí)行棧中蒲跨。如果遇到異步的代碼,會被掛起并加入到 Task(有多種 task) 隊列中授翻。一旦執(zhí)行棧為空或悲,Event Loop 就會從 Task 隊列中拿出需要執(zhí)行的代碼并放入執(zhí)行棧中執(zhí)行孙咪,所以本質(zhì)上來說 JS 中的異步還是同步行為
舉個栗子
console.log('0');
setTimeout(() => {
console.log('1');
}, 0);
console.log('2');
//輸出 0 , 2 ,1
看起來是setTimeout 設(shè)置了時間為0 但是 setTimeout 是一個“異步”的操作巡语,其實真是的情況是 setTimeout 的0 參數(shù)是無效的翎蹈, JS會給他默認(rèn)一個值為4毫秒。所以結(jié)果是 0 2 1 男公。
我們剛剛說到了“異步” 那么JS是怎么異步的呢 荤堪,其實在JS執(zhí)行的時候 不同的任務(wù)會分配到不同的隊列中,每個任務(wù)在制定的時候 已經(jīng)規(guī)定了他的基礎(chǔ)要素 也就是他屬于哪個隊列的 枢赔,任務(wù)源可以分為2類 微任務(wù) microtask
和宏任務(wù) macrotask
,微任務(wù)又稱之為JOBS澄阳,宏任務(wù)稱為TASK。
我們在來看看下面這個例子
setTimeout(function() {
console.log(1)
}, 0);
new Promise((resolve)=>{
console.log(2);
for(var i = 0; i < 10000; i++) {
i == 9999 && resolve();
}
console.log(3);
}).then(function() {
console.log(4);
});
console.log(5)
// 2 3 5 4 1
為什么是這個結(jié)果呢 踏拜。這就是 Jobs 和 Task的區(qū)別 我們下面仔細(xì)梳理下
微任務(wù)(Jobs)包括
process.nextTick
Promise
Object.observe(已廢棄)
MutationObserver (html5 新特性)
宏任務(wù)(Task)包括
setTimeout/setInterval
setImmediate
I/O操作
UI rendering
瀏覽器中新標(biāo)準(zhǔn)中的事件循環(huán)機(jī)制與 node.js 類似碎赢,其中會介紹到幾個nodejs有但是瀏覽器中沒有的 API,大家只需要了解就好执隧。
比如process.nextTick
揩抡,setImmediate
我們稱他們?yōu)槭录矗?事件源作為任務(wù)分發(fā)器,他們的回調(diào)函數(shù)才是被分發(fā)到任務(wù)隊列镀琉,而本身會立即執(zhí)行峦嗤。
例如,setTimeout
第一個參數(shù)被分發(fā)到任務(wù)隊列屋摔,Promise
的 then 方法的回調(diào)函數(shù)被分發(fā)到任務(wù)隊列(catch方法同理)烁设。
不同源的事件被分發(fā)到不同的任務(wù)隊列,其中 setTimeout
和 setInterval
屬于同源
整體代碼開始第一次循環(huán)钓试。全局上下文進(jìn)入函數(shù)調(diào)用棧装黑。直到調(diào)用棧清空(只剩全局),然后執(zhí)行所有的job弓熏。
當(dāng)所有可執(zhí)行的 job 執(zhí)行完畢之后恋谭。循環(huán)再次從task開始,找到其中一個任務(wù)隊列執(zhí)行完畢挽鞠,然后再執(zhí)行所有的 job疚颊,這樣一直循環(huán)下去。
無論是 task 還是 job信认,都是通過函數(shù)調(diào)用棧來完成材义。
這個時候我們是不是有一個大發(fā)現(xiàn),除了首次整體代碼的執(zhí)行嫁赏,其他的都有規(guī)律其掂,先執(zhí)行task任務(wù)隊列,再執(zhí)行所有的 job 并清空 job 隊列潦蝇。
再執(zhí)行 task—job—task—job……款熬,往復(fù)循環(huán)直到?jīng)]有可執(zhí)行代碼深寥。
那我們可不可以這么理解,第一次 script 代碼的執(zhí)行也算是一個task任務(wù)呢贤牛,如果這么理解那整個事件循環(huán)就很容易理解了翩迈。
UI rendering是在Task執(zhí)行之后就運(yùn)行的 那么我們只要把DOM操作放入Job中就可以提高渲染的性能了
下面我們說說 vue的 nextTick
還是舉個栗子
<div id="app">
<ul ref="list">
<li v-for="li in list">
{{li.name}}
</li>
</ul>
</div>
new Vue({
el: '#app',
data: {
list: []
},
mounted() {
this.init()
},
methods: {
init() {
this.list = [{name:"lxl",age:18},{name:"kobe",age:19}]
this.$refs.list.getElementsByTagName('li')[0].style.color = 'red'
},
}
})
我們會發(fā)現(xiàn) 這樣會報錯
如下修改:
new Vue({
el: '#app',
data: {
list: []
},
mounted() {
this.init()
},
methods: {
init() {
this.list = [{name:"lxl",age:18},{name:"kobe",age:19}]
this.$nextTick(()=>{
this.$refs.list.getElementsByTagName('li')[0].style.color = 'red'
})
},
}
})
我在獲取到數(shù)據(jù)后賦值給 data 對象的 list 屬性,然后我想引用ul元素找到第一個li把它的顏色變?yōu)榧t色盔夜,但是事實上,這個要報錯的堤魁。
我們知道喂链,在執(zhí)行這句話時,ul 下面并沒有 li妥泉,也就是說剛剛進(jìn)行的賦值操作椭微,當(dāng)前并沒有引起視圖層的更新。
因為 Vue 的數(shù)據(jù)驅(qū)動視圖更新盲链,是異步的蝇率,即修改數(shù)據(jù)的當(dāng)下,視圖不會立刻更新刽沾,而是等同一事件循環(huán)中的所有數(shù)據(jù)變化完成之后本慕,再統(tǒng)一進(jìn)行視圖更新。
因此侧漓,在這樣的情況下锅尘,vue 給我們提供了
nextTick 方法藤违,vue 在更新完視圖后就會執(zhí)行我們的函數(shù)幫我們做事情。
nextTick
可以讓我們在下次 DOM 更新循環(huán)結(jié)束之后執(zhí)行延遲回調(diào)纵揍,用于獲得更新后的 DOM顿乒。
var callbacks = [];
var pending = false;
function flushCallbacks () {
pending = false;
var copies = callbacks.slice(0);
callbacks.length = 0;
for (var i = 0; i < copies.length; i++) {
copies[i]();
}
}
// Here we have async deferring wrappers using both microtasks and (macro) tasks.
// In < 2.4 we used microtasks everywhere, but there are some scenarios where
// microtasks have too high a priority and fire in between supposedly
// sequential events (e.g. #4521, #6690) or even between bubbling of the same
// event (#6566). However, using (macro) tasks everywhere also has subtle problems
// when state is changed right before repaint (e.g. #6813, out-in transitions).
// Here we use microtask by default, but expose a way to force (macro) task when
// needed (e.g. in event handlers attached by v-on).
var microTimerFunc;
var macroTimerFunc;
var useMacroTask = false;
// Determine (macro) task defer implementation.
// Technically setImmediate should be the ideal choice, but it's only available
// in IE. The only polyfill that consistently queues the callback after all DOM
// events triggered in the same loop is by using MessageChannel.
/* istanbul ignore if */
if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
macroTimerFunc = function () {
setImmediate(flushCallbacks);
};
} else if (typeof MessageChannel !== 'undefined' && (
isNative(MessageChannel) ||
// PhantomJS
MessageChannel.toString() === '[object MessageChannelConstructor]'
)) {
var channel = new MessageChannel();
var port = channel.port2;
channel.port1.onmessage = flushCallbacks;
macroTimerFunc = function () {
port.postMessage(1);
};
} else {
/* istanbul ignore next */
macroTimerFunc = function () {
setTimeout(flushCallbacks, 0);
};
}
// Determine microtask defer implementation.
/* istanbul ignore next, $flow-disable-line */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
var p = Promise.resolve();
microTimerFunc = function () {
p.then(flushCallbacks);
// in problematic UIWebViews, Promise.then doesn't completely break, but
// it can get stuck in a weird state where callbacks are pushed into the
// microtask queue but the queue isn't being flushed, until the browser
// needs to do some other work, e.g. handle a timer. Therefore we can
// "force" the microtask queue to be flushed by adding an empty timer.
if (isIOS) { setTimeout(noop); }
};
} else {
// fallback to macro
microTimerFunc = macroTimerFunc;
}
/**
* Wrap a function so that if any code inside triggers state change,
* the changes are queued using a (macro) task instead of a microtask.
*/
function withMacroTask (fn) {
return fn._withTask || (fn._withTask = function () {
useMacroTask = true;
var res = fn.apply(null, arguments);
useMacroTask = false;
return res
})
}
function nextTick (cb, ctx) {
var _resolve;
callbacks.push(function () {
if (cb) {
try {
cb.call(ctx);
} catch (e) {
handleError(e, ctx, 'nextTick');
}
} else if (_resolve) {
_resolve(ctx);
}
});
if (!pending) {
pending = true;
if (useMacroTask) {
macroTimerFunc();
} else {
microTimerFunc();
}
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(function (resolve) {
_resolve = resolve;
})
}
}
綜合上面的代碼我們可以知道
在 Vue 2.4 之前都是使用的 microtasks
,但是 microtasks
的優(yōu)先級過高泽谨,在某些情況下可能會出現(xiàn)比事件冒泡更快的情況璧榄,但如果都使用 macrotasks
又可能會出現(xiàn)渲染的性能問題。所以在新版本中隔盛,會默認(rèn)使用 microtasks
犹菱,但在特殊情況下會使用 macrotasks
,比如 v-on吮炕。
對于實現(xiàn) macrotasks
腊脱,會先判斷是否能使用 setImmediate
,不能的話降級為 MessageChannel
龙亲,以上都不行的話就使用 setTimeout
setImmediate傳送門
MessageChannel傳送門
event-loops傳送門
總結(jié)一下今天的知識
(1)所有同步任務(wù)都在主線程上執(zhí)行陕凹,形成一個執(zhí)行棧(execution context stack)悍抑。
(2)主線程之外,還存在一個"任務(wù)隊列"(task queue)杜耙。只要異步任務(wù)有了運(yùn)行結(jié)果搜骡,就在"任務(wù)隊列"之中放置一個事件。
(3)一旦"執(zhí)行棧"中的所有同步任務(wù)執(zhí)行完畢佑女,系統(tǒng)就會讀取"任務(wù)隊列"记靡,看看里面有哪些事件。那些對應(yīng)的異步任務(wù)团驱,于是結(jié)束等待狀態(tài)摸吠,進(jìn)入執(zhí)行棧,開始執(zhí)行嚎花。
(4)主線程不斷重復(fù)上面的第三步