JS線程是單線程運行機制,就是自己按順序做自己的事,瀏覽器線程用於交互和控制,JS可以操作DOM元素,
說起JS中的異步時,我們需要注意的是,JS中其實有兩種異步,一種是基於瀏覽器的異步IO,比如Ajax,另外一種是基於計時方法setTimeout和setInterval的異步。
對於異步IO,比如ajax,寫代碼的時候都是順序執行的,但是在真正處理請求的時候,有一個單獨的瀏覽器線程來處理,並且在處理完成后會觸發回調。這種情況下,參與異步過程的其實有2個線程主體,一個是javascript的主線程,另一個是瀏覽器線程。
熟悉Javascript的人都知道,Javascript內部有一個事件隊列,所有的處理方法都在這個隊列中,Javascript主線程就是輪詢這個隊列處理,這個好處是使得CPU永遠是忙碌狀態。這個機制和Windows中的消息泵是一樣的,都是靠消息(事件)驅動,
對於setTimeout和setInterval來說,當js線程執行到該代碼片段時,js主線程會根據所設定的時間,當設定的時間到期時,將設置的回調函數放到事件隊列中,然后js主線程繼續去執行下邊的代碼,當js線程執行完主線程上的代碼之后,會去循環執行事件隊列中的函數。至於setTimeout或者setInterval設定的執行時間在實際表現時會有一些偏差,普遍的一個解釋為,當定時任務的時間到期時,本應該去執行該回調函數,但是這時js主線程可能還有任務在執行,或者是該回調函數再事件隊列中排的比較靠后,就導致該回調函數執行時間與定時器設定時間不一致。
那么問題來了,什么是事件隊列?
eventloop是一個用作隊列的數組,eventloop是一個一直在循環執行的,循環的每一輪成為一個tick,在每一個tick中,如果隊列中有等待事件,那么就會從隊列中摘取下一個事件進行執行,這些事件就是我們之前的回調函數。現在ES6精確指定了事件循環的工作細節,這意味着在技術上將其納入了JavaScript引擎的勢力范圍,而不只是由宿主環境決定了,主要的一個原因是ES6中promise的引入。
var eventloop = []
var event;
while(true){
if(eventloop.length>0){
//拿到隊列中的下一個事件
event = eventloop.shift();
//現在執行下一個事件
try{
event();
}catch(e){
reportError(e);
}
}
}
在瀏覽器端,setTimeout中的最小時間間隔是W3C在HTML標准中規定,規定要求低於4ms的時間間隔算為4ms。
任何時候,只要把一個代碼包裝成一個函數,並指定它在響應某個事件時執行,你就是在代碼中創建了一個將來執行的模塊,也由此在這個程序中引入了異步機制。
js引擎並不是獨立運行的,它運行在宿主環境中,就是我們所看到的Web瀏覽器,當然,隨着js的發展,包括最近的Node,便是給js提供了一個在服務器端運行的環境。並且現在的js還嵌入到了機器人到電燈泡的各種各樣的設配中。
但是這些所有的環境都有一個共同的“點”,即為都提供了一種機制來處理程序中的多個塊的執行,且執行每個塊時調用JavaScript引擎,這種機制被稱為事件循環。
js引擎本身並沒有時間的概念,只是一個按需要執行JavaScript任意代碼片段的環境。
對於js中的回調,我們最常見的就是鏈式回調和嵌套回調
我們經常再ajax中嵌套ajax調用然后再嵌套ajax調用,這就是回調地獄,回調最大的問題就是控制反轉,它會導致信任鏈的完全斷裂,為了解決回調中的控制反轉問題,有些API提供了分離回調(一個用於成功通知,一個用於失敗通知),例如ajax中的success函數和failure函數,這種情況下,API的出錯處理函數failure()常常是可以省略的,如果沒有提供的話,就是假定這個錯誤可以吞掉。
還有一種回調模式叫做“error-first"風格,其中回調的第一個參數保留用作錯誤對象,如果成功的話,這個參數就會被清空/置假。
回調函數是JavaScript異步的基礎單元,但是隨着JavaScript越來越成熟,對於異步領域的發展,回調已經不夠用了。
Promise
Promise 是異步編程中的一種解決方案,最早由社區提出和實現,ES6將其寫進了語言標准,統一了用法,原生提供了Promise對象。
Promise是一種封裝和組合未來值的易於復用的機制。一種在異步任務中作為兩個或更多步驟的流程控制機制,時序上的this-then-that. 假定調用一個函數foo(),我們並不需要去關心foo中的更多細節,這個函數可能立即完成任務,也可能過一段時間才去完成。對於我們來講,我們只需要知道foo()什么時候完成任務,這樣我們就可以去繼續執行下一個任務了,在傳統的方法中,我們回去選擇監聽這個函數的完成度,當它完成時,通過回調函數通知我們,這時候通知我們就是執行foo中的回調,但是使用promise時,我們要做的是偵聽來自foo的事件,然后在得到通知的時候,根據情況而定。
其中一個重要的好出就是,我們可以把這個事件中的偵聽對象提供給代碼中多個獨立的部分,在foo()完成的時候,他們都可以獨立的得到通知:
var evt = foo();
//讓bar()偵聽foo()的完成
bar(evt);
//讓baz()偵聽foo()的完成
baz(evt);
上邊的例子中,bar和baz中不需要去知道或者關注foo中的實現細節。而且foo也不需要去關注baz和bar中的實現細節。
同樣的道理,在promise中,前面的代碼片段會讓foo()創建並返回一個Promise實例,而且在這個Promise會被傳遞到bar()和baz()中。所以本質上,promise就是某個函數返回的對象。你可以把回調函數綁定再這個對象上,而不是把回調函數當成參數傳進函數。
const promise = doSomething();
promsie.then(successCallback,failureCallback){
}
當然啦,promise不像舊式函數將回調函數傳遞到兩個處理函數中,而且會有一個優點:
- 在JavaScript事件隊列的本次tick運行完成之前,回調函數永遠不會執行。
- 通過.then形式添加的回調函數,甚至都在異步操作完成之后才被添加的函數,都會被調用。
- 通過多次調用.then,可以添加多個回調函數,他們會按照插入順序並且獨立運行。
但是,Promise最直接的好出就是鏈式調用。
doSomething().then(function(result) {
return doSomethingElse(result);
})
.then(function(newResult) {
return doThirdThing(newResult);
})
.then(function(finalResult) {
console.log('Got the final result: ' + finalResult);
})
.catch(failureCallback);
並且在一個失敗操作之后還可以繼續使用鏈式操作,即使鏈式中的一個動作失敗之后還能有助於新的動作繼續完成。
在調用Promise中的resolve()和reject()函數時如果帶有參數,那么他們的參數會被傳遞給回調函數。
Promise.resolve()和Promise.reject()是手動創建一個已經resolve或者reject的promise快捷方法。通常,我們可以使用Promise.resolve()去鏈式調用一個由異步函數組成的數組。例如:
Promise.resolve().then(func1).then(func2);
Promise.all()和Promise。race()是並行運行異步操作的兩個組合式工具。
Promise.then()方法用來分別指定resolved狀態和rejected狀態的回調函數。傳遞到then中的函數被置入了一個微任務隊列,而不是立即執行,這意味着它是在JavaScript事件隊列的所有運行結束了,事件隊列被清空之后才開始執行
let promise = new Promise(function(resolve, reject) {
console.log('Promise');
resolve();
});
promise.then(function() {
console.log('resolved.');
});
console.log('Hi!');
// Promise
// Hi!
// resolved
Promise.then()方法返回一個Promise,它最多需要有兩個參數:Promise的成功和失敗情況的回調函數。
p.then(onFulfilled, onRejected);
p.then(function(value) {
// fulfillment
}, function(reason) {
// rejection
});
onFulfilled:當Promise變成接受狀態(fulfillment)時,該參數作為回調函數被調用。該函數有一個參數,即接受的值。
onRejected:當Promise變成拒絕狀態時,該參數作為回調函數被調用。該函數有一個參數,即拒絕的原因。
Promise的狀態一旦改變,就永久保持該狀態,不會再改變了。
Promise中的錯誤處理
一般的情況,我們會在每次的Promise中拋出錯誤,在Promise中的then函數中的rejected處理函數會被調用,這是我們作為錯誤處理的常用方法:
let p = new Promise(function(resolve,reject){
reject('error');
});
p.then(function(value){
success(value);
},function(error){
error(error)
}
)
但是一種更好的方式是使用catch函數,這樣可以處理Promise內部發生的錯誤,catch方法返回的還是一個Promise對象,后邊還可以接着調用then方法。而且catch方法盡量寫在鏈式調用的最后一個,避免后邊的then方法的錯誤無法捕獲。
let p = new Promise(function(resolve,reject){
reject('error');
});
p.then(function(value){
success(value);
}).catch(function(error){
console.log('error');
}}
Promise.finally()函數,該方法是ES2018引入標准的。指定不管Promise對象最后狀態如何,都會執行的操作。finally方法的回調函數不接受任何參數,這意味着沒有辦法知道,前面的Promise狀態到底是fulfilled還是rejected。這標明,finally方法里面的操作,是與狀態無關的,不依賴於Promise的執行結果。
上述文章,如有錯誤,還請指正,謝謝!!!