衆所周知,Javascript語言的執行環境是單線程(single thread),做爲瀏覽器腳本語言,JavaScript的主要用途是與用戶互動,以及操做DOM。若以多線程的方式操做這些DOM,則可能出現操做的衝突。假設有兩個線程同時操做一個DOM元素,線程1要求瀏覽器刪除DOM,而線程2卻要求修改DOM樣式,這時瀏覽器就沒法決定採用哪一個線程的操做。固然,咱們能夠爲瀏覽器引入「鎖」的機制來解決這些衝突,但這會大大提升複雜性,因此JavaScript從誕生開始就選擇了單線程執行。
而單線程就是指一次只能完成一件任務。若是有多個任務,就必須排隊,前面一個任務完成,再執行後面一個任務。由於javascript 設計之初是爲瀏覽器設計的GUI編程語言,GUI編程的特性之一是保證UI線程必定不能阻塞,不然性能很差,可能會界面卡死,由於JavaScript是單線程的,有一個致命問題是在某一時刻內只能執行特定的一個任務,而且會阻塞其它任務執行,爲了解決這個問題,Javascript語言將任務的執行模式分紅同步(Synchronous)和異步(Asynchronous),在遇到相似I/O等耗時的任務時js會採用異步操做,而此時異步操做不進入主線程、而進入"任務隊列",只有"任務隊列"通知主線程,某個異步任務能夠執行了,該任務纔會進入主線程執行,這時就不會阻塞其它任務執,而這種模式稱爲js的事件循環機制(Event Loop)。javascript
- 同步:調用者發出調用後,在沒有獲得結果以前,該調用就不返回。後一個任務等待前一個任務結束,而後再執行,程序的執行順序與任務的排列順序是一致的、同步的,具備同步關係的一組任務相互發送的信息稱爲消息或事件。
- 異步:調用者發出調用後不會馬上獲得結果,該調用就返回了。每個任務有一個或多個回調函數(callback),前一個任務結束後,不是執行後一個任務,而是執行回調函數,後一個任務則是不等前一個任務結束就執行,因此程序的執行順序與任務的排列順序是不一致的、異步的,線程就是實現異步的一個方式,異步是讓調用方法的主線程不須要同步等待另外一線程的完成,從而可讓主線程幹其它的事情。
- 阻塞:指調用結果返回以前,調用者會進入阻塞狀態等待。只有在獲得結果以後纔會返回。
- 非阻塞:指在不能馬上獲得結果以前,該函數不會阻塞當前線程,而會馬上返回。
- 事件循環機制: (1)全部同步任務都在主線程上執行,造成一個執行棧(execution context stack)。 (2)主線程以外,還存在一個"任務隊列"(task queue)。只要異步任務有了運行結果,就在"任務隊列"之中放置一個事件。 (3)一旦"執行棧"中的全部同步任務執行完畢,系統就會讀取"任務隊列",看看裏面有哪些事件。那些對應的異步任務,因而結束等待狀態,進入執行棧,開始執行。 (4)主線程不斷重複上面的第三步,造成一個事件的循環。
![]()
- 阻塞非阻塞和同步異步的主要區別在於前者是相對於調用者來講,後者是相對於被調用者來講。舉個栗子,把js比做一個老公的話,有一天上班的時候老公在微信約她老婆今天晚上去吃飯,若是老婆看到消息後立刻贊成或者拒絕,對老婆來講這就是同步(老公的消息被老婆返回了,同時也獲得告終果),若是老婆看到消息後回覆說我晚上可能會加班還不肯定,過段時間肯定了我再來發條消息通知你結果(能夠理解爲回調函數),對老婆來講這就是異步(老公的消息被老婆返回了,可是還沒獲得結果,須要等待)。而在老婆尚未給出最終通知結果時(不論是同步回覆仍是異步回覆),若是此時老公打開另外一個微信窗口約小三明天晚上去吃飯,此時對老公來講就是非阻塞的,而若是老公在老婆沒有最終通知結果以前一直在那等着而沒幹其餘事情,對老公來講這就是阻塞的。顯而易見,在這裏老公是調用者,老婆是被調用者。
- 仍是上面那個栗子,若是老婆說要過段時間才能通知老公最後結果(也就是異步的時候),此時老公也不能在老婆通知前什麼都不幹就待在那裏,老公沒有分身,也就是說老公不是多線程的,他會把這個異步事件先擱置(也就是放到任務隊列裏) ,做爲單線程的他只能親自去處理其餘事情(主線程中處理執行棧),等老婆通知後再來處理這件事情(把這個異步事件從任務隊列中取回來在主線程中執行)。因此當js採用異步模式的時候js就是非阻塞了,這也就是爲何說node.js是非阻塞異步I/O了,由於異步和事件循環機制的特性使它是非阻塞的。
"異步模式"很是重要。在瀏覽器端,耗時很長的操做都應該異步執行,避免瀏覽器失去響應,最好的例子就是Ajax操做。在服務器端,"異步模式"甚至是惟一的模式,由於執行環境是單線程的,若是容許同步執行全部http請求,服務器性能會急劇降低,很快就會失去響應。最先異步模式採用的是回調函數的方法,可是這種方法不利於代碼的閱讀和維護,各個部分之間高度耦合,流程會很混亂,並且每一個任務只能指定一個回調函數,這樣就很容易陷入回調地獄,因此異步流程控制模式慢慢衍生出許多方式,下面主要來介紹這些方式有哪些。html
1.回調函數
有兩個任務函數taskFun1和taskFun2,若是按同步方式寫java
taskFun1();
taskFun2();
複製代碼
taskFun1()若是是一個很耗時的任務,會嚴重阻塞taskFun2()的執行,用回調函數能夠這樣寫:node
function taskFun1(callbackFun){
setTimeout(function () {
// do something
callbackFun();
}, 2000);   
}
taskFun1(taskFun2);
複製代碼
- 優勢:簡單、容易理解和部署,
- 缺點:不利於代碼的閱讀和維護,各個部分之間高度耦合,流程會很混亂,並且每一個任務只能指定一個回調函數。
2.事件監聽
ios
另外一種思路是採用事件驅動模式。任務的執行不取決於代碼的順序,而取決於某個事件是否發生。express
taskFun1.on("event", taskFun2);
function taskFun1(){
setTimeout(function () {
// taskFun1的任務代碼
taskFun1.trigger('event');    
}, 2000);
}
/* taskFun1.trigger('event')表示執行完成後,當即觸發事件,從而開始執行taskFun2。*/
複製代碼
- 優勢:比較容易理解,能夠綁定多個事件,每一個事件能夠指定多個回調函數,耦合度很低,有利於實現模塊化
- 缺點:整個程序都要變成事件驅動型,事件不能獲得流程控制,運行流程會變得很不清晰。
3.發佈/訂閱
編程
上一節的"事件",徹底能夠理解成"信號"。假定,存在一個"信號中心",某個任務執行完成,就向信號中心"發佈"(publish)一個信號,其餘任務能夠向信號中心"訂閱"(subscribe)這個信號,從而知道何時本身能夠開始執行。這就叫作"發佈/訂閱模式",又稱"觀察者模式"。axios
element.subscribe("event", taskFun2);
function taskFun1(){
setTimeout(function () {
// taskFun1的任務代碼
element.publish("event");
}, 2000);
}
複製代碼
- 優勢:能夠徹底掌握事件被訂閱的次數,以及訂閱者的信息,管理起來特別方便。
4.Promise對象
關於Promises的具體介紹和實現,能夠參考用ES6實現一個簡單易懂的Promisepromise
好比平時咱們經常使用的axios插件就是採用了promise模式:瀏覽器
axios.get('./demo.txt')
.then(function(response){
console.log(response);
})
.catch(function(err){
console.log(err);
});
複製代碼
而實現的機制就是promise把成功和失敗分別代理到resolved 和 rejected .
var promise = new Promise(function(resolve, reject) {
// 異步操做的代碼
if (/* 異步操做成功 */){
resolve(value);
} else {
reject(error);
}
});
複製代碼
- 優勢:回調函數變成了鏈式寫法,程序的流程能夠看得很清楚,能夠實現許多強大的功能,同時還能夠捕獲到catch異常。
- 缺點:寫法和理解起來都相對費勁
5.Generator與co相結合
與promise不一樣的是,Generator設計的初衷並非爲了來控制異步流程的,這種寫法是express和koa框架的做者拿Generator與co相結合的一種寫法,因爲generator是一個狀態機,因此須要手動調用next 才能執行,node框架的做者開發了co模塊,能夠自動執行generator,能夠理解爲一種geek寫法。
function readFile(filename) {
return new Promise(function (resolve, reject) {
fs.readFile(filename, 'utf8', function (err, data) {
err ? reject(err) : resolve(data);
});
})
}
function *read() {
console.log('開始');
let a = yield readFile('1.txt');
console.log(a);
let b = yield readFile('2.txt');
console.log(b);
let c = yield readFile('3.txt');
console.log(c);
return c;
}
co(read).then(function (data) {
console.log(data);
});
複製代碼
- 優勢:能夠用同步的方式編寫異步代碼
- 缺點:不夠直觀,沒有語義化
6.await,async
await,async是ES7 引入了的關鍵字,async函數徹底能夠看做多個異步操做,包裝成的一個Promise對象,實質上是generator+promise的語法糖
*async function read(){
//await後面必須跟一個promise,
let a = await readFile('./1.txt');
console.log(a);
let b = await readFile('./2.txt');
console.log(b);
let c = await readFile('./3.txt');
console.log(c);
return 'ok';
}*/
複製代碼
- 優勢:相比於以前的方式有很好的語義,實現也比較簡單,被認爲是目前最優的異步流程控制模式。