JavaScript異步編程
js為什么是單執行緒的
JavaScript單執行緒,與它的用途有關,作為瀏覽器腳本語言,JavaScript 的主要用途是與用戶互動,以及操作 DOM,這決定了它只能是單執行緒,否則會帶來很復雜的同步問題,
比如,假定JavaScript同時有兩個執行緒,一個執行緒在某個 DOM 節點上添加內容,另一個執行緒洗掉了這個節點,這時瀏覽器應該以哪個執行緒為準?
所以,為了避免復雜性,從一誕生,JavaScript 就是單執行緒的,
js單執行緒會遇到哪些問題
js是單執行緒的,所以它執行代碼時是順序執行的,這樣不可避免的會產生一些問題:當一段代碼耗時特別長的時候,js執行緒會被阻塞,出現卡死的情況(這是因為js是單執行緒的,它要將這段耗時的代碼執行完成之后才會執行后面的代碼),比如說io操作,請求資料這些情況,
對于這個問題,js使用了同步,異步,回呼函式來解決,
異步vs同步&回呼函式
- 同步: 同步就是代碼的順序執行,比如這樣:
console.log('我是同步代碼1');
console.log('我是同步代碼2');
console.log('我是同步代碼3');
它會按照代碼撰寫的順序,依次執行下去,
- 異步: 不是同步的就是異步的,這樣說有點抽象,具體來講就是:js同步執行的程序中,碰到一段異步代碼,它會將這段異步代碼交給瀏覽器 引擎
擇機執行,js會跳過這段異步代碼,接著執行后面的同步代碼,就像這樣:
console.log('我是同步代碼1');
console.log('我是同步代碼2');
// js會將下面這段異步代碼交給瀏覽器引擎,跳過這段異步代碼,接著執行后面的同步代碼,
(function(){
xxxxxxxxxxxxxx
console.log('我是異步代碼');
})();
console.log('我是同步代碼3');
- 回呼函式:js將異步代碼交給瀏覽器引擎
擇機執行,執行完成之后呢,會發生什么,就是說如果這段代碼執行失敗了要干什么,執行成功了要干什么,這個干什么就是回呼函式,就像這樣:
console.log('我是同步代碼1');
console.log('我是同步代碼2');
// js會將下面這段異步代碼交給瀏覽器引擎,跳過這段異步代碼,接著執行后面的同步代碼,
(function(successFun, failseFun){
xxxxxxxxxxxxxx
console.log('我是異步代碼');
if(異步代碼執行成功){
successFun(); // 這個就是回呼函式
}else{
failseFun(); // 這個也是回呼函式
}
})();
console.log('我是同步代碼3');
上面的解釋有些不太嚴肅,我們看一下比較嚴肅的描述:
javascript是單執行緒,單執行緒就意味著,所有任務需要排隊,前一個任務結束,才會執行后一個任務,如果前一個任務耗時很長,后一個任務就不得不一直等著,于是就有一個概念——任務佇列,
如果排隊是因為計算量大,CPU忙不過來,倒也算了,但是很多時候CPU是閑著的,因為IO設備(輸入輸出設備)很慢(比如Ajax操作從網路讀取資料),不得不等著結果出來,再往下執行,于是JavaScript語言的設計者意識到,這時主執行緒完全可以不管IO設備,掛起處于等待中的任務,先運行排在后面的任務,等到IO設備回傳了結果,再回過頭,把掛起的任務繼續執行下去,
于是,所有任務可以分成兩種,一種是同步任務(synchronous),另一種是異步任務(asynchronous),
同步任務指的是,在主執行緒上排隊執行的任務,只有前一個任務執行完畢,才能執行后一個任務;
異步任務指的是,不進入主執行緒、而進入"任務佇列"(task queue)的任務,只有等主執行緒任務執行完畢,"任務佇列"開始通知主執行緒,請求執行任務,該任務才會進入主執行緒執行,
具體來說,異步運行機制如下:
- (1)所有同步任務都在主執行緒上執行,形成一個執行堆疊(execution context stack),
- (2)主執行緒之外,還存在一個"任務佇列"(task queue),只要異步任務有了運行結果,就在"任務佇列"之中放置一個事件,
- (3)一旦"執行堆疊"中的所有同步任務執行完畢,系統就會讀取"任務佇列",看看里面有哪些事件,那些對應的異步任務,于是結束等待狀態,進入執行堆疊,開始執行,
- (4)主執行緒不斷重復上面的第三步,
回呼地獄
js單執行緒,同步任務,異步任務,回呼函式這四者組合起來當然可以解決js世界中所有的問題,但是會帶來一些麻煩,這個麻煩就是:我在回呼函式中繼續執行異步任務,
按照我們的理解,回呼函式式這樣作業的:
console.log('我是同步任務1');
asynFun(callBack);
console.log('我是同步任務2');
這當然很好,可以完美解決我們的問題,但是如果這樣呢:
console.log('我是同步任務1');
asynFun(function callBack() {
asynFun2(function callBack2() {
asynFun3(function callBack3(){
asynFun4...
});
});
});
console.log('我是同步任務2');
看,發生了什么,我們會發現這種場景下會無限套娃,這種情況就是回呼地獄,
回呼地獄雖然能解決我們的問題,但是它并不符合我們的閱讀習慣,我們不習慣一層又一層的去閱讀代碼,我們還是比較習慣順序的去閱讀代碼,為了解決回呼地獄的問題,promise應運而生,
promise是怎么產生的
promise的產生就是為了解決回呼地獄的,它是commonjs標準,然后被收錄到es5中,promise之所以受到歡迎,是因為它可以鏈式呼叫,就像這樣:
console.log('我是同步任務1');
asynFun().then( () => {
return asynFun2();
}).then( () => {
return asynFun3()
}).then...
console.log('我是同步任務2');
是不是這樣更符合我們的閱讀習慣,
js的執行時序
寫到這里,js異步編程,就只有一個問題沒有解釋清楚了,擇機執行,到底是怎樣擇機的,看下面的代碼:
console.log('start');
setTimeout(() => {
console.log('setTimeout');
}, 0);
new Promise( (resolve, reject) => {
console.log('promise'),
resolve(1);
}).then((res) => {
console.log('promise value: ', res);
});
console.log('end');
上面這段代碼中,promise和setTimeout都是異步任務,那么js應該先執行誰?上面這段代碼的執行結果是什么?
為了解決這個問題,我們先來探究下訊息佇列與事件回圈,
訊息佇列與事件回圈
JavaScript 確實只有一個執行緒(由js引擎維護),這個執行緒用來解釋和執行 JavaScript 代碼,我們稱之為“主執行緒”,
瀏覽器中還存在其它執行緒,例如:處理ajax、dom、定時器等,我們稱他們為“作業執行緒”,同時瀏覽器還維護了一個訊息佇列,主執行緒會將執行程序中遇到的異步請求,發送給訊息佇列,等到主執行緒空閑時再來執行訊息佇列中的任務,
js異步執行程序中的任務佇列就是訊息佇列,
主執行緒在執行程序中遇到了異步任務,就發起函式或者稱為注冊函式,通過event loop通知相應的作業執行緒,同時主執行緒繼續往后執行,不會等待,等到作業執行緒完成了任務,event loop 會將訊息添加到訊息佇列中,如果此時呼叫堆疊為空,就執行訊息佇列中排在最前面的訊息,依次執行,
事件回圈,就是主執行緒重復從訊息佇列中取訊息、執行的程序,
宏任務與微任務
一個執行緒中,事件回圈是唯一的,但任務佇列可以擁有多個,任務佇列由分為“宏任務”和“微任務”,
宏任務大概包括:script(整體代碼)、setTimeout、setInterval、setImmediate、I/O、UI Rendering;
微任務大概包括:process.nextTick、Promise、MutationObserver(H5新特性)
舉一個形象的類比: 銀行業務辦理
銀行中一堆人在排隊辦理業務,按照排隊的順序柜臺作業人員依次給每個人辦理業務,每個人要辦理的業務就是一個宏任務;
當輪到你辦理業務了,你要存錢,存完之后你又想起你還想辦個信用卡,順便轉個賬,這個額外的業務就是一個微任務,
所以:
宏任務執行程序中產生的新的任務可以作為一個新的宏任務插入到宏任務佇列(訊息佇列)中等待被執行;
也可以作為當前任務的微任務插入到當前任務的微任務隊里中,等待當前任務結束后依次執行當前任務的微任務佇列中的微任務,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qianduan/352236.html
標籤:其他
