原文地址:banggan.github.io/2019/08/26/…javascript
javascript
是一門單線程的語言,也就是說一次只能完成一件任務,若是有多個任務,就須要排隊進行處理。若是一個任務耗時很長,後面的任務也必須排隊等待,這樣大大的影響了整個程序的執行。爲了解決這個問題,javascript
語言將任務分爲兩種模式:java
本文主要針對近兩年javascript
的發展,主要介紹異步處理的進化史。目前,在javascript
異步處理中,有如下幾種方式:jquery
回調函數是最先解決異步編程的方法。不管是常見的
setTimeout
仍是ajax
請求,都是採用回調的形式把事情在某一固定的時刻進行執行。git
 //常見的:setTimeout
 setTimeout(function callback(){
  console.log('aa');
}, 1000);
//ajax請求
ajax(url,function callback(){
console.log("ajax success",res);
})
複製代碼
回調函數的處理通常將函數
callback
做爲參數傳進函數,在合適的時候被調用執行。回調函數的優勢就是簡單、容易理解和實現,但有個致命的缺點,容易出現回調地獄(Callback hell),即多個回調函數嵌套使用。形成代碼可讀性差、可維護性差且只能在回調中處理異常。github
ajax(url, () => {
//todo
ajax(url1, () => {
//todo
ajax(url2, () => {
//todo
})
})
})
複製代碼
事件監聽採用的是事件驅動的模式。事件的執行不取決於代碼的順序,而是某個事件的發生。ajax
假設有兩個函數,爲f1綁定一個事件(jQuery
的寫法),當f1函數發生success
事件時,執行函數f2:編程
f1.on('success',f2);
複製代碼
對f1進行改寫:數組
function f1(){
ajax(url,() => {
//todo
f1.trigger('success');//觸發success事件,從而執行f2函數
})
}
複製代碼
事件監聽的方式較容易理解,能夠綁定多個事件,每一個事件能夠指定多個回調函數,並且能夠"去耦合",有利於實現模塊化。缺點是整個程序都要變成事件驅動型,運行流程會變得很不清晰。閱讀代碼的時候,很難看出主流程。promise
咱們假定,存在一個"信號中心",某個任務執行完成,就向信號中心"發佈"(publish)一個信號,其餘任務能夠向信號中心"訂閱"(subscribe)這個信號,從而知道何時本身能夠開始執行。這就叫作 發佈/訂閱模式(publish-subscribe pattern),又稱**觀察者模式"(observer pattern) **。併發
//利用jquery的插件實現
//首先,f2向消息中心訂閱success事件
jQuery.subscribe('success',f2);
//對f1進行改寫:
function f1(){
ajax(url,() => {
//todo
jQuery.publish('success');//當f1執行完畢後,向消息中心jQuery發佈success事件,從而執行f2函數
})
}
//f2執行完畢後,能夠取消訂閱
jQuery.unsubscribe('success',f2)
複製代碼
該方法和事件監聽的性質相似,但咱們能夠經過消息中心來查閱一共有多少個信號,每一個信號有多少個訂閱者。
**Promise**是CommonJS工做組提出的一種規範,能夠獲取異步操做的消息,也是異步處理中經常使用的一種解決方案。Promise的出現主要是用來解決回調地獄、支持多個併發的請求,獲取併發請求的數據而且解決異步的問題。
let p = new Promise((resolve, reject) => {
//作一些異步操做
setTimeout(()=>{
let num = parseInt(Math.random()*100);
if(num > 50){
resolve("num > 50"); // 若是數字大於50就調用成功的函數,而且將狀態變成Resolved
}else{
reject("num <50");// 不然就調用失敗的函數,將狀態變成Rejected
}
},10000)
});
p.then((res) => {
console.log(res);
}).catch((err) =>{
console.log(err);
})
複製代碼
Promise
有三種狀態:等待pending
、成功fulfied
、失敗rejected
;狀態一旦改變,就不會再變化,在Promise
對象建立後,會立刻執行。等待狀態能夠變爲fulfied
狀態並傳遞一個值給相應的狀態處理方法,也可能變爲失敗狀態rejected
並傳遞失敗信息。任一一種狀況出現時,Promise
對象的then
方法就會被調用(then方法包含兩個參數:onfulfilled 和 onrejected,均爲 Function。當Promise狀態爲fulfilled時,調用 then 的 onfulfilled 方法,當Promise狀態爲rejected時,調用 then 的 onrejected 方法)。須要注意的是:
Promise.prototype.then
和Promise.prototype.catch
方法返回promise 對象, 因此能夠被鏈式調用,以下圖:
Promise
的方法:
Promise.all(iterable)
:誰執行得慢,以誰爲準執行回調。返回一個promise
對象,只有當iterable
裏面的全部promise
對象成功後纔會執行。一旦iterable
裏面有promise
對象執行失敗就觸發該對象的失敗。對象在觸發成功後,會把一個包iterable
裏全部promise
返回值的數組做爲成功回調的返回值,順序跟iterable
的順序保持一致;若是這個新的promise對象觸發了失敗狀態,它會把iterable
裏第一個觸發失敗的promise對象的錯誤信息做爲它的失敗錯誤信息。Promise.all
方法常被用於處理多個promise
對象的狀態集合。Promise.race(iterable)
: 誰執行得快,以誰爲準執行回調。iterable
參數裏的任意一個子promise被成功或失敗後,父promise立刻也會用子promise的成功返回值或失敗詳情做爲參數調用父promise綁定的相應句柄,並返回該promise對象。Promise.reject(err)
與Promise.resolve(res)
Generators是ES6提供的異步解決方案,其最大的特色就是能夠控制函數的執行。能夠理解成一個內部封裝了不少狀態的狀態機,也是一個遍歷器對象生成函數。Generator 函數的特徵:
function
關鍵字與函數名之間有一個星號;- 函數體內部使用
yield
表達式,定義不一樣的內部狀態;
- 經過
yield
暫停函數,next
啓動函數,每次返回的是yield
表達式結果。next
能夠接受參數,從而實如今函數運行的不一樣階段,能夠從外部向內部注入不一樣的值。next
返回一個包含value
和done
的對象,其中value
表示迭代的值,後者表示迭代是否完成。舉個例子:
function* createIterator(x) {
let y = yield (x+1)
let z = 2*(yield(y/3))
return (x+y+z)
}
// generators能夠像正常函數同樣被調用,不一樣的是會返回一個 iterator
let iterator = createIterator(4);
console.log(iterator.next()); // {value:5,done:false}
console.log(iterator.next()); // {value:NaN,done:false}
console.log(iterator.next()); // {value:NaN,done:true}
let iterator1 = createIterator(4);//返回一個iterator
//next傳參數
console.log(iterator1.next()); // {value:5,done:false}
console.log(iterator1.next(12)); // {value:4,done:false}
console.log(iterator1.next(15)); // {value:46,done:true}
複製代碼
代碼分析:
當不參數時,next的value返回NaN;
當傳參數時,做爲上一個yeild的值,在第一次使用next時,傳參數無效,只有第二次開始,纔有效。
第一次執行next時,函數會被暫停在yeild(x+1),因此返回的是4+1=5;
第二次執行next時,傳入的12爲上一次yeild表達式的值,因此y=12,返回的是12/3=4;
第三次執行next時,傳入的15爲上一次yeild表達式的值,因此z=30,y=12;x=4,返回30+12+4=46
async/await
在ES7提出,是目前在javascript
異步處理的終極解決方案。
async
其本質是Generator
函數的語法糖。相較於Generator
放入改進以下:
- 內置執行器:Generator 函數的執行必須靠執行器,而
async
函數自帶執行器。其調用方式與普通函數如出一轍,不須要調next
方法;- 更好的語義:
async
表示定義異步函數,而await
表示後面的表達式須要等待,相較於*和yeild
更語義化;- 更廣的適用性:
co
模塊約定,yield
命令後面只能是Thunk
函數或Promise
對象。而async
函數的await
命令後面則能夠是Promise
或者 原始類型的值;- 返回
Promise
:async
函數返回值是Promise
對象,比Generator
函數返回的Iterator
對象方便,能夠直接使用then()
方法進行鏈式調用;
async
語法
用來定義異步函數,自動將函數轉換爲promise
對象,可使用then
來添加回調,其內部return
的值做爲then
回調的參數。
async function f(){
return "hello async";
}
f().then((res) => { //經過then來添加回調且內部返回的res做爲回調的參數
console.log(res); // hello async
})
複製代碼
在異步函數的內部可使用await
,其返回的promise
對象必須等到內部因此await
命令後的promise
對象執行完,纔會發生狀態變化即執行then
回調。
const delay = function(timeout){
return new Promise(function(resolve){
return setTimeout(resolve, timeout);
});
}
async function f(){
await delay(1000);
await delay(2000);
return '完成';
}
f().then(res => console.log(res));//須要等待3秒以後纔會打印:完成
複製代碼
await
即表示異步等待,用來暫停異步函數的執行,只能在異步函數和promise
使用,且當使用在promise
前面,表示等待promise
完成並返回結果。
async function f() {
return await 1 //await後面不是Promise的話,也會被轉換爲一個當即爲resolve的promise
};
f().then( res => console.log("處理成功",res))//打印出:處理成功 1
.catch(err => console.log("處理是被",err))////打印出:Promise{<resolved>:undefined}
複製代碼
若是
await
後面的異步出現錯誤,等同於async
返回的promise
對象爲reject
,其錯誤會被catch的回調函數接收到。須要注意的是,當async
函數中只要一個await
出現 reject 狀態,則後面的await
都不會被執行。
let a;
async function f(){
await Promise.reject("error")
a = await 1 //該await並無執行
}
err().then(res => console.log(a))
複製代碼
怎麼處理呢,能夠把第一個
await
放在try/catch
,遇到函數的時候,能夠將錯誤拋出並往下執行。
async function f() {
try{
await Promise.reject('error');
}catch(error){
console.log(error);
}
return await 1
}
f().then(res => console.log('成功', res))//成功打印出1
複製代碼
若是有多個
await
處理,能夠統一放在try/catch
模塊中,並且async
可使得try/catch
同時處理同步和異步錯誤。
經過以上六種
javascript
異步處理的經常使用方法,能夠看出async/await
能夠說是異步終極解決方案了,最後看一下async/await
用得最多的場景:若是一個業務須要不少個異步操做組成,而且每一個步驟都依賴於上一步的執行結果,這裏採用不一樣的延時來體現:
//首先定義一個延時函數
function delay(time) {
return new Promise(resolve => {
setTimeout(() => resolve(time), time);
});
}
//採用promise鏈式調用實現
delay(500).then(result => {
return delay(result + 1000)
}).then(result => {
return delay(result + 2000)
}).then(result => {
console.log(result) //3500ms後打印出3500
}).catch(error => {
console.log(error)
})
//採用async實現
async function f(){
const r1 = await delay(500)
const r2 = await delay(r1+1000)
const r3 = await delay(r2+2000)
return r3
}
f().then(res =>{
console.log(res)
}).catch(err=>{
console.log(err)
})
複製代碼
能夠看出,採用
promise
實現採用了不少then進行不停的鏈式調用,使得代碼變得冗長和複雜且沒有語義化。而async/await
首先使用同步的方法來寫異步,代碼很是清晰直觀,並且使代碼語義化,一眼就能看出代碼執行的順序,最後 async 函數自帶執行器,執行的時候無需手動加載。