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



























