今天看了兩篇不錯(cuò)的文章,想自己總結(jié)一下JS的執(zhí)行機(jī)制
這一次,徹底弄懂 JavaScript 執(zhí)行機(jī)制
JavaScript 運(yùn)行機(jī)制詳解:再談Event Loop
一、 JavaScript最大的特點(diǎn)就是單線程
JavaScript語言的一大特點(diǎn)就是單線程,也就是說,同一個(gè)時(shí)間只能做一件事。那么,為什么JavaScript不能有多個(gè)線程呢?這樣能提高效率啊。
JavaScript的單線程,與它的用途有關(guān)。作為瀏覽器腳本語言,JavaScript的主要用途是與用戶互動(dòng),以及操作DOM。這決定了它只能是單線程,否則會(huì)帶來很復(fù)雜的同步問題。比如,假定JavaScript同時(shí)有兩個(gè)線程,一個(gè)線程在某個(gè)DOM節(jié)點(diǎn)上添加內(nèi)容,另一個(gè)線程刪除了這個(gè)節(jié)點(diǎn),這時(shí)瀏覽器應(yīng)該以哪個(gè)線程為準(zhǔn)?
所以,為了避免復(fù)雜性,從一誕生,JavaScript就是單線程,這已經(jīng)成了這門語言的核心特征,將來也不會(huì)改變。
為了利用多核CPU的計(jì)算能力,HTML5提出Web Worker標(biāo)準(zhǔn),允許JavaScript腳本創(chuàng)建多個(gè)線程,但是子線程完全受主線程控制,且不得操作DOM。所以,這個(gè)新標(biāo)準(zhǔn)并沒有改變JavaScript單線程的本質(zhì)。
運(yùn)行機(jī)制
既然js是單線程,那就像只有一個(gè)窗口的銀行,客戶需要排隊(duì)一個(gè)一個(gè)辦理業(yè)務(wù),同理js任務(wù)也要一個(gè)一個(gè)順序執(zhí)行。如果一個(gè)任務(wù)耗時(shí)過長(zhǎng),那么后一個(gè)任務(wù)也必須等著。那么問題來了,假如我們想瀏覽新聞,但是新聞包含的超清圖片加載很慢,難道我們的網(wǎng)頁(yè)要一直卡著直到圖片完全顯示出來?因此聰明的程序員將任務(wù)分為兩類:
同步任務(wù)
異步任務(wù)
當(dāng)我們打開網(wǎng)站時(shí),網(wǎng)頁(yè)的渲染過程就是一大堆同步任務(wù),比如頁(yè)面骨架和頁(yè)面元素的渲染。而像加載圖片音樂之類占用資源大耗時(shí)久的任務(wù),就是異步任務(wù)。
(1)所有同步任務(wù)都在主線程上執(zhí)行,形成一個(gè)執(zhí)行棧(execution context stack)。
(2)主線程之外,還存在一個(gè)"任務(wù)隊(duì)列"(task queue)。只要異步任務(wù)有了運(yùn)行結(jié)果,就在"任務(wù)隊(duì)列"之中放置一個(gè)事件。
(3)一旦"執(zhí)行棧"中的所有同步任務(wù)執(zhí)行完畢,系統(tǒng)就會(huì)讀取"任務(wù)隊(duì)列",看看里面有哪些事件。那些對(duì)應(yīng)的異步任務(wù),于是結(jié)束等待狀態(tài),進(jìn)入執(zhí)行棧,開始執(zhí)行。
(4)主線程不斷重復(fù)上面的第三步。

