前言
編程語言很多的新概念都是為了更好的解決老問題而提出來的鸽疾。這篇博客就是一步步分析異步編程解決方案的問題以及后續(xù)提出的新概念是否解決了問題。
將回調(diào)包裝成promise
觀察者模式與事件監(jiān)聽
內(nèi)容來源于《ES6入門-阮一峰》《你不知道的JS》《MDN web文檔》《N
ode.js》训貌、‘網(wǎng)上的諸多博客’制肮、‘自己以前的代碼’。
本博客沒有什么有價值的知識递沪,僅作總結(jié)梳理之用豺鼻,初學(xué)者可以看看。
異步與多線程
先說結(jié)論:JS是單線程的但是宿主環(huán)境:瀏覽器或node 不是款慨。
簡單的看JS的代碼是從上至下執(zhí)行的儒飒,如果中間遇到了特別耗時的任務(wù)。那此任務(wù)下面的代碼就會一直等待等待樱调。也就是阻塞
约素。想解決這個問題,也就是解決耗時任務(wù)的問題笆凌,一種可以開啟多個線程
圣猎,將任務(wù)移到子線程去不占用主線程,比如安卓app就是在主線程更新UI乞而,在子線程種執(zhí)行耗時操作譬如網(wǎng)絡(luò)請求送悔。(html5新增了web worker
也可以開個子線程執(zhí)行其他操作了)。一種就是現(xiàn)在js主流采用的異步編程
:先執(zhí)行所有的同步代碼塊爪模,然后再執(zhí)行異步代碼塊欠啤。
一個比較有趣的點是,剛開始聽說異步的時候屋灌,總是會想起來JS是單線程的洁段。就好像是只有一個工人一樣,分給他的任務(wù)即使先做簡單的共郭,那復(fù)雜的不一樣的得慢慢做嗎祠丝?很顯然我是錯的疾呻,JS是單線程的,但是瀏覽器是多線程的写半。
具體請看這篇博客岸蜗,講的很好,這篇
- 同步任務(wù)都在主線程上執(zhí)行叠蝇,形成一個執(zhí)行棧
- 主線程之外璃岳,事件觸發(fā)線程管理著一個任務(wù)隊列,只要異步任務(wù)有了運行結(jié)果悔捶,就在任務(wù)隊列之中放置一個事件铃慷。
-
一旦執(zhí)行棧中的所有同步任務(wù)執(zhí)行完畢(此時JS引擎空閑),系統(tǒng)就會讀取任務(wù)隊列蜕该,執(zhí)行回調(diào)枚冗。
在node種也有類似的Event loop
1、每個Node.js進程只有一個主線程在執(zhí)行程序代碼蛇损,形成一個執(zhí)行棧(execution context stack)。
2坛怪、主線程之外淤齐,還維護了一個"事件隊列"(Event queue)。當(dāng)用戶的網(wǎng)絡(luò)請求或者其它的異步操作到來時袜匿,node都會把它放到Event Queue之中更啄,此時并不會立即執(zhí)行它,代碼也不會被阻塞居灯,繼續(xù)往下走祭务,直到主線程代碼執(zhí)行完畢。
3怪嫌、主線程代碼執(zhí)行完畢完成后义锥,然后通過Event Loop,也就是事件循環(huán)機制岩灭,開始到Event Queue的開頭取出第一個事件拌倍,從線程池中分配一個線程去執(zhí)行這個事件,接下來繼續(xù)取出第二個事件噪径,再從線程池中分配一個線程去執(zhí)行柱恤,然后第三個,第四個找爱。主線程不斷的檢查事件隊列中是否有未執(zhí)行的事件梗顺,直到事件隊列中所有事件都執(zhí)行完了,此后每當(dāng)有新的事件加入到事件隊列中车摄,都會通知主線程按順序取出交EventLoop處理寺谤。當(dāng)有事件執(zhí)行完畢后仑鸥,會通知主線程,主線程執(zhí)行回調(diào)矗漾,線程歸還給線程池锈候。
4、主線程不斷重復(fù)上面的第三步敞贡。
至于具體實現(xiàn)以及與瀏覽器之間的差異就不寫了泵琳,我也沒搞懂。
回調(diào)
wiki定義:In computer programming, a callback, also known as a "call-after" function, is any executable code that is passed as an argument to other code that is expected to call back (execute) the argument at a given time.This execution may be immediate as in a synchronous callback, or it might happen at a later time as in an asynchronous callback
簡單的說就是做好了叫我誊役。根據(jù)上文的瀏覽器或node異步事件處理機制敘述获列,可以很輕松的看出,怎么用回調(diào)寫異步代碼蛔垢。將耗時操作與需要得到結(jié)果后執(zhí)行的操作用回調(diào)函數(shù)寫就击孩。例如(還有事件監(jiān)聽或訂閱模式與本篇內(nèi)容關(guān)系不大,不寫鹏漆。)
function foo() {
console.log('執(zhí)行完了')
}
function bar1() {
console.log('同步函數(shù)1')
}
function bar2() {
console.log('同步函數(shù)2')
}
bar1()
setTimeout(foo, 1000)
bar2()
/**
同步函數(shù)1
同步函數(shù)2
執(zhí)行完了
*/
回調(diào)是異步的解決方案巩梢,但是回調(diào)的問題又是什么?
回調(diào)地獄
listen( "click", function handler(evt){
setTimeout( function request(){
ajax( "http://some.url.1", function response(text){
if (text == "hello") {
handler();
}
else if (text == "world") {
request();
}
} );
}, 500) ;
} );
誰都寫過類似的代碼艺玲,一個套一個括蝠。所以回調(diào)的問題一是不清晰。
順序
如果想組織兩個或多個異步函數(shù)的順序用回調(diào)函數(shù)就會變得復(fù)雜饭聚,比如上面回調(diào)地獄那種忌警,按照順序一一執(zhí)行,又或要等兩個異步函數(shù)都出結(jié)果才會回調(diào)秒梳,或一個出了結(jié)果立刻回調(diào)另一個作廢法绵。這些操作都需要在每一個回調(diào)函數(shù)里書寫判斷邏輯。
控制倒轉(zhuǎn)
假設(shè)
bar1()
setTimeout(foo, 1000)
bar2()
我們的目的是先執(zhí)行bar1酪碘、bar2稍后執(zhí)行一次foo朋譬。這個代碼用了最熟悉的setTimeout,所以可以保證按照設(shè)計執(zhí)行婆跑,但是如果這個異步函數(shù)是第三方提供的此熬,那么就失去了foo的執(zhí)行控制權(quán)。稱之為控制倒轉(zhuǎn)滑进。
這種情況可能會導(dǎo)致回調(diào)不調(diào)用犀忱、調(diào)用過早、調(diào)用多次扶关、沒有得到必要參數(shù)阴汇、吞掉了異常等等。
可以利用錯誤優(yōu)先模式解決部分問題
function response(err,data) {
// 有錯节槐?
if (err) {
console.error( err );
}
// 否則搀庶,認(rèn)為成功
else {
console.log( data );
}
}
ajax( "http://some.url.1", response );
總結(jié)
又不是不能用拐纱,應(yīng)該可以很準(zhǔn)確的形容異步回調(diào)。組織代碼不清晰缺乏順序性哥倔,對很多場景要加判斷邏輯缺乏可靠性〗占埽現(xiàn)在輪到promise解決這些問題了。
Promise
回調(diào)缺乏可靠性的原因就是我們不知道它何時會完成也不知道是否成功咆蒿。Promise就解決了這個問題东抹。
Promise中保存著未來才會結(jié)束的事件,有三種狀態(tài):pending(進行中)沃测、fulfilled(已成功)和rejected(已失旂郧)一旦狀態(tài)改變,就不會再變蒂破。這時就稱為 resolved(已定型)馏谨。
const promise = new Promise(function(resolve, reject) {
// ... some code
if (/* 異步操作成功 */){
resolve(value);
} else {
reject(error);
}
});
==============
promise.then(function(value) {
// success
}, function(error) {
// failure
});
Promise構(gòu)造函數(shù)接受一個函數(shù)作為參數(shù),該函數(shù)的兩個參數(shù)分別是resolve和reject附迷。它們是兩個函數(shù)惧互,由 JavaScript 引擎提供,不用自己部署喇伯。
resolve函數(shù)的作用是壹哺,將Promise對象的狀態(tài)從“未完成”變?yōu)椤俺晒Α保磸?pending 變?yōu)?resolved),在異步操作成功時調(diào)用艘刚,并將異步操作的結(jié)果,作為參數(shù)傳遞出去截珍;reject函數(shù)的作用是攀甚,將Promise對象的狀態(tài)從“未完成”變?yōu)椤笆 保磸?pending 變?yōu)?rejected),在異步操作失敗時調(diào)用岗喉,并將異步操作報出的錯誤秋度,作為參數(shù)傳遞出去。
Promise實例生成以后钱床,可以用then方法分別指定resolved狀態(tài)和rejected狀態(tài)的回調(diào)函數(shù)荚斯。
雖然依然是基于回調(diào)的,但是卻是通過固定的形式將回調(diào)的形式固定下來查牌,并且會傳遞異步得到的數(shù)據(jù)或錯誤給回調(diào)函數(shù)事期。解決了信任問題。
回調(diào)包裝成promise
假設(shè)是包裝某個特定的函數(shù),可以直接構(gòu)造并設(shè)定好resolve與reject纸颜。
const readFile = function (fileName) {
return new Promise(function (resolve, reject) {
fs.readFile(fileName, function(error, data) {
if (error) return reject(error);
resolve(data);
});
});
};
一種是調(diào)用個方法可以隨時將回調(diào)promise化兽泣,這個可以下載第三方庫,比如bluebird
解決回調(diào)問題
- 回調(diào)過早調(diào)用
如果對一個 Promise 調(diào)用 then(..) 的時候胁孙,即使這個 Promise是立即resolve的函數(shù)(即Promise內(nèi)部沒有ajax等異步操作唠倦,只有同步操作)称鳞, 提供給then(..) 的回調(diào)也是會被異步調(diào)用的
Promise.resolve()
可以將現(xiàn)有對象轉(zhuǎn)化為Promise對象。根據(jù)參數(shù)不同而結(jié)果不同
- 參數(shù)Promise實例
不做任何修改返回這個實例稠鼻。 - 參數(shù)是個thenable對象
將這個對象轉(zhuǎn)為Promise對象冈止,并立刻執(zhí)行thenable對象的then方法。thenable對象指的是具有then方法的對象 - 參數(shù)不是具有then方法的對象候齿,或根本就不是對象熙暴,比如基本類型值
返回一個新的 Promise 對象,狀態(tài)為resolved毛肋。 - 不帶有任何參數(shù)
直接返回一個resolved狀態(tài)的 Promise 對象怨咪。
- 回調(diào)調(diào)用次數(shù)過多
Promise 的內(nèi)部機制決定了調(diào)用單個Promise的then方法, 回調(diào)只會被執(zhí)行一次润匙,因為Promise的狀態(tài)變化是單向不可逆的诗眨,當(dāng)這個Promise第一次調(diào)用resolve方法, 使得它的狀態(tài)從pending(正在進行)變成fullfilled(已成功)或者rejected(被拒絕)后孕讳, 它的狀態(tài)就再也不能變化了 - 回調(diào)中的報錯被吞掉
Promise中的then方法中的error回調(diào)被調(diào)用的時機有兩種情況:- Promise中主動調(diào)用了reject (有意識地使得Promise的狀態(tài)被拒絕)匠楚, 這時error
回調(diào)能夠接收到reject方法傳來的參數(shù)(reject(error)) - 在定義的Promise中, 運行時候報錯(未預(yù)料到的錯誤)厂财, 也會使得Promise的
狀態(tài)被拒絕芋簿,從而使得error回調(diào)能夠接收到捕捉到的錯誤
- Promise中主動調(diào)用了reject (有意識地使得Promise的狀態(tài)被拒絕)匠楚, 這時error
4.回調(diào)沒有調(diào)用
設(shè)置一個超時用Promise.race組合。Promise.race方法是將多個 Promise 實例璃饱,包裝成一個新的 Promise 實例与斤,只要其中一個實例改變狀態(tài)新的大實例就會改變狀態(tài)。
const p = Promise.race([
fetch('/resource-that-may-take-a-while'),
new Promise(function (resolve, reject) {
setTimeout(() => reject(new Error('request timeout')), 5000)
})
]);
p
.then(console.log)
.catch(console.error);
組織問題
- 鏈?zhǔn)?br>
將回調(diào)地獄改成鏈?zhǔn)郊远瘢恳粋€then里面的異步操作都可以返回一個值撩穿,傳遞給下一個異步操作當(dāng)作參數(shù)。
- 每次你在一個Promise上調(diào)用then(..)的時候谒撼,它都創(chuàng)建并返回一個新的Promise食寡,我們可以在它上面進行 鏈接。
- 無論你從then(..)調(diào)用的完成回調(diào)中(第一個參數(shù))返回什么值廓潜,它都做為被鏈接的Promise的完成抵皱。
Promise.then(
// 第一個異步操作
).then(
// 第二個異步操作
).then(
// 第三個異步操作
)
- 門
Promise.all
方法
all方法接收一個Promise數(shù)組,并且返回一個新的“大Promise”辩蛋, 只有數(shù)組里的全部Promise的狀態(tài)都轉(zhuǎn)為Fulfilled(成功)呻畸,這個“大Promise”的狀態(tài)才會轉(zhuǎn)為Fulfilled(成功), 這時候悼院, then方法里的成功的回調(diào)接收的參數(shù)也是數(shù)組擂错,分別和數(shù)組里的子Promise一一對應(yīng) - 競態(tài)
Promise.race
方法
Promise.race方法是將多個 Promise 實例,包裝成一個新的 Promise 實例樱蛤,只要其中一個實例改變狀態(tài)新的大實例就會改變狀態(tài)钮呀。
錯誤處理
Promise.prototype.catch方法是.then(null, rejection)的別名剑鞍,用于指定發(fā)生錯誤時的回調(diào)函數(shù)。如果異步操作拋出錯誤爽醋,狀態(tài)就會變?yōu)閞ejected蚁署,就會調(diào)用catch方法指定的回調(diào)函數(shù),處理這個錯誤蚂四。另外光戈,then方法指定的回調(diào)函數(shù),如果運行中拋出錯誤遂赠,也會被catch方法捕獲久妆。
promise
.then(function(data) {
// success
})
.catch(function(err) {
// error
});
總結(jié)
Promise是一個用可靠語義來增強回調(diào)的模式,所以它的行為更合理更可靠跷睦。通過將回調(diào)的 控制倒轉(zhuǎn) 反置過來筷弦,我們將控制交給一個可靠的系統(tǒng)(Promise),它是為了將你的異步處理進行清晰的表達而特意設(shè)計的抑诸。
Promise也有一些缺點烂琴。首先,無法取消Promise蜕乡,一旦新建它就會立即執(zhí)行奸绷,無法中途取消。其次层玲,如果不設(shè)置回調(diào)函數(shù)号醉,Promise內(nèi)部拋出的錯誤,不會反應(yīng)到外部辛块。第三扣癣,當(dāng)處于pending狀態(tài)時,無法得知目前進展到哪一個階段(剛剛開始還是即將完成)憨降。
Genarator
遍歷器(Intertor)
遍歷器(Iterator)是一種接口,為各種不同的數(shù)據(jù)結(jié)構(gòu)提供統(tǒng)一的訪問機制该酗。任何數(shù)據(jù)結(jié)構(gòu)只要部署 Iterator 接口授药,就可以完成遍歷操作(即依次處理該數(shù)據(jù)結(jié)構(gòu)的所有成員)。
Iterator 的遍歷過程是這樣的呜魄。
(1)創(chuàng)建一個指針對象悔叽,指向當(dāng)前數(shù)據(jù)結(jié)構(gòu)的起始位置。也就是說爵嗅,遍歷器對象本質(zhì)上娇澎,就是一個指針對象。
(2)第一次調(diào)用指針對象的next方法睹晒,可以將指針指向數(shù)據(jù)結(jié)構(gòu)的第一個成員趟庄。
(3)第二次調(diào)用指針對象的next方法括细,指針就指向數(shù)據(jù)結(jié)構(gòu)的第二個成員。
(4)不斷調(diào)用指針對象的next方法戚啥,直到它指向數(shù)據(jù)結(jié)構(gòu)的結(jié)束位置奋单。
每一次調(diào)用next方法,都會返回數(shù)據(jù)結(jié)構(gòu)的當(dāng)前成員的信息猫十。具體來說览濒,就是返回一個包含value和done兩個屬性的對象。其中拖云,value屬性是當(dāng)前成員的值贷笛,done屬性是一個布爾值,表示遍歷是否結(jié)束宙项。
有些數(shù)據(jù)結(jié)構(gòu)原生部署了Iterator接口乏苦,可以直接調(diào)用原生Array、Map杉允、Set邑贴、NodeList 對象等等。對象是沒有此接口的叔磷,如果需要可以用Map代替拢驾。或者用Generator 函數(shù)改基。
這些數(shù)據(jù)結(jié)構(gòu)有此接口的原因是它們有Symbol.iterator
屬性繁疤, 因為是Symbol
值要用[]
調(diào)用,此屬性為一個函數(shù)秕狰,叫遍歷器生成函數(shù)稠腊,調(diào)用后返回遍歷器。
原生數(shù)組可以直接這樣遍歷鸣哀。
let arr = [1,2,3]
let arrIter = arr[Symbol.iterator]()
arrIter.next() //? ?????{ value: 1, done: false }?????
arrIter.next() //? { value: 2, done: false }?????
arrIter.next() //? ?????{ value: 3, done: false }?????
arrIter.next() //? ?????{ value: undefined, done: true }?????
================================
Generator 函數(shù)順便測試一下
let obj = {
bar: {
b : 2
},
[Symbol.iterator]: function *() {
yield 1
yield 2+3
yield 'hello'
yield (function () {
return '立即執(zhí)行函數(shù)的返回值'
})()
yield foo = {
a : 1
}
yield this.bar
}
}
let ObjIter = obj[Symbol.iterator]()
ObjIter.next() //? ?????{ value: 1, done: false }?????
ObjIter.next() //? ?????{ value: 5, done: false }?????
ObjIter.next() //? ?????{ value: 'hello', done: false }?????
ObjIter.next() //? ?????{ value: '立即執(zhí)行函數(shù)的返回值', done: false }?????
ObjIter.next() //? ?????{ value: { a: 1 }, done: false }?????
ObjIter.next() //? ????????????????????{ value: { b: 2 }, done: false }?????
let arr = [...obj] //? ?????[ 1, 5, 'hello', '立即執(zhí)行函數(shù)的返回值', { a: 1 }架忌,{ b: 2 } ]?????
for (const x of obj) {
console.log(x) //? 1, 5, 'hello', '立即執(zhí)行函數(shù)的返回值', { a: 1 } 我衬,{ b: 2 }
}
for (const key in obj) {
const element = obj[key]
console.log(element) //叹放??????{ b: 2 }?????
}
Reflect.ownKeys(obj) //? ?????[ 'bar', Symbol(Symbol.iterator) ]?????
for (const x of Reflect.ownKeys(obj)) {
console.log(obj[x]) //? ?????{ b: 2 }挠羔,?????[λ: [Symbol.iterator]]?????
}
想讓對象有遍歷器接口只要給它一個Symbol.iterator
屬性就行井仰,這個屬性也得是個函數(shù)。上面的做法破加,將一個generator
函數(shù)賦予給對象的遍歷器屬性俱恶,當(dāng)遍歷這個對象時就會調(diào)用此函數(shù),但是遍歷結(jié)果是此函數(shù)內(nèi)部的表達式結(jié)果,與對象屬性無關(guān)合是。
下面兩種方法可以遍歷對象屬性的值了罪,一種就是不給它賦予遍歷器屬性而是借其他數(shù)據(jù)結(jié)構(gòu)之手刃榨,另一種在Symbol.iterator
屬性函數(shù)里寫邏輯將對象屬性遍歷后返回屬性值苛让。
for (let key of Object.keys(someObject)) {
console.log(key + ': ' + someObject[key]);
}
==========================
function* entries() {
for (let key of Reflect.ownKeys(this)) {
yield [key, this[key]];
}
}
let obj1 = {
a : 1,
b: 2
}
obj1[Symbol.iterator] = entries
for (const [key, value] of obj1) {
console.log(`${key}: ${value}`)
}
Generator
generator
函數(shù)是遍歷器對象生成函數(shù)阁将,執(zhí)行后返回遍歷器對象匣沼。*
號注明函數(shù)為生成器函數(shù)宵凌,在函數(shù)內(nèi)部使用yield
表達式產(chǎn)出不同的內(nèi)部狀態(tài)
function* foo() {
yield 1
yield 2
return 3
}
let bar = foo()
bar.next() //? ?????{ value: 1, done: false }?????
bar.next() //? ?????{ value: 2, done: false }?????
bar.next() //? ?????{ value: 3, done: true }?????
當(dāng)調(diào)用next
方法的時候曲掰,函數(shù)內(nèi)部語句會運行到第一個yield
停止并將yield
后的表達式計算出結(jié)果后返回埋心,與return
類似羡洛,不同的是鹤竭,yiled后的語句會在調(diào)用下一個next()方法后繼續(xù)執(zhí)行知道遇到第二個yield
踊餐。
狀態(tài)機與協(xié)程
這個和此篇無關(guān),所以只寫個標(biāo)題吧臀稚。
Generator內(nèi)外部通信
生成器函數(shù)不僅可以一步一步的運行下去輸出結(jié)果吝岭,還可以傳入不同數(shù)據(jù)來操控下面的步驟。
next方法的參數(shù)
yield
意為產(chǎn)出吧寺,其后跟的表達式的結(jié)果在計算后會直接產(chǎn)出窜管,函數(shù)內(nèi)部并不能得到
function* foo(x) {
let y = 2*(yield x+1)
yield y
}
let bar = foo(3)
bar.next() //? ?????{ value: 4, done: false }?????
bar.next() //? ?????{ value: NaN, done: false }?????
bar.next() //? ?????{ value: undefined, done: true }?????
第一步運行的是x+1
得出的結(jié)果為4產(chǎn)出了,外部可以看到稚机。當(dāng)內(nèi)部需要它再參與計算的時候就發(fā)現(xiàn)根本不存在變成2*undefined
結(jié)果就是NaN
幕帆。
如果yield算產(chǎn)出,那next
方法的參數(shù)算投入赖条。參數(shù)會被當(dāng)作上個yield
表達式的結(jié)果值,如果沒有上個自然無效失乾。所以第一個next()算是啟動遍歷器的不能加參數(shù)。
上例改一下
function* foo(x) {
let y = x*(yield x+1)
console.log(x) //纬乍?3
yield y
}
let bar = foo(3)
bar.next() //? ?????{ value: 4, done: false }?????
bar.next(5) //? ?????{ value: 15, done: false }?????
return和throw
return
方法碱茁,可以返回給定的值,并且終結(jié)遍歷 Generator 函數(shù)仿贬。
function *foo() {
yield 1
yield 2
yield 3
}
let bar = foo()
bar.next() //? ?????{ value: 1, done: false }?????
bar.return('結(jié)束了') //? ?????{ value: '結(jié)束了', done: true }?????
bar.next() //? ?????{ value: undefined, done: true }?????
如果函數(shù)內(nèi)部有finally
就會在調(diào)用return后立刻執(zhí)行纽竣,finally內(nèi)代碼,最后執(zhí)行return
function *foo() {
try {
yield 1
} finally {
yield 2
}
yield 3
}
let bar = foo()
bar.next() //? ?????{ value: 1, done: false }?????
bar.return('結(jié)束了') //? ?????{ value: 2, done: false }?????
bar.next() //? ?????{ value: '結(jié)束了', done: true }?????
throw
方法茧泪,可以在函數(shù)體外拋出錯誤蜓氨,然后在 Generator 函數(shù)體內(nèi)捕獲。
function *foo() {
try {
yield 1
yield 2
} catch (e) {
yield 3
console.log(e) //?出錯了
}
yield 4
}
let bar = foo()
bar.next() //? ?????{ value: 1, done: false }?????
bar.throw('出錯了') //? ?????{ value: 3, done: false }?????
bar.next() //? ?????{ value: 4, done: false }?????
throw
方法被調(diào)用后调炬,會結(jié)束調(diào)用下面的代碼,并從catch后繼續(xù)舱馅,注意throw自帶next()缰泡,傳入的錯誤值會被捕獲成為catch的參數(shù),并傳出下一個yield的值。
總結(jié)
next
棘钞、return
缠借、throw
可以粗略的理解為替換yield表達式,
next
是換為值
return
是換為return語句
throw
是換為throw語句
不同點在于宜猜,生成器函數(shù)內(nèi)如果存在try catch或finally即使return或throw了還是可以繼續(xù)運行泼返。
Generator與Promise
Generator 函數(shù)可以暫停執(zhí)行和恢復(fù)執(zhí)行,這是它能封裝異步任務(wù)的根本原因姨拥。除此之外绅喉,它還有兩個特性,使它可以作為異步編程的完整解決方案:函數(shù)體內(nèi)外的數(shù)據(jù)交換和錯誤處理機制叫乌。具體的就是上文寫的柴罐,
下面的代碼用Fetch
實現(xiàn),它是基于promise
的憨奸,在node環(huán)境中使用要裝個插件
npm install node-fetch --save
簡單的用下
const fetch = require('node-fetch');
let url = 'http://api.apiopen.top/singlePoetry' //下文省略上面兩句
fetch(url)
.then(res => res.json())
.then(json => console.log(json));
會隨機出個詩句革屠,網(wǎng)上隨便找的接口
{ code: 200,
message: '成功!',
result:
{ author: '陸游',
origin: '夜泊水村',
category: '古詩文-人生-青春',
content: '一身報國有萬死,雙鬢向人無再青排宰。' } }
異步
回到起點似芝,為什么需要回調(diào)?因為如果把耗時的代碼寫成同步的形式板甘,那代碼就會卡在那里也就是發(fā)生了阻塞
党瓮,所以需要先執(zhí)行完同步代碼,最后執(zhí)行耗時代碼得出結(jié)果后再執(zhí)行處理結(jié)果的代碼虾啦,這個過程叫做用回調(diào)完成異步麻诀。
在generator
函數(shù)中的yield
表達式有兩個特性,一是向函數(shù)外傳出消息傲醉、二是暫停函數(shù)蝇闭。所以generator函數(shù)天生就具有異步的特征,如果手動執(zhí)行g(shù)enerator函數(shù)硬毕,那此函數(shù)是不會執(zhí)行的呻引,自然會在同步代碼后執(zhí)行,當(dāng)我們執(zhí)行函數(shù)時吐咳,在yeild后放異步操作逻悠,那函數(shù)就會暫停于此直到執(zhí)行完畢,當(dāng)傳出結(jié)果就可以繼續(xù)next()執(zhí)行下一步韭脊,這個下一步就和回調(diào)函數(shù)一個含義童谒。
function *foo() {
try {
let result = yield fetch(url)
console.log(result)
} catch (error) {
console.log(error)
}
}
===================================
let it = foo()
let result = it.next().value
result
.then(data => data.json())
.then(data => it.next(data))
.catch(e => it.throw(e))
可以看到let result = yield fetch(url) console.log(result)
將異步回調(diào)的形式,寫成了同步的方式沪羔,沒有回調(diào)地獄也沒有無限的then饥伊。
這個函數(shù)問題是執(zhí)行起來復(fù)雜,雖然函數(shù)內(nèi)部包裝異步操作將異步改同步,但是真正運行的時候要多幾步琅豆,首先要先啟動第一步愉豺,其次yield
是產(chǎn)出值,所以代碼中的result
其實是等于undefined
茫因,需要在bar.next(data)
下一次執(zhí)行時傳入上次的結(jié)果值 并賦值蚪拦。
如果生成器函數(shù)里包含著一個異步操作序列,很多步的promise冻押,那generator
內(nèi)的復(fù)雜度其實不高驰贷,寫成同步形式反而更好分清執(zhí)行順序。但是運行起來的時候就要在外面寫一長串的promise鏈了翼雀,因為要將函數(shù)分步運行并且將yield傳出的異步結(jié)果值再傳進去饱苟。
基于Promise的自動執(zhí)行
(還有基于回調(diào)的自動執(zhí)行 Thunk函數(shù),不寫了狼渊,所有下面yield后跟的異步操作必須是基于promise的箱熬,如果不是要先用new Promise包裝。)
上文的問題就是寫generator函數(shù)簡單狈邑,執(zhí)行起來麻煩城须,如果自動執(zhí)行的話就完美了。
function run(fn) {
let it = fn()
function next(data) {
let result = it.next(data)
if (result.done) return result.value
result.value
.then(res => res.json())
.then((data) => next(data))
.catch(e => it.throw(e))
}
next()
}
一個利用遞歸簡單的實現(xiàn)米苹。運行一下
function *foo() {
try {
let result = yield fetch(url)
console.log(result)
let result2 = yield fetch(url)
console.log(result2)
} catch (error) {
console.log(error)
}
}
run(foo) //結(jié)果是吟了兩句詩
只是簡單的實現(xiàn)糕伐,下面復(fù)制下《你不知道的js》與es6入門中的實現(xiàn)
function run(gen) {
var args = [].slice.call( arguments, 1), it;
// 在當(dāng)前的上下文環(huán)境中初始化generator
it = gen.apply( this, args );
// 為generator的完成返回一個promise
return Promise.resolve()
.then( function handleNext(value){
// 運行至下一個讓出的值
var next = it.next( value );
return (function handleResult(next){
// generator已經(jīng)完成運行了?
if (next.done) {
return next.value;
}
// 否則繼續(xù)執(zhí)行
else {
return Promise.resolve( next.value )
.then(
// 在成功的情況下繼續(xù)異步循環(huán)蘸嘶,將解析的值送回generator
handleNext,
// 如果`value`是一個拒絕的promise良瞧,就將錯誤傳播回generator自己的錯誤處理g
function handleErr(err) {
return Promise.resolve(
it.throw( err )
)
.then( handleResult );
}
);
}
})(next);
} );
}
--------------------------------------------------------------------------------
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
語言內(nèi)置的,不需要執(zhí)行器的generator異步實現(xiàn)训唱。
async function foo() {
try {
let result = await fetch(url)
let data = await result.json()
console.log(data)
} catch (error) {
console.log(error)
}
}
foo()
await
yeild后面只能跟promise對象褥蚯,且結(jié)果值會傳出函數(shù)外還要再傳進來一次,await后面不僅可以跟promise對象還可以跟原生類型况增,更重要的是await的結(jié)果可以直接賦值赞庶,不需要傳來傳去。當(dāng)yield
兩個特性:產(chǎn)出值與自我阻塞澳骤,去掉了產(chǎn)出那就剩下await
了
async
async返回的是Promise歧强,所以可以在后面用then添加回調(diào)函數(shù)
總結(jié)
寫了generator之后async確實沒什么好寫的了。因為這個用起來確實太方便了为肮,將異步改為同步寫法摊册,流程明了,錯誤處理也很清晰颊艳。
總結(jié)
回調(diào)茅特,promise蟆沫,async,其實相當(dāng)于一步一步的封裝温治。promise通過固定的形式封裝了回調(diào),在then里添加回調(diào)函數(shù)在catch里捕捉錯誤戒悠,并且通過自身特性固化了異步操作的完成與否熬荆。相當(dāng)于好用版的回調(diào),async通過進行了進一步的封裝绸狐,將promise表達式跟在await后面等待解析卤恳,并將結(jié)果賦值下個語句處理,達成了用同步代碼的寫法組織異步代碼寒矿。所以如果有原生異步回調(diào)的方法或第三方庫突琳,想要方便的使用async就需要一步步封裝了。
下一篇寫分別封裝好三者符相,并在復(fù)雜的異步環(huán)境下比較拆融。