JavaScript的執行機制在上篇文章中進行了深入的探討,那麼既然是一門單線程語言,如何進行良好體驗的異步編程呢
回調函數Callbacks
當程序跑起來時,一般情況下,應用程序(application program)會時常通過API調用庫裏所預先備好的函數。但是有些庫函數(library function)卻要求應用先傳給它一個函數,好在合適的時候調用,以完成目標任務。這個被傳入的、後又被調用的函數就稱為回調函數(callback function)。
什麼是異步
"調用"在發出之後,這個調用就直接返回了,所以沒有返回結果。換句話説,當一個異步過程調用發出後,調用者不會立刻得到結果。而是在"調用"發出後,"被調用者"通過狀態、通知來通知調用者,或通過回調函數處理這個調用。異步調用發出後,不影響後面代碼的執行。
簡單説就是一個任務分成兩段,先執行第一段,然後轉而執行其他任務,等做好了準備,再回過頭執行第二段。
在異步執行的模式下,每一個異步的任務都有其自己一個或着多個回調函數,這樣當前在執行的異步任務執行完之後,不會馬上執行事件隊列中的下一項任務,而是執行它的回調函數,而下一項任務也不會等當前這個回調函數執行完,因為它也不能確定當前的回調合適執行完畢,只要引它被觸發就會執行,
地獄回調階段
異步最早的解決方案是回調函數,如事件的回調,setInterval/setTimeout中的回調。但是回調函數有一個很常見的問題,就是回調地獄的問題
下面這幾種都屬於回調
- 事件回調
- Node API
- setTimeout/setInterval中的回調函數
- ajax 請求
異步回調嵌套會導致代碼難以維護,並且不方便統一處理錯誤,不能 try catch會陷入回調地獄
fs.readFile(A, 'utf-8', function(err, data) {
fs.readFile(B, 'utf-8', function(err, data) {
fs.readFile(C, 'utf-8', function(err, data) {
fs.readFile(D, 'utf-8', function(err, data) {
//....
});
});
});
});
ajax(url, () => {
// 處理邏輯
ajax(url1, () => {
// 處理邏輯
ajax(url2, () => {
// 處理邏輯
})
})
})
Promise解決地獄回調階段
Promise 一定程度上解決了回調地獄的問題,Promise 最早由社區提出和實現,ES6 將其寫進了語言標準,統一了用法,原生提供了Promise對象。
- Promise存在三個狀態(state)pending、fulfilled、rejected
- pending(等待態)為初始態,並可以轉化為fulfilled(成功態)和rejected(失敗態)
- 成功時,不可轉為其他狀態,且必須有一個不可改變的值(value)
- 失敗時,不可轉為其他狀態,且必須有一個不可改變的原因(reason)
- new Promise((resolve, reject)=>{resolve(value)}) resolve為成功,接收參數value,狀態改變為fulfilled,不可再次改變。
- new Promise((resolve, reject)=>{reject(reason)}) reject為失敗,接收參數reason,狀態改變為rejected,不可再次改變。
- 若是executor函數報錯 直接執行reject();
Promise 是一個構造函數,new Promise 返回一個 promise對象
const promise = new Promise((resolve, reject) => {
// 異步處理
// 處理結束後、調用resolve 或 reject
});
then方法註冊 當resolve(成功)/reject(失敗)的回調函數
// onFulfilled 參數是用來接收promise成功的值,
// onRejected 參數是用來接收promise失敗的原因
//兩個回調返回的都是promise,這樣就可以鏈式調用
promise.then(onFulfilled, onRejected);
const promise = new Promise((resolve, reject) => {
resolve('fulfilled'); // 狀態由 pending => fulfilled
});
promise.then(result => { // onFulfilled
console.log(result); // 'fulfilled'
}, reason => { // onRejected 不會被調用
})
then方法的鏈式調用
Promise對象的then方法返回一個新的Promise對象,因此可以通過鏈式調用then方法。then方法接收兩個函數作為參數,第一個參數是Promise執行成功時的回調,第二個參數是Promise執行失敗時的回調。兩個函數只會有一個被調用,函數的返回值將被用作創建then返回的Promise對象。這兩個參數的返回值可以是以下三種情況中的一種:
return了一個值,那麼then返回的 Promise 將會成為接受(resolved)狀態,並且將返回的值作為接受狀態的回調函數的參數值。- 沒有返回任何值(,默認返回
undefined),那麼then返回的 Promise 將會成為接受(resolved)狀態,並且該接受狀態的回調函數的參數值為undefined。 - 拋出(
throw)一個錯誤,那麼then返回的 Promise 將會成為拒絕狀態,並且將拋出的錯誤作為拒絕狀態的回調函數的參數值。 -
return 另一個 Promise,then方法將根據這個Promise的狀態和值創建一個新的Promise對象返回。
- 返回一個已經是接受狀態的 Promise,那麼
then返回的 Promise 也會成為接受狀態,並且將那個 Promise 的接受狀態的回調函數的參數值作為該被返回的Promise的接受狀態回調函數的參數值。 - 返回一個已經是拒絕狀態的 Promise,那麼
then返回的 Promise 也會成為拒絕狀態,並且將那個 Promise 的拒絕狀態的回調函數的參數值作為該被返回的Promise的拒絕狀態回調函數的參數值。 - 返回一個未定狀態(
pending)的 Promise,那麼then返回 Promise 的狀態也是未定的,並且它的終態與那個 Promise 的終態相同;同時,它變為終態時調用的回調函數參數與那個 Promise 變為終態時的回調函數的參數是相同的。
- 返回一個已經是接受狀態的 Promise,那麼
解決層層回調問題
//對應上面第一個node讀取文件的例子
function read(url) {
return new Promise((resolve, reject) => {
fs.readFile(url, 'utf8', (err, data) => {
if(err) reject(err);
resolve(data);
});
});
}
read(A).then(data => {
return read(B);
}).then(data => {
return read(C);
}).then(data => {
return read(D);
}).catch(reason => {
console.log(reason);
});
//對應第二個ajax請求例子
ajax(url)
.then(res => {
console.log(res)
return ajax(url1)
}).then(res => {
console.log(res)
return ajax(url2)
}).then(res => console.log(res))
可以看到,Promise在一定程度上其實改善了回調函數的書寫方式,最明顯的一點就是去除了橫向擴展,無論有再多的業務依賴,通過多個then(...)來獲取數據,讓代碼只在縱向進行擴展;另外一點就是邏輯性更明顯了,將異步業務提取成單個函數,整個流程可以看到是一步步向下執行的,依賴層級也很清晰,最後需要的數據是在整個代碼的最後一步獲得。
所以,Promise在一定程度上解決了回調函數的書寫結構問題,但回調函數依然在主流程上存在,只不過都放到了then(...)裏面,和我們大腦順序線性的思維邏輯還是有出入的。
catch方法
catch() 方法返回一個Promise,並且處理拒絕的情況。它的行為與調用Promise.prototype.then(undefined, onRejected) 相同。 (事實上, calling obj.catch(onRejected) 內部calls obj.then(undefined, onRejected)).
p.catch(onRejected);
p.catch(function(reason) {
// 拒絕
});
onRejected
當Promise 被rejected時,被調用的一個Function。 該函數擁有一個參數:
reason rejection 的原因。
如果 onRejected 拋出一個錯誤或返回一個本身失敗的 Promise , 通過 catch() 返回的Promise 被rejected;否則,它將顯示為成功(resolved)。
Promise缺點
- 無法取消 Promise
- 當處於pending狀態時,無法得知目前進展到哪一個階段
- 錯誤不能被 try catch(try..catch 結構,它只能是同步的,無法用於異步代碼模式)
執行f2(),無法通過try/catch捕獲promise.reject,控制枱拋出Uncaught (in promise)
function f2() {
try {
Promise.reject('出錯了');
} catch(e) {
console.log(e)
}
}
改成await/async後,執行f()就能在catch中捕獲到錯誤了,並不會拋出Uncaught (in promise)
async function f() {
try {
await Promise.reject('出錯了')
} catch(e) {
console.log(e)
}
}
可以這麼理解,promise中的錯誤發生在未來,所以無法現在捕獲
生成器Generators/ yield
什麼是Generator
Generator 函數是 ES6 提供的一種異步編程解決方案,語法行為與傳統函數完全不同
Generator 函數有多種理解角度。語法上,首先可以把它理解成,Generator 函數是一個狀態機,封裝了多個內部狀態。
執行 Generator 函數會返回一個遍歷器對象,也就是説,Generator 函數除了狀態機,還是一個遍歷器對象生成函數。返回的遍歷器對象,可以依次遍歷 Generator 函數內部的每一個狀態。形式上,Generator 函數是一個普通函數,但是有兩個特徵。
- 一是,function關鍵字與函數名之間有一個星號;
- 二是,函數體內部使用yield表達式,定義不同的內部狀態
Generator調用方式
Generator 函數的調用方法與普通函數一樣,也是在函數名後面加上一對圓括號。不同的是,調用 Generator 函數後,該函數並不執行,返回的也不是函數運行結果,而是一個指向內部狀態的指針對象,也就是上一章介紹的遍歷器對象(Iterator Object)。
下一步,必須調用遍歷器對象的next方法,使得指針移向下一個狀態。也就是説,每次調用next方法,內部指針就從函數頭部或上一次停下來的地方開始執行,直到遇到下一個yield表達式(或return語句)為止。換言之,Generator 函數是分段執行的,yield表達式是暫停執行的標記,而next方法可以恢復執行。
function* foo () {
var index = 0;
while (index < 2) {
yield index++; //暫停函數執行,並執行yield後的操作
}
}
var bar = foo(); // 返回的其實是一個迭代器
console.log(bar.next()); // { value: 0, done: false }
console.log(bar.next()); // { value: 1, done: false }
console.log(bar.next()); // { value: undefined, done: true }
瞭解Co
可以看到上個例子當中我們需要一步一步去調用next這樣也會很麻煩,這時我們可以引入co來幫我們控制
Co是一個為Node.js和瀏覽器打造的基於生成器的流程控制工具,藉助於Promise,你可以使用更加優雅的方式編寫非阻塞代碼。
Co 函數庫約定,yield 命令後面只能是 Thunk 函數或 Promise 對象,而 async 函數的 await 命令後面,可以跟 Promise 對象和原始類型的值(數值、字符串和布爾值,但這時等同於同步操作)。
説白了就是幫你自動執行你的Generator不用手動調用next
可以簡單實現下:
function co(it) {
return new Promise(function (resolve, reject) {
function step(d) {
let { value, done } = it.next(d);
if (!done) {
value.then(function (data) { // 2,txt
step(data)
}, reject)
} else {
resolve(value);
}
}
step();
});
}
比如我們有個生成器函數是r(),直接扔進co裏自動執行
function* r() {
let content1 = yield read('1.txt', 'utf8');
let content2 = yield read(content1, 'utf8');
return content2;
}
co(r()).then(function (data) {
console.log(data)
})
解決異步問題
我們可以通過 Generator 函數解決回調地獄的問題,可以把之前的回調地獄例子改寫為如下代碼:
const co = require('co');
co(
function* read() {
yield readFile(A, 'utf-8');
yield readFile(B, 'utf-8');
yield readFile(C, 'utf-8');
//....
}
).then(data => {
//code
}).catch(err => {
//code
});
function *fetch() {
yield ajax(url, () => {})
yield ajax(url1, () => {})
yield ajax(url2, () => {})
}
let it = fetch()
let result1 = it.next()
let result2 = it.next()
let result3 = it.next()
終極解決方案Async/ await
async 函數是Generator 函數的語法糖,是對Generator做了進一步的封裝。
async函數對 Generator 函數的改進
- 1.
內置執行器。Generator 函數的執行必須依靠執行器,而 async 函數自帶執行器,無需手動執行 next() 方法。 - 2.
更好的語義。async和await,比起星號和yield,語義更清楚了。async表示函數裏有異步操作,await表示緊跟在後面的表達式需要等待結果。 - 3.
更廣的適用性。co模塊約定,yield命令後面只能是 Thunk 函數或 Promise 對象,而async函數的await命令後面,可以是 Promise 對象和原始類型的值(數值、字符串和布爾值,但這時會自動轉成立即 resolved 的 Promise 對象)。 - 4.
返回值是 Promise。async 函數返回值是 Promise 對象,比 Generator 函數返回的 Iterator 對象方便,可以直接使用 then() 方法進行調用。
Co+Promise+Generator實現Async
async重點是自帶了執行器,相當於把我們要額外做的(寫執行器/依賴co模塊)都封裝了在內部。比如:
async function fn(args) {
// ...
}
等同於:
function fn(args) {
return spawn(function* () {
// ...
});
}
function spawn(genF) { //spawn函數就是自動執行器,跟簡單版的思路是一樣的,多了Promise和容錯處理
return new Promise(function(resolve, reject) {
const gen = genF();
function step(nextF) {
let next;
try {
next = nextF();
} catch(e) {
return reject(e);
}
if(next.done) {
return resolve(next.value);
}
// 沒有結束,遞歸調用step
Promise.resolve(next.value).then(function(v) {
step(function() { return gen.next(v); });
}, function(e) {
step(function() { return gen.throw(e); });
});
}
step(function() { return gen.next(undefined); });
});
}
這樣就不難理解為什麼await函數會等到後面跟promise在resolve後才會執行,因為可以看出整個代碼是在Generator裏執行,這樣在resolve裏調用了gen.next(v),才會往下走。
注意一個細節:await下一行的代碼相當註冊在await後面跟的promise的.then回調裏,這裏和事件循環有關後面舉例説明
Async特點
- 當調用一個 async 函數時,會返回一個 Promise 對象。
async function async1() {
return "1"
}
console.log(async1()) // -> Promise {<resolved>: "1"}
- 當這個 async 函數返回一個值時,Promise 的 resolve 方法會負責傳遞這個值;
- 沒有返回值Promise 的 resolve 方法專遞undefined
- 當 async 函數拋出異常時,Promise 的 reject 方法也會傳遞這個異常值。
- async 函數中可能會有 await 表達式,這會使 async 函數暫停執行,等待 Promise 的結果出來,然後恢復async函數的執行並返回解析(resolved)。
- 內置執行器。 Generator 函數的執行必須靠執行器,所以才有了 co 函數庫,而 async 函數自帶執行器。也就是説,async 函數的執行,與普通函數一模一樣,只要一行。
- 更廣的適用性。co 模塊約定,yield 命令後面只能是 Thunk 函數或 Promise對象。而 async 函數的 await 命令後面則可以是 Promise 或者 原始類型的值(Number,string,boolean,但這時等同於同步操作)
await特點
- await 操作符用於等待一個Promise 對象。它只能在異步函數 async function 中使用。
- [return_value] = await expression;
- await 表達式會暫停當前 async function 的執行,等待 Promise 處理完成。若 Promise 正常處理(fulfilled),其回調的resolve函數參數作為 await 表達式的值,繼續執行 async function。
- 另外,如果 await 操作符後的表達式的值不是一個 Promise,則返回該值本身。
- 若 Promise 處理異常(rejected),await 表達式會把 Promise 的異常原因拋出。可以用catch在外部捕獲
重點:遇到 await 表達式時,會讓 async 函數 暫停執行,等到 await 後面的語句(Promise)狀態發生改變(resolved或者rejected)之後,再恢復 async 函數的執行(再之後 await 下面的語句),並返回解析值(Promise的值)
async await 異常處理
let last;
async function throwError() {
await Promise.reject('error');//這裏就是異常
last = await '沒有執行';
}
throwError().then(success => console.log('成功', success,last))
.catch(error => console.log('失敗',error,last))
上面函數,執行的到await排除一個錯誤後,就停止往下執行,導致last沒有賦值報錯。
async裏如果有多個await函數的時候,如果其中任一一個拋出異常或者報錯了,都會導致函數停止執行,直接reject;
怎麼處理呢,可以用try/catch,遇到函數的時候,可以將錯誤拋出,並且繼續往下執行。
let last;
async function throwError() {
try{
await Promise.reject('error');
last = await '沒有執行';
}catch(error){
console.log('has Error stop');
}
}
throwError().then(success => console.log('成功', last))
.catch(error => console.log('失敗',last))
Async執行方式
簡單説 , async/awit 就是對上面gennerator自動化流程的封裝 , 讓每一個異步任務都是自動化的執行 , 當第一個異步任務readFile(A)執行完如上一點説明的, async內部自己執行next(),調用第二個任務readFile(B);
這裏引入ES6阮一峯老師的例子
const fs = require('fs');
const readFile = function (fileName) {
return new Promise(function (resolve, reject) {
fs.readFile(fileName, function(error, data) {
if (error) return reject(error);
resolve(data);
});
});
};
async function read() {
await readFile(A);//執行到這裏停止往下執行,等待readFile內部resolve(data)後,再往下執行
await readFile(B);
await readFile(C);
//code
}
//這裏可用於捕獲錯誤
read().then((data) => {
//code
}).catch(err => {
//code
});
注意await下面的代碼執行的時機
async function async1() {
console.log('async1 start')
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2')
}
async1();
new Promise((resolve) => {
console.log(1)
resolve()
}).then(() => {
console.log(2)
}).then(() => {
console.log(3)
}).then(() => {
console.log(4)
})
執行順序:
async1 start
async2
1
async1 end//註冊在async2()的.then裏,推遲了一個時序
2
3
4
新版V8中執行的時序等價於(激進優化後與老版不同):
function async1(){
console.log('async1 start');
const p = async2();
return Promise.resolve(p)
.then(() => {
console.log('async1 end')
});
}
function async2(){
console.log('async2');
return Promise.resolve();
}
async1();
new Promise((resolve) => {
console.log(1)
resolve()
}).then(() => {
console.log(2)
}).then(() => {
console.log(3)
}).then(() => {
console.log(4)
})
參考文章
- http://es6.ruanyifeng.com/
- https://developer.mozilla.org...
- https://juejin.im/post/5aa786...
- https://juejin.im/post/5b83cb...
- https://juejin.im/post/596e14...