JavaScript異步與Promise實現(xiàn)
【引自熊建剛的博客】前言
如果你已經(jīng)對JavaScript異步有一定了解,或者已經(jīng)閱讀過本系列的其他兩篇文章,那請繼續(xù)閱讀下一小節(jié),若你還有疑惑或者想了解JavaScript異步機制與編程,可以閱讀一遍這兩篇文章:
回調(diào)函數(shù)
回調(diào)函數(shù),作為JavaScript異步編程的基本單元,非常常見,你肯定對下面這類代碼一點都不陌生:
- component.do('purchase', funcA);
- function funcA(args, callback) {
- //...
- setTimeout(function() {
- $.ajax(url, function(res) {
- if (res) {
- callback(res)
- } else {//...}
- });
- }, 300);
- funcB();
- setTimeout(function() {
- $.ajax(arg, function(res) {
- if (res) {
- callback(res);
- }
- });
- }, 400);
- }
上面這些代碼,一層一層,嵌套在一起,這種代碼通常稱為回調(diào)地獄,無論是可讀性,還是代碼順序,或者回調(diào)是否可信任,亦或是異常處理角度看,都是不盡人意的,下面做簡單闡述。
順序性
上文例子中代碼funcB函數(shù),還有兩個定時器回調(diào)函數(shù),回調(diào)內(nèi)各自又有一個ajax異步請求然后在請求回調(diào)里面執(zhí)行最外層傳入的回調(diào)函數(shù),對于這類代碼,你是否能明確指出個回調(diào)的執(zhí)行順序呢?如果funcB函數(shù)內(nèi)還有異步任務呢?,情況又如何?
假如某一天,比如幾個月后,線上出了問題,我們需要跟蹤異步流,找出問題所在,而跟蹤這類異步流,不僅需要理清個異步任務執(zhí)行順序,還需要在眾多回調(diào)函數(shù)中不斷地跳躍,調(diào)試(或許你還能記得諸如funcB這些函數(shù)的作用和實現(xiàn)),無論是出于效率,可讀性,還是出于人性化,都不希望開開發(fā)者們再經(jīng)歷這種痛苦。
信任問題
如上,我們調(diào)用了一個第三方支付組件的支付API,進行購買支付,正常情況發(fā)現(xiàn)一切運行良好,但是假如某一天,第三方組件出問題了,可能多次調(diào)用傳入的回調(diào),也可能傳回錯誤的數(shù)據(jù)。說到底,這樣的回調(diào)嵌套,控制權在第三方,對于回調(diào)函數(shù)的調(diào)用方式、時間、次數(shù)、順序,回調(diào)函數(shù)參數(shù),還有下一節(jié)將要介紹的異常和錯誤都是不可控的,因為無論如何,并不總能保證第三方是可信任的。
錯誤處理
關于JavaScript錯誤異常,初中級開發(fā)接觸的可能并不多,但是其實還是有很多可以學習實踐的地方,如前端異常監(jiān)控系統(tǒng)的設計,開發(fā)和部署,并不是三言兩語能闡述的,之后會繼續(xù)推出相關文章。
錯誤堆棧
我們知道當JavaScript拋出錯誤或異常時,對于未捕獲異常,瀏覽器會默認在控制臺輸出錯誤堆棧信息,如下,當test未定義時:
- function init(name) {
- test(name)
- }
- init('jh');
輸出如圖:
如圖中自頂向下輸出紅色異常堆棧信息,Uncaught表示該異常未捕獲,ReferenceError表明該異常類型為引用異常,冒號后是異常的詳細信息:test is not defined,test未定義;后面以at起始的行就是該異常發(fā)生處的調(diào)用堆棧。第一行說明異常發(fā)生在init函數(shù),第二行說明init函數(shù)的調(diào)用環(huán)境,此處在控制臺直接調(diào)用,即相當于在匿名函數(shù)環(huán)境內(nèi)調(diào)用。
異步錯誤堆棧
上面例子是同步代碼執(zhí)行的異常,當異常發(fā)生在異步任務內(nèi)時,又會如何呢?,假如把上例中代碼放在一個setTimeout定時器內(nèi)執(zhí)行:
- function init(name) {
- test(name)
- }
- setTimeout(function A() {
- setTimeout(function() {
- init();
- }, 0);
- }, 0);
如圖:
可以看到,異步任務中的未捕獲異常,也會在控制臺輸出,但是setTimeout異步任務回調(diào)函數(shù)沒有出現(xiàn)在異常堆棧,為什么呢?這是因為當init函數(shù)執(zhí)行時,setTimeout的異步回調(diào)函數(shù)不在執(zhí)行棧內(nèi),而是通過事件隊列調(diào)用。
JavaScript錯誤處理
JavaScript的異常捕獲,主要有兩種方式:
- try{}catch(e){}主動捕獲異常;
如上,對于同步執(zhí)行大代碼出現(xiàn)異常,try{}catch(e){}是可以捕獲的,那么異步錯誤呢?
如上圖,我們發(fā)現(xiàn),異步回調(diào)中的異常無法被主動捕獲,由瀏覽器默認處理,輸出錯誤信息。
window.onerror事件處理器,所有未捕獲異常都會自動進入此事件回調(diào)
如上圖,輸出了script error錯誤信息,同時,你也許注意到了,控制臺依然打印出了錯誤堆棧信 息,或許你不希望用戶看到這么醒目的錯誤提醒,那么可以使window.onerror的回調(diào)返回true即可阻止瀏覽器的默認錯誤處理行為:
當然,一般不隨意設置window.onerror回調(diào),因為程序通??赡苄枰渴鹎岸水惓1O(jiān)控系統(tǒng),而通常就是使用window.onerror處理器實現(xiàn)全局異常監(jiān)控,而該事件處理器只能注冊一個回調(diào)。
回調(diào)與Promise
以上我們談到的諸多關于回調(diào)的不足,都很常見,所以必須是需要解決的,而Promise正是一種很好的解決這些問題的方式,當然,現(xiàn)在已經(jīng)提出了比Promise更先進的異步任務處理方式,但是目前更大范圍使用,兼容性更好的方式還是Promise,也是本篇要介紹的,之后會繼續(xù)介紹其他處理方式。
Promises/A+
分析了一大波問題后,我們知道Promise的目標是異步管理,那么Promise到底是什么呢?
- 異步,表示在將來某一時刻執(zhí)行,那么Promise也必須可以表示一個將來值;
- 異步任務,可能成功也可能失敗,則Promise需要能完成事件,標記其狀態(tài)值(這個過程即決議-resolve,下文將詳細介紹);
- 可能存在多重異步任務,即異步任務回調(diào)中有異步任務,所以Promise還需要支持可重復使用,添加異步任務(表現(xiàn)為順序鏈式調(diào)用,注冊異步任務,這些異步任務將按注冊的順序執(zhí)行)。
所以,Promise是一種封裝未來值的易于復用的異步任務管理機制。
為了更好的理解Promise,我們介紹一下Promises/A+,一個公開的可操作的Promises實現(xiàn)標準。先介紹標準規(guī)范,再去分析具體實現(xiàn),更有益于理解。
Promise代表一個異步計算的最終結果。使用promise最基礎的方式是使用它的then方法,該方法會注冊兩個回調(diào)函數(shù),一個接收promise完成的最終值,一個接收promise被拒絕的原因。
Promises/A
你可能還會想問Promises/A是什么,和Promises/A+有什么區(qū)別。Promises/A+在Promises/A議案的基礎上,更清晰闡述了一些準則,拓展覆蓋了一些事實上的行為規(guī)范,同時刪除了一些不足或者有問題的部分。
Promises/A+規(guī)范目前只關注如何提供一個可操作的then方法,而關于如何創(chuàng)建,決議promises是日后的工作。
術語
- promise: 指一個擁有符合規(guī)范的then方法的對象;
- thenable: 指一個定義了then方法的對象;
- 決議(resolve): 改變一個promise等待狀態(tài)至已完成或被拒絕狀態(tài), 一旦決議,不再可變;
- 值(value): 一個任意合法的JavaScript值,包括undefined,thenable對象,promise對象;
- exception/error: JavaScript引擎拋出的異常/錯誤
- 拒絕原因(reject reason): 一個promise被拒絕的原因
Promise狀態(tài)
一個promise只可能處于三種狀態(tài)之一:
- 等待(pending):初始狀態(tài);
- 已完成(fulfilled):操作成功完成;
- 被拒絕(rejected):操作失敗;
這三個狀態(tài)變更關系需滿足以下三個條件:
- 處于等待(pending)狀態(tài)時,可以轉變?yōu)橐淹瓿?fulfilled)或者被拒絕狀態(tài)(rejected);
- 處于已完成狀態(tài)時,狀態(tài)不可變,且需要有一個最終值;
- 處于被拒絕狀態(tài)時,狀態(tài)不可變,且需要有一個拒絕原因。
then方法
一個promise必須提供一個then方法,以供訪問其當前狀態(tài),或最終值或拒絕原因。
參數(shù)
該方法接收兩個參數(shù),如promise.then(onFulfilled, onRejected):
- 兩個參數(shù)均為可選,均有默認值,若不傳入,則會使用默認值;
- 兩個參數(shù)必須是函數(shù),否則會被忽略,使用默認函數(shù);
- onFulfilled: 在promise已完成后調(diào)用且僅調(diào)用一次該方法,該方法接受promise最終值作參數(shù);
- onRejected: 在promise被拒絕后調(diào)用且僅調(diào)用一次該方法,該方法接受promise拒絕原因作參數(shù);
- 兩個函數(shù)都是異步事件的回調(diào),符合JavaScript事件循環(huán)處理流程
返回值
該方法必須返回一個promise:
- var promise2 = promise1.then(onFulfilled, onRejected);
- // promise2依然是一個promise對象
決議過程(resolution)
決議是一個抽象操作過程,該操作接受兩個輸入:一個promise和一個值,可以記為;[[resolve]](promise, x),如果x是一個thenable對象,則嘗試讓promise參數(shù)使用x的狀態(tài)值;否則,將使用x值完成傳入的promise,決議過程規(guī)則如下:
1.如果promise和x引用自同一對象,則使用一個TypeError原因拒絕此promise;
2.x為Promise,則promise直接使用x的狀態(tài);
3.x為對象或函數(shù):
- 獲取一個x.then的引用;
- 若獲取x.then時拋出異常e,使用該e作為原因拒絕promise;
- 否則將該引用賦值給then;
- 若then是一個函數(shù),就調(diào)用該函數(shù),其作用域為x,并傳遞兩個回調(diào)函數(shù)參數(shù),第一個是resolvePromise,第二個是rejectPromise:
- 若調(diào)用了resolvePromise(y),則執(zhí)行resolve(promise, y);
- 若調(diào)用了rejectPrtomise(r),則使用原因r拒絕promise;
- 若多次調(diào)用,只會執(zhí)行第一次調(diào)用流程,后續(xù)調(diào)用將被忽略;
- 若調(diào)用then拋出異常e,則:
- 若promise已決議,即調(diào)用了resolvePromise或rejectPrtomise,則忽略此異常;
- 否則,使用原因e拒絕promise;
5.若then不是函數(shù),則使用x值完成promise;
4.若x不是對象或函數(shù),則使用x完成promise。
自然,以上規(guī)則可能存在遞歸循環(huán)調(diào)用的情況,如一個promsie被一個循環(huán)的thenable對象鏈決議,此時自然是不行的,所以規(guī)范建議進行檢測,是否存在遞歸調(diào)用,若存在,則以原因TypeError拒絕promise。
Promise
在ES6中,JavaScript已支持Promise,一些主流瀏覽器也已支持該Promise功能,如Chrome,先來看一個Promsie使用實例:
- var promise = new Promise((resolve, reject) => {
- setTimeout(function() {
- resolve('完成');
- }, 10);
- });
- promise.then((msg) => {
- console.log('first messaeg: ' + msg);
- })
- promise.then((msg) => {
- console.log('second messaeg: ' + msg);
- });
輸出如下:
構造器
創(chuàng)建promise語法如下:
- new Promise(function(resolve, reject) {});
- 參數(shù)
一個函數(shù),該函數(shù)接受兩個參數(shù):resolve函數(shù)和reject函數(shù);當實例化Promise構造函數(shù)時,將立即調(diào)用該函數(shù),隨后返回一個Promise對象。通常,實例化時,會初始一個異步任務,在異步任務完成或失敗時,調(diào)用resolve或reject函數(shù)來完成或拒絕返回的Promise對象。另外需要注意的是,若傳入的函數(shù)執(zhí)行拋出異常,那么這個promsie將被拒絕。
靜態(tài)方法
Promise.all(iterable)
all方法接受一個或多個promsie(以數(shù)組方式傳遞),返回一個新promise,該promise狀態(tài)取決于傳入的參數(shù)中的所有promsie的狀態(tài):
- 當所有promise都完成是,返回的promise完成,其最終值為由所有完成promsie的最終值組成的數(shù)組;
- 當某一promise被拒絕時,則返回的promise被拒絕,其拒絕原因為第一個被拒絕promise的拒絕原因;
- var p1 = new Promise((resolve, reject) => {
- setTimeout(function(){
- console.log('p1決議');
- resolve('p1');
- }, 10);
- });
- var p2 = new Promise((resolve, reject) => {
- setTimeout(function(){
- console.log('p2決議');
- resolve('p2');
- }, 10);
- });
- Promise.all( [p1, p2] )
- .then((msgs) => {
- // p1和p2完成并傳入最終值
- console.log(JSON.stringify(msgs));
- })
- .then((msg) => {
- console.log( msg );
- });
輸出如下:
Promise.race(iterable)
race方法返回一個promise,只要傳入的諸多promise中的某一個完成或被拒絕,則該promise同樣完成或被拒絕,最終值或拒絕原因也與之相同。
Promise.resolve(x)
resolve方法返回一個已決議的Promsie對象:
- 若x是一個promise或thenable對象,則返回的promise對象狀態(tài)同x;
- 若x不是對象或函數(shù),則返回的promise對象以該值為完成最終值;
- 否則,詳細過程依然按前文Promsies/A+規(guī)范中提到的規(guī)則進行。
該方法遵循Promise/A+決議規(guī)范。
Promsie.reject(reason)
返回一個使用傳入的原因拒絕的Promise對象。
實例方法
Promise.prototype.then(onFulfilled, onRejected)
該方法為promsie添加完成或拒絕處理器,將返回一個新的promise,該新promise接受傳入的處理器調(diào)用后的返回值進行決議;若promise未被處理,如傳入的處理器不是函數(shù),則新promise維持原來promise的狀態(tài)。
我們通過兩個例子介紹then方法,首先看第一個實例:
- var promise = new Promise((resolve, reject) => {
- setTimeout(function() {
- resolve('完成');
- }, 10);
- });
- promise.then((msg) => {
- console.log('first messaeg: ' + msg);
- }).then((msg) => {
- console.log('second messaeg: ' + msg);
- });
輸出如下:
輸出兩行信息:我們發(fā)現(xiàn)第二個then方法接收到的最終值是undefined,為什么呢?看看第一個then方法調(diào)用后返回的promise狀態(tài)如下:
如上圖,發(fā)現(xiàn)調(diào)用第一個then方法后,返回promise最終值為undefined,傳遞給第二個then的回調(diào),如果把上面的例子稍加改動:
- var promise = new Promise((resolve, reject) => {
- setTimeout(function() {
- resolve('完成');
- }, 10);
- });
- promise.then((msg) => {
- console.log('first messaeg: ' + msg);
- return msg + '第二次';
- }).then((msg) => {
- console.log('second messaeg: ' + msg);
- });
輸出如下:
這次兩個then方法的回調(diào)都接收到了最終值,正如我們前文所說,'then'方法返回一個新promise,并且該新promise根據(jù)其傳入的回調(diào)執(zhí)行的返回值,進行決議,而函數(shù)未明確return返回值時,默認返回的是undefined,這也是上面實例第二個then方法的回調(diào)接收undefined參數(shù)的原因。
這里使用了鏈式調(diào)用,我們需要明確:共產(chǎn)生三個promise,初始promise,兩個then方法分別返回一個promise;而第一個then方法返回的新promise是第二個then方法的主體,而不是初始promise。
Promise.prototype.catch(onRejected)
該方法為promise添加拒絕回調(diào)函數(shù),將返回一個新promise,該新promise根據(jù)回調(diào)函數(shù)執(zhí)行的返回值進行決議;若promise決議為完成狀態(tài),則新promise根據(jù)其最終值進行決議。
- var promise = new Promise((resolve, reject) => {
- setTimeout(() => {
- reject('failed');
- }, 0);
- });
- var promise2 = promise.catch((reason) => {
- console.log(reason);
- return 'successed';
- });
- var promise3 = promise.catch((reason) => {
- console.log(reason);
- });
- var promise4 = promise.catch((reason) => {
- console.log(reason);
- throw 'failed 2';
- });
輸出如下圖:
如圖中所輸出內(nèi)容,我們需要明白以下幾點:
- catch會為promise注冊拒絕回調(diào)函數(shù),一旦異步操作結束,調(diào)用了reject回調(diào)函數(shù),則依次執(zhí)行注冊的拒絕回調(diào);
- 另外有一點和then方法相似,catch方法返回的新promise將使用其回調(diào)函數(shù)執(zhí)行的返回值進行決議,如promise2,promise3狀態(tài)均為完成(resolved),但是promise3最終值為undefined,而promise2最終值為successed,這是因為在調(diào)用promise.catch方法時,傳入的回調(diào)沒有顯式的設置返回值;
- 對于promise4,由于調(diào)用catch方法時,回調(diào)中throw拋出異常,所以promise4狀態(tài)為拒絕(rejected),拒絕原因為拋出的異常;
- 特別需要注意的是這里一共有四個promise,一旦決議,它們之間都是獨立的,我們需要明白無論是then方法,還是catch方法,都會返回一個新promise,此新promise與初始promise相互獨立。
catch方法和then方法的第二個參數(shù)一樣,都是為promise注冊拒絕回調(diào)。
鏈式調(diào)用
和jQuery的鏈式調(diào)用一樣,Promise設計也支持鏈式調(diào)用,上一步的返回值作為下一步方法調(diào)用的主體:
- new Promise((resolve, reject) => {
- setTimeout(()=>{
- resolve('success');
- },0);
- }).then((msg) => {
- return 'second success';
- }).then((msg) => {
- console.log(msg);
- });
最后輸出:second success,初始化promise作為主體調(diào)用第一個then方法,返回完成狀態(tài)的新promise其最終值為second success,然后該新promise作為主體調(diào)用第二個then方法,該方法返回第三個promise,而且該promise最終值為undefined,若不清楚為什么,請回到關于Promise.prototype.then和Promise.prototype.catch的介紹。
錯誤處理
我們前文提到了JavaScript異步回調(diào)中的異常是難以處理的,而Promise對異步異常和錯誤的處理是比較方便的:
- var promise = new Promise((resolve, reject) => {
- test(); // 拋出異常
- resolve('success'); // 被忽略
- });
- console.log(promise);
- promise.catch((reason) => {
- console.log(reason);
- });
輸出如圖,執(zhí)行test拋出異常,導致promise被拒絕,拒絕原因即拋出的異常,然后執(zhí)行catch方法注冊的拒絕回調(diào):
決議,完成與拒絕
目前為止,關于Promise是什么,我們應該有了一定的認識,這里,需要再次說明的是Promise的三個重要概念及其關系:決議(resolve),完成(fulfill),拒絕(reject)。
- 完成與拒絕是Promise可能處于的兩種狀態(tài);
- 決議是一個過程,是Promise由等待狀態(tài)變更為完成或拒絕狀態(tài)的一個過程;
- 靜態(tài)方法Promise.resolve描述的就是一個決議過程,而Promise構造函數(shù),傳入的回調(diào)函數(shù)的兩個參數(shù):resolve和reject,一個是完成函數(shù),一個是拒絕函數(shù),這里令人疑惑的是為什么這里依然使用resolve而不是fulfill,我們通過一個例子解釋這個問題:
- var promise = new Promise((resolve, reject) => {
- resolve(Promise.reject('failed'));
- });
- promise.then((msg) => {
- console.log('完成:' + msg);
- }, (reason) => {
- console.log('拒絕:' + reason);
- });
輸出如圖:
上例中,在創(chuàng)建一個Promise時,給resolve函數(shù)傳遞的是一個拒絕Promise,此時我們發(fā)現(xiàn)promise狀態(tài)是rejected,所以這里第一個參數(shù)函數(shù)執(zhí)行,完成的是一個更接近決議的過程(可以參考前文講述的決議過程),所以命名為resolve是更合理的;而第二個參數(shù)函數(shù),則只是拒絕該promise:
- var promise = new Promise((resolve, reject) => {
- reject(Promise.resolve('success'));
- });
- promise.then((msg) => {
- console.log('完成:' + msg);
- }, (reason) => {
- console.log('拒絕:' + reason);
- });
reject函數(shù)并不會處理參數(shù),而只是直接將其當做拒絕原因拒絕promise。
Promise實現(xiàn)
Promise是什么,怎么樣使用就介紹到此,另外一個問題是面試過程中經(jīng)常也會被提及的:如何實現(xiàn)一個Promise,當然,限于篇幅,我們這里只講思路,不會長篇大論。
構造函數(shù)
首先創(chuàng)建一個構造函數(shù),供實例化創(chuàng)建promise,該構造函數(shù)接受一個函數(shù)參數(shù),實例化時,會立即調(diào)用該函數(shù),然后返回一個Promise對象:
- var MyPromise = (() => {
- var value = undefined; // 當前Promise
- var tasks = []; // 完成回調(diào)隊列
- var rejectTasks = []; // 拒絕回調(diào)隊列
- var state = 'pending'; // Promise初始為等待態(tài)
- // 輔助函數(shù),使異步回調(diào)下一輪事件循環(huán)執(zhí)行
- var nextTick = (callback) => {
- setTimeout(callback, 0);
- };
- // 輔助函數(shù),傳遞Promsie的狀態(tài)值
- var ref = (value) => {
- if (value && typeof value.then === 'function') {
- // 若狀態(tài)值為thenable對象或Promise,直接返回
- return value;
- }
- // 否則,將最終值傳遞給下一個then方法注冊的回調(diào)函數(shù)
- return {
- then: function(callback) {
- return ref(callback(value));
- }
- }
- };
- var resolve = (val) => {};
- var reject = (reason) => {};
- function MyPromise(func) {
- func(resolve.bind(this), reject.bind(this));
- }
- return MyPromise;
- });
靜態(tài)方法
在實例化創(chuàng)建Promise時,我們會將構造函數(shù)的兩個靜態(tài)方法:resolve和reject傳入初始函數(shù),接下來需要實現(xiàn)這兩個函數(shù):
- var resolve = (val) => {
- if (tasks) {
- value = ref(val);
- state = 'resolved'; // 將狀態(tài)標記為已完成
- // 依次執(zhí)行任務回調(diào)
- tasks.forEach((task) => {
- value = nextTick((val) => {task[0](self.value);});
- });
- tasks = undefined; // 決議后狀態(tài)不可變
- return this;
- }
- };
- var reject = (reason) => {
- if (tasks) {
- value = ref(reason);
- state = 'rejected'; // 將狀態(tài)標記為已完成
- // 依次執(zhí)行任務回調(diào)
- tasks.forEach((task) => {
- nextTick((reason) => {task[1](value);});
- });
- tasks = undefined; // 決議后狀態(tài)不可變
- return this;
- }
- };
還有另外兩個靜態(tài)方法,原理還是一樣,就不細說了。
實例方法
目前構造函數(shù),和靜態(tài)方法完成和拒絕Promise都已經(jīng)實現(xiàn),接下來需要考慮的是Promise的實例方法和鏈式調(diào)用:
- MyPromise.prototype.then = (onFulfilled, onRejected) => {
- onFulfilled = onFulfilled || function(value) {
- // 默認的完成回調(diào)
- return value;
- };
- onRejected = onRejected || function(reason) {
- // 默認的拒絕回調(diào)
- return reject(reason);
- };
- if (tasks) {
- // 未決議時加入隊列
- tasks.push(onFulfilled);
- rejectTasks.push(onRejected);
- } else {
- // 已決議,直接加入事件循環(huán)執(zhí)行
- nextTick(() => {
- if (state === 'resolved') {
- value.then(onFulfilled);
- } else if (state === 'rejected') {
- value.then(onRejected);
- }
- });
- }
- return this;
- };
實例
以上可以簡單實現(xiàn)Promise部分異步管理功能:
- var promise = new MyPromise((resolve, reject) => {
- setTimeout(() => {
- resolve('完成');
- }, 0);
- });
- promise.then((msg) => {console.log(msg);});
本篇由回調(diào)函數(shù)起,介紹了回調(diào)處理異步任務的常見問題,然后介紹Promises/A+規(guī)范及Promise使用,最后就Promise實現(xiàn)做了簡單闡述(之后有機會會詳細實現(xiàn)一個Promise),花費一周終于把基本知識點介紹完,下一篇將介紹JavaScript異步與生成器實現(xiàn)。
參考