- 同步和異步任務(wù)分別進(jìn)入不同的執(zhí)行"場(chǎng)所",同步的進(jìn)入主線程,異步的進(jìn)入Event Table并注冊(cè)函數(shù)。
- 當(dāng)指定的事情完成時(shí),Event Table會(huì)將這個(gè)函數(shù)移入Event Queue。
- 主線程內(nèi)的任務(wù)執(zhí)行完畢為空,會(huì)去Event Queue讀取對(duì)應(yīng)的函數(shù),進(jìn)入主線程執(zhí)行。
- 上述過程會(huì)不斷重復(fù),也就是常說的Event Loop(事件循環(huán))。
正題
macro-task(宏任務(wù)):包括整體代碼script,setTimeout,setInterval
micro-task(微任務(wù)):Promise,process.nextTick
事件循環(huán)的順序,決定js代碼的執(zhí)行順序。進(jìn)入整體代碼(宏任務(wù))后,開始第一次循環(huán)。接著執(zhí)行所有的微任務(wù)。然后再次從宏任務(wù)開始,找到其中一個(gè)任務(wù)隊(duì)列執(zhí)行完畢,再執(zhí)行所有的微任務(wù)。
setTimeout,將其回調(diào)函數(shù)注冊(cè)后分發(fā)到宏任務(wù)Event Queue。(同理setInterval)
Promise,new Promise立即執(zhí)行,then函數(shù)分發(fā)到微任務(wù)Event Queue。
除了setTimeout和setInterval這兩個(gè)方法,Node.js還提供了另外兩個(gè)與"任務(wù)隊(duì)列"有關(guān)的方法:process.nextTick和setImmediate。它們可以幫助我們加深對(duì)"任務(wù)隊(duì)列"的理解。process.nextTick方法可以在當(dāng)前"執(zhí)行棧"的尾部----下一次Event Loop(主線程讀取"任務(wù)隊(duì)列")之前----觸發(fā)回調(diào)函數(shù)。也就是說,它指定的任務(wù)總是發(fā)生在所有異步任務(wù)之前。
setImmediate方法則是在當(dāng)前"任務(wù)隊(duì)列"的尾部添加事件,也就是說,它指定的任務(wù)總是在下一次Event Loop時(shí)執(zhí)行,這與setTimeout(fn, 0)很像。
process.nextTick(function A() {
console.log(1);
process.nextTick(function B(){console.log(2);});
});
setTimeout(function timeout() {
console.log('TIMEOUT FIRED');
}, 0)
// 1
// 2
// TIMEOUT FIRED
上面代碼中,由于process.nextTick方法指定的回調(diào)函數(shù),總是在當(dāng)前"執(zhí)行棧"的尾部觸發(fā),所以不僅函數(shù)A比setTimeout指定的回調(diào)函數(shù)timeout先執(zhí)行,而且函數(shù)B也比timeout先執(zhí)行。這說明,如果有多個(gè)process.nextTick語句(不管它們是否嵌套),將全部在當(dāng)前"執(zhí)行棧"執(zhí)行。
setImmediate(function (){
setImmediate(function A() {
console.log(1);
setImmediate(function B(){console.log(2);});
});
setTimeout(function timeout() {
console.log('TIMEOUT FIRED');
}, 0);
});
// 1
// TIMEOUT FIRED
// 2
上面代碼中,setImmediate和setTimeout被封裝在一個(gè)setImmediate里面,它的運(yùn)行結(jié)果總是1--TIMEOUT FIRED--2,這時(shí)函數(shù)A一定在timeout前面觸發(fā)。至于2排在TIMEOUT FIRED的后面(即函數(shù)B在timeout后面觸發(fā)),是因?yàn)閟etImmediate總是將事件注冊(cè)到下一輪Event Loop,所以函數(shù)A和timeout是在同一輪Loop執(zhí)行,而函數(shù)B在下一輪Loop執(zhí)行。
我們由此得到了process.nextTick和setImmediate的一個(gè)重要區(qū)別:多個(gè)process.nextTick語句總是在當(dāng)前"執(zhí)行棧"一次執(zhí)行完,多個(gè)setImmediate可能則需要多次loop才能執(zhí)行完。
另外,由于process.nextTick指定的回調(diào)函數(shù)是在本次"事件循環(huán)"觸發(fā),而setImmediate指定的是在下次"事件循環(huán)"觸發(fā),所以很顯然,前者總是比后者發(fā)生得早,而且執(zhí)行效率也高(因?yàn)椴挥脵z查"任務(wù)隊(duì)列")。
最后來一段比較復(fù)雜的代碼
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')
})
})
process.nextTick(function() {
console.log('6');
})
new Promise(function(resolve) {
console.log('7');
resolve();
}).then(function() {
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,9,11,3,10,5,12
最后的最后
- javascript是一門單線程語言
- Event Loop是javascript的執(zhí)行機(jī)制
【Vue中的異步更新隊(duì)列】
可能你還沒有注意到,Vue 在更新 DOM 時(shí)是異步執(zhí)行的。只要偵聽到數(shù)據(jù)變化,Vue 將開啟一個(gè)隊(duì)列,并緩沖在同一事件循環(huán)中發(fā)生的所有數(shù)據(jù)變更。如果同一個(gè) watcher 被多次觸發(fā),只會(huì)被推入到隊(duì)列中一次。這種在緩沖時(shí)去除重復(fù)數(shù)據(jù)對(duì)于避免不必要的計(jì)算和 DOM 操作是非常重要的。然后,在下一個(gè)的事件循環(huán)“tick”中,Vue 刷新隊(duì)列并執(zhí)行實(shí)際 (已去重的) 工作。Vue 在內(nèi)部對(duì)異步隊(duì)列嘗試使用原生的 Promise.then 和 MessageChannel,如果執(zhí)行環(huán)境不支持,則會(huì)采用 setTimeout(fn, 0) 代替。
例如,當(dāng)你設(shè)置 vm.someData = 'new value',該組件不會(huì)立即重新渲染。當(dāng)刷新隊(duì)列時(shí),組件會(huì)在下一個(gè)事件循環(huán)“tick”中更新。多數(shù)情況我們不需要關(guān)心這個(gè)過程,但是如果你想基于更新后的 DOM 狀態(tài)來做點(diǎn)什么,這就可能會(huì)有些棘手。雖然 Vue.js 通常鼓勵(lì)開發(fā)人員使用“數(shù)據(jù)驅(qū)動(dòng)”的方式思考,避免直接接觸 DOM,但是有時(shí)我們必須要這么做。為了在數(shù)據(jù)變化之后等待 Vue 完成更新 DOM,可以在數(shù)據(jù)變化之后立即使用Vue.nextTick(callback)這樣回調(diào)函數(shù)將在 DOM 更新完成后被調(diào)用。例如:
<div id="example">{{message}}</div>
Vue.component('example', {
template: '<span>{{ message }}</span>',
data: function () {
return {
message: '未更新'
}
},
methods: {
updateMessage: function () {
this.message = '已更新'
console.log(this.$el.textContent) // => '未更新'
this.$nextTick(function () {
console.log(this.$el.textContent) // => '已更新'
})
}
}
})
因?yàn)?$nextTick() 返回一個(gè) Promise 對(duì)象,所以你可以使用新的 ES2016 async/await 語法完成相同的事情:
methods: {
updateMessage: async function () {
this.message = '已更新'
console.log(this.$el.textContent) // => '未更新'
await this.$nextTick()
console.log(this.$el.textContent) // => '已更新'
}
}