Javascript的異步編程知多少?
1寫在前面
Generator執行后返回什么?
Async/await的方式比Promise和Generatir好在哪里?
2同步和異步
同步:就是在執行某段代碼時,在該代碼沒有得到返回結果前,其它代碼是阻塞的無法執行,但是一旦執行完成拿到返回值后,就可以執行其它代碼了。
異步:就是當某段代碼執行異步過程調用發出后,這段代碼不會立即得到返回結果,而是掛起在后臺執行。在異步調用發出后,一般通過回調函數處理這個調用后才能拿到結果。
前面知道Javascript是單線程的,如果JS都是同步代碼執行可能會造成阻塞。如果使用就不會造成阻塞,就不需要等待異步代碼執行的返回結果,可以繼續執行該異步任務之后的代碼邏輯。
那么JS異步編程的實現方式是如何發展的呢?
早些年為了實現JS的異步編程,一般采用回調函數的方式,如:比較典型的事件回調,但是使用回調函數來實現存在一個很常見的問題,就是回調地獄。看下面的代碼像不像俄羅斯套娃。
- fs.readFile(a,"utf-8",(err,data)=>{
- fs.readFile(b,"utf-8",(err,data)=>{
- fs.readFile(c,"utf-8",(err,data)=>{
- fs.readFile(d,"utf-8",(err,data)=>{
- ....
- })
- })
- })
- })
常見的異步編程的場景有:
- ajax請求的回調
- 定時器中的回調
- 事件回調
- Node.js中的一些方法回調
異步回調如果層級很少,可讀性和代碼的維護性暫時還是可以接受的,但是當層級變多后就會陷入回調地獄。
3Promise
為了解決回調地獄的問題,社區提出了Promise的解決方案,ES6又將其寫入語言標準,采用Promise的實現方式在一定程度上解決了回調地獄的問題。
Promise簡單理解就是一個容器,里面保存了某個未來才會結束的事件的結果。從語法而言,Promise是一個可以獲取異步操作消息的對象。Promise具有三個狀態:
- 待定狀態pending:初始狀態,既沒有被完成,也沒有被拒絕
- 已完成fulfilled:操作成功完成
- 已拒絕rejected:操作失敗
關于Promise的狀態切換,如果想深入研究,可以學習『有限狀態機』知識點。
待定狀態的Promise對象執行的話,最后要么通過一個值完成,要么就是通過一個原因拒絕。當待定狀態改成為完成或拒絕狀態時,我們可以使用Promise.then的形式進行鏈式調用。因為最后Promise.prototype.then和Promise.prototype.catch方法返回的是一個Promise,所以它們可以繼續被鏈式調用。
Promise是如何結局回調地獄問題的?
- 解決多層嵌套問題
- 每種任務的處理結果存在兩種可能性(成功或失敗),那么需要在每種任務執行結束后分別處理這兩種可能性
Promise主要利用三大技術來解決回調地獄:回調函數延遲綁定、返回值穿透、錯誤冒泡
Promise.all
Promise.all(iterable)可以傳遞一個可迭代對象作為參數,此方法對于匯總多個Promise的結果很有用,在es6中可以將多個Promise.all異步請求并行操作。當所有結果成功返回時按照順序返回成功,當其中一個方法失敗則進入失敗方法。
- Promise.all(iterable);
使用Promise.all解決上面的異步編程問題。
- function read(url){
- return new Promise((resolve,reject)=>{
- fs.readFile(url,"utf-8",(err,data)=>{
- if(err) return 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);
- })
我們看到上面使用Promise的使用對回調地獄的解決有所提升,但是依舊不是很好維護,對此有了新的方法。
- function read(url){
- return new Promise((resolve,reject)=>{
- fs.readFile(url,"utf-8",(err,data)=>{
- if(err) return err;
- resolve(data);
- })
- })
- }
- //通過Promise.all可以實現多個異步并行執行,同一時刻獲取最終解決的問題
- Promise.all([read(A),read(B),read(C)]).(data=>{
- console.log(data)
- }).catch(reason=>{
- console.log(reason);
- })
Promise.allSettled
Promise.allSettled的語法和Promise.all類似,都是接受一個可迭代對象作為參數,返回一個新的Promise。當Promise.allSettled全部處理完畢后,我們可以拿到每個Promise的狀態,而不管其是否處理成功。
- Promise.allSettled(iterable);
Promise.any
Promise.any也是接收一個可迭代對象作為參數,any方法返回一個Promise。只要參數Promise實例有一個變成fulfilled狀態,最后any返回的實例就會變成fullfiled狀態;如果所有參數Promise實例都變成rejected狀態,最后any返回的實例就會變成rejected狀態。
Promise.race
Promise.race接收一個可迭代對象作為參數,race方法返回一個Promise,只要參數之中有一個實例率先改變狀態,則race方法的返回狀態就跟著改變。
Promise方法 | 作用 |
---|---|
all | 參數所有返回結果都為成功才返回 |
allSettled | 參數無論返回結果是否成功,都返回每個參數執行狀態 |
any | 參數中只要有一個成功,就返回該成功的執行結果 |
race | 返回最先執行成功的參數的執行結果 |
4Generator
Generator生成器是es6的新關鍵詞,Generator是一個帶星號的函數,可以配合yield關鍵字來暫停或執行函數。
Generator最大的特點就是可以交出函數的執行權,Generator函數可以看作是異步任務的容器,需要暫停的地方使用yield語法進行標注。
- function* gen(){
- let a = yield 111;
- console.log(a);
- let b = yield 222;
- console.log(b);
- let c = yield 333;
- console.log(c);
- let d = yield 444;
- console.log(d);
- }
- let t = gen();
- t.next(1);//第一調用next函數時,傳遞的參數無效,因此無法打印結果
- t.next(2);//2
- t.next(3);//3
- t.next(4);//4
- t.next(5);//5
上面代碼中,調用gen()后程序會被阻塞住,不會執行任何語句;而調用g.next()后程序會繼續執行,直到遇到yield關鍵詞時執行暫停;一直執行next方法,最后返回一個對象,其存在兩個屬性:value和done。
yield也是es6的關鍵詞,配合Generator執行以及暫停,yield關鍵詞最后返回一個迭代器對象,該對象有value和done兩個屬性,value表示返回的值,done便是當前是否完成。
- function* gen(){
- yield 1;
- yield* gen2();
- yield 4;
- }
- function* gen2(){
- yield 2;
- yield 3;
- }
- const g = gen();
- console.log(g.next());
- console.log(g.next());
- console.log(g.next());
- console.log(g.next());
運行結果:
那么,Generator和異步編程有著什么聯系呢?澤呢么才能將Generator函數按照順序一次執行完畢呢?
thunk函數
thunk函數的基本思路就是接收一定的參數,會產生觸定制化的函數,最后使用定制化的函數去完成想要實現的功能。
- const isType = type => {
- return obj => {
- return Object.prototype.toString.call(obj) === `[object ${type}]`;
- }
- }
- const isString = isType("string");
- const isArray = isType("Array");
- isString("yichuan");//true
- isArray(["red","green","blue"]);//true
- const readFileThunk = filename=>{
- return callback=>{
- fs.readFile(filename,callback);
- }
- }
- const gen = function* (){
- const data1 = yield readFileThunk("a.txt");
- console.log(data1.toString());
- const data2 = yield readFileThunk("b.txt");
- console.log(data2.toString());
- }
- const g = gen();
- g.next().value((err,data1)=>{
- g.next(data1).value((err,data2)=>{
- g.next(data2);
- })
- })
我們可以看到上面的代碼還是像俄羅斯套娃,理解費勁,我們進行優化以下:
- function fun(get){
- const next = (err,data)=>{
- const res = gen.next(data);
- if(res.done) return;
- res.value(next);
- }
- next();
- }
- run(g);
co函數庫是用于處理Generator函數的自動執行,核心原理是前面講到的通過和thunk函數以及Promise對象進行配合,包裝成一個庫。
Generator函數就是一個異步操作的容器,co函數接收Generator函數作為參數,并最后返回一個Promise對象。在返回的Promise對象中,co先檢查參數gen是否為Generator函數。如果是就執行函數,如果不是就直接返回,并將Promise對象的狀態改為resolved。co將Generator函數的內部指針對象的next方法包裝成onFulfilled函數,主要是為了能夠捕獲到拋出的錯誤。關鍵在于next,他會反復調用自身。
- const co = require("co");
- const g = gen();
- co(g).then(res=>{
- console.log(res);
- })
5Async/await
JS異步編程從最開始的回調函數的方式演化到使用Promise對象,再到Generator+co函數的方式,每次都有一些改變但是都不徹底。async/await被稱為JS中異步終極解決方案,既能夠像Generator+co函數一樣用同步方式阿里寫異步代碼,又能夠得到底層的語法支持,無需借助任何第三方庫。
async是Generator函數的語法糖,async/await的優點是代碼清晰,可以處理回調的問題。
- function testWait(){
- return new Promise((resolve,reject)=>{
- setTimeout(()=>{
- console.log("testWait");
- resolve();
- },1000);
- })
- }
- async function testAwaitUse(){
- await testWait();
- console.log("hello");
- return "yichuan";
- }
- //輸出順序依次是:testWait hello yichuan
- console.log(testAwaitUse());
6異步編程方式小結
JS異步編程方式 | 簡單總結 |
---|---|
回調函數 | 最拉胯的異步編程方式 |
Promise | es6新增語法,解決回調地獄問題 |
Generator | 和yield配合使用,返回的是迭代器 |
async/await | 二者配合使用,async返回的是Promise對象,await控制執行順序 |
7參考文章
《Javascript核心原理精講》
《Javascript高級程序設計》
《你不知道的Javascrtipt》
《JS 異步編程六種方案》
8寫在最后
本文主要介紹了Javascript的最重要的知識點之一,也是之后開發工作中經常要接觸的概念,常用的異步編程方式有:回調函數、Promise、Generator和async/await。頻繁使用回調函數會造成回調地獄,Promise的出現就是解決回調地獄的,但是Promise的鏈式函數也有長,對于出現了async/await的終極解決方案。