摘要:在誕生以前,異步編程的方式大概有下面四種回調函數事件監聽發布訂閱對象將異步編程帶入了一個全新的階段,中的函數更是給出了異步編程的終極解決方案。這意味著,出錯的代碼與處理錯誤的代碼,實現了時間和空間上的分離,這對于異步編程無疑是很重要的。
寫在前面
有一個有趣的問題:
為什么Node.js約定回調函數的第一個參數必須是錯誤對象err(如果沒有錯誤,該參數就是null)?
原因是執行回調函數對應的異步操作,它的執行分成兩段,這兩段之間拋出的錯誤程序無法捕獲,所以只能作為參數傳入第二段。大家知道,JavaScript只有一個線程,如果沒有異步編輯,復雜的程序基本沒法使用。在ES6誕生以前,異步編程的方式大概有下面四種:
回調函數
事件監聽
發布/訂閱
Promise對象
ES6將JavaScript異步編程帶入了一個全新的階段,ES7中的async函數更是給出了異步編程的終極解決方案。下面將具體講解異步編程的原理和值得注意的地方,待我細細道來~
異步編程的演變 基本理解所謂異步,簡單地說就是一個任務分成兩段,先執行第一段,然后轉而執行其他任務,等做好準備再回過頭執行第二段。
舉個例子
讀取一個文件進行處理,任務的第一段是向操作系統發出請求,要求讀取文件。然后,程序執行其他任務,等到操作系統返回文件,再接著執行任務的第二段(處理文件)。這種不連續的執行,就叫做異步。
相應地,連續的執行就叫作同步。由于是連續執行,不能插入其他任務,所以操作系統從硬盤讀取文件的這段時間,程序只能干等著。
回調函數所謂回調函數,就是把任務的第二段多帶帶寫在一個函數中,等到重新執行該任務時直接調用這個函數。其英文名字 callback 直譯過來就是 "重新調用"的意思。
拿上面的例子講,讀取文件操作是這樣的:
fs.readFile(fileA, (err, data) => { if (err) throw err; console.log(data) }) fs.readFile(fileB, (err, data) => { if (err) throw err; console.log(data) })
注意:上面兩段代碼彼此是異步的,雖然開始執行的順序是從上到下,但是第二段并不會等到第一段結束才執行,而是并發執行。
那么問題來了,如果想fileB等到fileA讀取成功后再開始執行應該怎么處理呢?最簡單的辦法是通過 回調嵌套:
fs.readFile(fileA, (err, data) => { if (err) throw err; console.log(data) fs.readFile(fileB, (_err, _data) => { if (_err) throw err; console.log(_data) }) })
這種方式我只能容忍個位數字的嵌套,而且它使得代碼橫向發展,實在是丑的一筆,次數多了根本是沒法看。試想萬一要同步執行100個異步操作呢?瘋掉算了吧!有沒有更好的辦法呢?
使用Promise要澄清一點,Promise的概念并不是ES6新出的,而是ES6整合了一套新的寫法。同樣繼續上面的例子,使用Promise代碼就變成這樣了:
var readFile = require("fs-readfile-promise"); readFile(fileA) .then((data)=>{console.log(data)}) .then(()=>{return readFile(fileB)}) .then((data)=>{console.log(data)}) // ... 讀取n次 .catch((err)=>{console.log(err)})
注意:上面代碼使用了Node封裝好的Promise版本的readFile函數,它的原理其實就是返回一個Promise對象,咱也簡單地寫一個:
var fs = require("fs"); var readFile = function(path) { return new Promise((resolve, reject) => { fs.readFile(path, (err, data) => { if (err) reject(err) resolve(data) }) }) } module.export = readFile
但是,Promise的寫法只是回調函數的改進,使用then()之后,異步任務的兩段執行看得更清楚,除此之外并無新意。撇開優點,Promise的最大問題就是代碼冗余,原來的任務被Promise包裝一下,不管什么操作,一眼看上去都是一堆then(),原本的語意變得很不清楚。
把酒問蒼天,MD還有更好的辦法嗎?
使用Generator在引入generator之前,先介紹一下什么叫 協程
協程"攜程在手,說走就走"。哈哈,別混淆了, "協程" 非 "攜程"
所謂 "協程" ,就是多個線程相互協作,完成異步任務。協程有點像函數,又有點像線程。其運行流程大致如下:
第一步: 協程A開始執行
第二步:協程A執行到一半,暫停,執行權轉移到協程B
第三步:一段時間后,協程B交還執行權
第四步:協程A恢復執行
function asyncJob() { // ... 其他代碼 var f = yield readFile(fileA); // ... 其他代碼 }
上面的asyncJob()就是一個協程,它的奧妙就在于其中的yield命令。它表示執行到此處執行權交給其他協程,換而言之,yield就是異步兩個階段的分界線。
協程遇到yield命令就暫停,等到執行權返回,再從暫停的地方繼續往后執行。它的最大優點就是代碼的寫法非常像同步操作,如果除去 yield命令,簡直一模一樣。
Generator函數Generator函數是協程在ES6中的實現,最大的特點就是可以交出函數的執行權(即暫停執行)。整個Generator函數就是一個封裝的異步任務,或者說就是異步任務的容器。
function* gen(x) { var y = yield x + 2; return y; } var g = gen(1); g.next() // { value: 3, done: false } g.next() // { value: undefined, done: true }
上面的代碼中,調用Generator函數,會返回一個內部指針(即遍歷器)g,這是Generator函數不同于普通函數的另一個地方,即執行它不會返回結果,返回的是指針對象。調用指針g的next()方法,會移動內部指針(即執行異步任務的第一段),指向第一個遇到的yield語句。
換而言之,next()方法的作用是分階段執行Generator函數。每次調用next()方法,會返回一個對象,表示當前階段的信息(value屬性和done屬性)。value屬性是yield語句后面表達式的值,表示當前階段的值;done屬性是一個布爾值,表示Generator函數是否執行完畢,即是否還有一個階段。
Generator函數的數據交換和錯誤處理Generator函數可以暫停執行和恢復執行,這是它封裝異步任務的根本原因。除此之外,它還有兩個特性,使它可以作為異步編程的解決方案:函數體內外的數據交換和錯誤處理機制。
next()方法返回值的value屬性,是Generator函數向外輸出的數據;next()方法還可以接受參數,向Generator函數體內輸入數據。
function* gen(x) { var y = yield x + 2; return y; } var g = gen(1); g.next() // { value: 3, done: false } g.next(2) // { value: 2, done: true }
上面的代碼中,第一個next()方法的value屬性,返回表達式x+2的值(3)。第二個next()方法帶有參數2,這個參數可以傳入Generator函數,作為上個階段異步任務的返回結果,被函數體內的變量y接收,因此這一步的value屬性返回的就是2(變量y的值)。
Generator函數內部還可以部署錯誤處理代碼,捕獲函數體外拋出的錯誤。
function* gen(x) { try { var y = yield x + 2 } catch(e) { console.log(e) } return y } var g = gen(1); g.next(); g.throw("出錯了");
上面代碼的最后一行,Generator函數體外,使用指針對象的throw方法拋出的錯誤,可以被函數體內的try...catch 代碼塊捕獲。這意味著,出錯的代碼與處理錯誤的代碼,實現了時間和空間上的分離,這對于異步編程無疑是很重要的。
異步任務的封裝下面看看如何使用Generator函數,執行一個真實的異步任務。
var fetch = require("node-fetch") function* gen() { var url = "https://api.github.com/usrs/github"; var result = yield fetch(url); console.log(result.bio); }
上面代碼中,Generator函數封裝了一個異步操作,該操作先讀取一個遠程接口,然后從JSON格式的數據解析信息。就像前面說過的,這段代碼非常像同步操作。除了加上yield命令。
執行這段代碼的方法如下:
var g = gen(); var result = g.next(); result.value.then(function(data) { return data.json() }).then(function(data) { g.next(data) });
上面代碼中,首先執行Generator函數,獲取遍歷器對象。然后使用next()方法,執行異步任務的第一階段。由于Fetch模塊返回的是一個Promise對象,因此需要用then()方法調用下一個next()方法。
可以看到,雖然Generator函數將異步操作表示得很簡潔,但是流程管理卻不方便(即合適執行第一階段,何時執行第二階段)
大Boss登場之 async函數所謂async函數,其實是Generator函數的語法糖。
繼續我們異步讀取文件的例子,使用Generator實現
var fs = require("fs"); var readFile = (path) => { return new Promise((resolve, reject) => { fs.readFile(path, (err, data) => { if (err) reject(err) resolve(data) }) }) } var gen = function* () { var f1 = yield readFile(fileA); var f2 = yield readFile(fileB); console.log(f1.toString()); console.log(f2.toString()); }
寫成async函數,就是下面這樣:
var asyncReadFile = async function() { var f1 = await readFile(fileA); var f2 = await readFile(fileB); console.log(f1.toString()) console.log(f2.toString()) }
發現了吧,async函數就是將Generator函數的*替換成了async,將yield替換成await,除此之外,還對 Generator做了以下四點改進:
(1)內置執行器。Generator函數的執行比如靠執行器,所以才有了co模塊等異步執行器,而async函數是自帶執行器的。也就是說:async函數的執行,與普通函數一模一樣,只要一行:
var result = asyncReadFile();
(2)上面的代碼調用了asyncReadFile(),就會自動執行,輸出最后結果。這完全不像Generator函數,需要調用next()方法,或者使用co模塊,才能得到真正執行,從而得到最終結果。
(3)更好的語義。async和await比起星號和yield,語義更清楚。async表示函數里有異步操作,await表示緊跟在后面的表達式需要等待結果。
(4)更廣的適用性。async函數的await命令后面可以是Promise對象和原始類型的值(數值、字符串和布爾值,而這是等同于同步操作)。
(5)返回值是Promise,這比Generator函數返回的是Iterator對象方便多了。你可以用then()指定下一步操作。
實現原理進一步說,async函數完全可以看作由多個異步操作包裝成的一個Promise對象,而await命令就是內部then()命令的語法糖。
async函數的實現就是將Generator函數和自動執行器包裝在一個函數中。如下代碼:
async function fn(args) { // ... } // 等同于 function fn(args) { return spawn(function*() { // ... }) } // 自動執行器 function spawn(genF) { return new Promise(function(resolve, reject) { var gen = genF(); function step(nextF) { try { var next = nextF() } catch(e) { return reject(e) } if (next.done) { return resolve(next.value) } 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) }) }) }async函數用法
(1)async函數返回一個Promise對象,可以是then()方法添加回調函數。
(2)當函數執行時,一旦遇到await()就會先返回,等到觸發的異步操作完成,再接著執行函數體內后面的語句。
下面是一個延遲輸出結果的例子:
function timeout(ms) { return new Promise((resolve) => { setTimeout(resolve, ms) }) } async function asyncPrint(value, ms) { await timeout(ms) console.log(value) } // 延遲500ms后輸出 "Hello World!" asyncPrint("Hello World!", 500)注意事項
(1)await命令后面的Promise對象,運行結果可能是reject,所以最好把await命令放在try...catch代碼塊中。
(2)await命令只能用在async函數中,用在普通函數中會報錯。
(3)ES6將await增加為保留字。如果使用這個詞作為標識符,在ES5中是合法的,但是ES6會拋出 SyntaxError(語法錯誤)。
終極一戰"倚天不出誰與爭鋒",上面介紹了一大堆,最后還是讓我們通過一個例子來看看 async 函數和Promise、Generator到底誰才是真正的老大吧!
用Promise實現需求:假定某個DOM元素上部署了一系列的動畫,前一個動畫結束才能開始后一個。如果當中又一個動畫出錯就不再往下執行,返回上一個成功執行動畫的返回值。
function chainAnimationsPromise(ele, animations) { // 變量ret用來保存上一個動畫的返回值 var ret = null; // 新建一個空的Promise var p = Promise.resolve(); // 使用then方法添加所有動畫 for (var anim in animations) { p = p.then(function(val) { ret = val; return anim(ele); }) } // 返回一個部署了錯誤捕獲機制的Promise return p.catch(function(e) { /* 忽略錯誤,繼續執行 */ }).then(function() { return ret; }) }
雖然Promise的寫法比起回調函數的寫法有很大的改進,但是操作本身的語義卻變得不太明朗。
用Generator實現function chainAnimationsGenerator(ele, animations) { return spawn(function*() { var ret = null; try { for(var anim of animations) { ret = yield anim(ele) } } catch(e) { /* 忽略錯誤,繼續執行 */ } return ret; }) }
使用Generator雖然語義比Promise寫法清晰不少,但是用戶定義的操作全部出現在spawn函數的內部。這個寫法的問題在于,必須有一個任務運行器自動執行Generator函數,它返回一個Promise對象,而且保證yield語句后的表達式返回的是一個Promise。上面的spawn就扮演了這一角色。它的實現如下:
function spawn(genF) { return new Promise(function(resolve, reject) { var gen = genF(); function step(nextF) { try { var next = nextF() } catch(e) { return reject(e) } if (next.done) { return resolve(next.value) } 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) }) }) }使用async實現
async function chainAnimationAsync(ele, animations) { var ret = null; try { for(var anim of animations) { ret = await anim(ele) } } catch(e) { /* 忽略錯誤,繼續執行 */ } return ret; }
好了,光從代碼量上就看出優勢了吧!簡潔又符合語義,幾乎沒有不相關代碼。完勝!
參考注意一點:async屬于ES7的提案,使用時請通過babel或者regenerator進行轉碼。
阮一峰 《ES6標準入門》
@歡迎關注我的 github 和 個人博客 -Jafeney
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/80070.html
摘要:對于而言,異步編程我們可以采用回調函數,事件監聽,發布訂閱等方案,在之后,又新添了,,的方案。總結本文闡述了從回調函數到的演變歷史。參考文檔深入掌握異步編程系列理解的 對于JS而言,異步編程我們可以采用回調函數,事件監聽,發布訂閱等方案,在ES6之后,又新添了Promise,Genertor,Async/Await的方案。本文將闡述從回調函數到Async/Await的演變歷史,以及它們...
摘要:的翻譯文檔由的維護很多人說,阮老師已經有一本關于的書了入門,覺得看看這本書就足夠了。前端的異步解決方案之和異步編程模式在前端開發過程中,顯得越來越重要。為了讓編程更美好,我們就需要引入來降低異步編程的復雜性。 JavaScript Promise 迷你書(中文版) 超詳細介紹promise的gitbook,看完再不會promise...... 本書的目的是以目前還在制定中的ECMASc...
摘要:簡介指的是兩個關鍵字,是引入的新標準,關鍵字用于聲明函數,關鍵字用來等待異步必須是操作,說白了就是的語法糖。最后希望大家在讀過異步發展流程這個系列之后,對異步已經有了較深的認識,并可以在不同情況下游刃有余的使用這些處理異步的編程手段。 showImg(https://segmentfault.com/img/remote/1460000018998406?w=1024&h=379); ...
摘要:三即生成器,它是生成器函數返回的一個對象,是中提供的一種異步編程解決方案而生成器函數有兩個特征,一是函數名前帶星號,二是內部執行語句前有關鍵字調用一個生成器函數并不會馬上執行它里面的語句,而是返回一個這個生成器的迭代器對象。 文章來自微信公眾號:前端工坊(fe_workshop),不定期更新有趣、好玩的前端相關原創技術文章。 如果喜歡,請關注公眾號:前端工坊版權歸微信公眾號所有,轉載請...
閱讀 1829·2021-09-22 15:55
閱讀 3521·2021-09-07 10:26
閱讀 628·2019-08-30 15:54
閱讀 684·2019-08-29 16:34
閱讀 839·2019-08-26 14:04
閱讀 3258·2019-08-26 11:47
閱讀 2134·2019-08-26 11:33
閱讀 2294·2019-08-23 15:17