在javascript單執行緒的世界裡,沒有非同步寸步難行。本章節介紹非同步程式設計的發展,從callback
,Events
到promise
,generator
,async/await
.
為什麼要使用非同步程式設計
在(javascript)單執行緒的世界裡,如果有多個任務,就必須排隊,前面一個完成,再繼續後面的任務。就像一個ATM排隊取錢似的,前面一個不取完,就不讓後面的取。 為了這個問題,javascript提供了2種方式: 同步和非同步。 非同步就像銀行取錢填了單子約了號,等著叫到號,再去做取錢,等待的時間裡還可以乾點其他的事兒~
非同步回撥帶來的問題
回撥地獄 (Callbacks Hell)
舉個例子:通過api拿到資料,資料裡面有圖片,圖片載入成功渲染,那麼程式碼如下:
// 虛擬碼
request(url, (data) => {
if(data){
loadImg(data.src, () => {
render();
})
}
})
複製程式碼
如果有在業務邏輯比較複雜或者NodeJS I/O操作比較頻繁的時候,就成了下面這個樣子:
doSth1((...args, callback) => {
doSth2((...args, callback) => {
doSth3((...args, callback) => {
doSth4((...args, callback) => {
doSth5((...args, callback) => {
})
})
})
})
})
複製程式碼
這樣的維護性
和可讀性
,整個人瞬間感覺不好了~
異常處理
try {
setTimeout(() => {
throw new Error('unexpected error');
}, 100);
} catch (e) {
console.log('error2', e.message);
}
複製程式碼
以上程式碼執行丟擲異常,但try catch不能得到未來時間段的異常。
流程控制不方便
流程控制只能通過維護各種狀態
來處理,不利於管理
非同步程式設計現有解決方案對比
事件機制
不管瀏覽器還是NodeJS,提供了大量內建事件API來處理非同步。
事件監聽
瀏覽器中如: websocket
, ajax
, canvas
, img
,FileReader
等
NodeJS如: stream
, http
等
自定義事件(本質上是一種釋出訂閱模式)
- NodeJS中的
EventEmitter
事件模型 - 瀏覽器中:如DOM可使用
addEventListener
,此外瀏覽器也提供一些自定義事件的API,但相容性不好,具體可以這篇文章;也可以用Node中的EventEmitter
;jquery
中也對此做了封裝,on
,bind
等方法支援自定義事件。
事件小結
事件一定程度上解決了解耦
和提升了程式碼可維護性
;對於異常處理
,只有部分支援類似error事件
才能處理。若想實現異常處理機制,只有自己模擬error事件,比較繁瑣。
Promise
Promise嚴格來說不是一種新技術,它只是一種機制,一種程式碼結構和流程,用於管理非同步回撥。為了統一規範產生一個Promise/A+規範,點選檢視Promise/A+中文版,cnode的William17
實現了Promise/A+規範,有興趣的可以點這裡檢視
promise
狀態由內部控制,外部不可變- 狀態只能從
pending
到resovled
,rejected
,一旦進行完成不可逆 - 每次
then/catch
操作返回一個promise例項,可以進行鏈式操作
readFile(path1).then(function (data) {
console.log(data.toString());
return readFile(path2);
}).then(function (data) {
console.log(data.toString());
return readFile(errorPath);
}).then(function (data) {
console.log(data.toString());
}).catch(function (e) {
console.log('error', e);
return readFile(path1);
}).then(function (data) {
console.log(data.toString());
});
複製程式碼
Promise的缺陷:
- 內部丟擲錯誤只能通過
promise.catch
才能才能接收到 - 語義不明確
Generator
generator介紹
Generator是ES6提供的方法,是生成器,最大的特點:可以暫停執行和恢復執行(可以交出函式的執行權),返回的是指標物件
.
- 需要自執行函式才能持續執行,否則需要手工呼叫流程
- 自執行函式借助promise, 獲取異常和最終結果
generator 和 promise自執行實現
const run = function (generator) {
var g = generator()
var perform = function (result) {
if (result.done === true) {
return result.value
}
if (isPromise(result.value)) {
return result.value.then(function (v) {
return perform(g.next(v))
}).catch(function (e) {
return perform(g.throw(e))
})
} else {
return perform(g.next(result.value))
}
}
return perform(g.next())
}
const isPromise = f => f.constructor === Promise
function* g() {
var a = yield sleep(1000, _ => 1)
var b = yield sleep(1000, _ => 2)
return a + b
}
function sleep(d, fn) {
return new Promise((resolve, reject) => {
setTimeout(_ => resolve(fn()), d)
})
}
複製程式碼
由於以上問題,於是一個叫 co庫誕生,支援thunk
和Promise
.
關於thunk可以檢視阮一峰老師的thunk介紹和應用
Async/Await
ES7提供了async
函式,使得非同步操作變得更加方便。
- 內建執行器
- 更好的語義
- 更多的實用場景,co引數Generator函式中
yield
只能是promise
和thunk
例項程式碼:
async function asyncReadFile() {
var p1 = readFile(path.join(__dirname, '../data/file1.txt'));
var p2 = readFile(path.join(__dirname, '../data/file2.txt'));
var [f1, f2] = await Promise.all([p1, p2]);
return `${f1.toString()}\n${f2.toString()}`;
}
(async function () {
try {
console.log(await asyncReadFile());
} catch (e) {
console.log(e.message)
}
})();
複製程式碼
Node8 async/await 支援
Node8.0
釋出,全面支援 async/await
, 推薦使用 async/await
, 低版本node可以使用 babel
來編譯處理。 而 為了方便 介面設計時 返回 promise
更方面使用者. 當然依然使用 callback
, 通過 promisify
做轉換, Node8.0
已經內建 util.promisify
方法。
參考資料
小結
非同步程式設計
在javascript中扮演者重要的角色,雖然現在需要通過babel
,typescript
等編譯或轉換程式碼,跟著規範
和標準
走,就沒有跑偏。
好久之前在github部落格上的文章了。
如需轉載,請備註出處。