深入淺出 JS 異步處理技術(shù)方案
為什么要異步
'當(dāng)我們?cè)谛前涂速I(mǎi)咖啡時(shí),假設(shè)有100個(gè)人在排隊(duì),也許咖啡的下單只要10S,但是咖啡的制作到客人領(lǐng)取咖啡要1000S。如果在同步的場(chǎng)景下,第一個(gè)客人下單到領(lǐng)取完咖啡要1010S才能輪到下一個(gè)客人,這在效率(某些場(chǎng)景)上來(lái)說(shuō)會(huì)比較低下。如果我們異步處理這個(gè)流程,客人下單10S拿到憑證,客人就可以去做別的事情,并且10S后下一個(gè)客人可以繼續(xù)下單,并不阻礙流程。反而可以通過(guò)憑證,讓客人拿到自己的咖啡,也許時(shí)間上并不是第一個(gè)下單的客人先拿到。'
在網(wǎng)頁(yè)的世界里也是同樣的道理,不妨我們看看在執(zhí)行JS代碼的主線程里,如果遇到了AJAX請(qǐng)求,用戶(hù)事件等,如果不采用異步的方案,你會(huì)一直等待,等待第一個(gè)耗時(shí)的處理完成才能接上下一個(gè)JS代碼的執(zhí)行,于是界面就卡住了。
“也許有人會(huì)想,既然大家都說(shuō)現(xiàn)在網(wǎng)頁(yè)上性能損耗最大的屬于DOM節(jié)點(diǎn)的操作,把這些搞成異步,行不行?其實(shí)這會(huì)帶來(lái)一個(gè)不確定性問(wèn)題:既“成功”的狀態(tài)到底誰(shuí)先來(lái)的問(wèn)題。可以想象一下,如果我們?cè)诓僮鱀OM,既給節(jié)點(diǎn)添加內(nèi)容,也給節(jié)點(diǎn)刪除,那么到底以誰(shuí)為基準(zhǔn)呢?考慮到復(fù)雜性,也就可見(jiàn)一斑了。”
Event loop
雖然異步與event loop沒(méi)有太直接的關(guān)系,準(zhǔn)確的來(lái)講event loop 只是實(shí)現(xiàn)異步的一種機(jī)制。(了解為主)
“還是以上面咖啡館為例子,假定場(chǎng)景還是100人,這100人除了下單是與咖啡本身有關(guān)聯(lián)之外,其余的時(shí)間,比如看書(shū),玩游戲的等可以視為自己的執(zhí)行邏輯。如果用event loop來(lái)給它做一個(gè)簡(jiǎn)單的畫(huà)像,那么它就像:在與咖啡店店員溝通下單視為主執(zhí)行棧,咖啡的制作可以視為一個(gè)異步任務(wù),添加到一個(gè)任務(wù)隊(duì)列里,一直等帶100個(gè)人都下單完成,然后開(kāi)始讀取任務(wù)隊(duì)列中的異步任務(wù),事件名就是下單憑證,如果有對(duì)應(yīng)的handler,那么就執(zhí)行叫對(duì)應(yīng)的客人來(lái)領(lǐng)取咖啡。這個(gè)過(guò)程,是循環(huán)不斷的。假設(shè)沒(méi)有客人來(lái)下單的時(shí)候,也就是店員處于空閑時(shí)間(可能自己去搞點(diǎn)別的)。”
傳統(tǒng)的Callback
假定一個(gè)asyncFetchDataSource函數(shù)用于獲取遠(yuǎn)程數(shù)據(jù)源,可能有20S。
function asyncFetchDataSource(cb){ (… 獲取數(shù)據(jù), function(response){ typeof cb === ’function’ && cb(response) })}
這種形式的callback可以適用于簡(jiǎn)單場(chǎng)景,如果這里有一個(gè)更復(fù)雜的場(chǎng)景,比如獲取完數(shù)據(jù)源之后,依據(jù)id,獲取到某個(gè)數(shù)據(jù),在這某個(gè)數(shù)據(jù)中再依據(jù)id來(lái)更新某個(gè)列表,可以遇見(jiàn)的能看到代碼變成了:
asyncFetchDataSource(’’,function(data_a){ const { id_a } = data_a asyncFetchDataSource( id_a,function(data_b){ const { id_b } = data_b asyncFetchDataSource(id, function(data_c){ }) })})
如果有極端情況出現(xiàn),這里的callback就會(huì)變成無(wú)極限了。
Thunk函數(shù)
這是一種“傳名調(diào)用”的策略,表現(xiàn)的形式就是將參數(shù)放入一個(gè)臨時(shí)函數(shù),然后再將這個(gè)臨時(shí)函數(shù)傳入函數(shù)體內(nèi)。
function asyncFetchDataSource(url){ return function(callback){ fetch(url, callback) }}const dataSource = asyncFetchDataSource(’https://github.com/icepy’);dataSource(function(data){})
Promise
Promise正是想來(lái)處理這樣的異步編程,如果我們用Promise該如何處理一段Ajax?
function fetch(){ return new Promise(function(resolve,reject){ $.ajax({ url: ’xxx’, success:function(data){resolve(data) }, error:function(error){reject(error) } }) })}fetch().then(function(data){}).catch(function(error){})
Promise聲明周期:
進(jìn)行中(pending) 已經(jīng)完成(fulfilled) 拒絕(rejected)如同上面Ajax的例子,我們可以很好的包裝一個(gè)函數(shù),讓fetch函數(shù)返回一個(gè)Promise對(duì)象。在Promise構(gòu)造函數(shù)里,可以傳入一個(gè)callback,并且在這里完成主體邏輯的編寫(xiě)。唯一需要注意的是:Promise對(duì)象只能通過(guò)resolve和reject函數(shù)來(lái)返回,在外部使用then或catch來(lái)獲取。如果你直接拋出一個(gè)錯(cuò)誤(throw new Error(’error’)),catch也是可以正確的捕獲到的。
Promise其他的方法
Promise.all(當(dāng)所有在可迭代參數(shù)中的 promises 已完成,或者第一個(gè)傳遞的 promise(指 reject)失敗時(shí),返回 promise。)
var p1 = Promise.resolve(3);var p2 = 1337;var p3 = new Promise((resolve, reject) => { setTimeout(resolve, 100, 'foo');}); Promise.all([p1, p2, p3]).then(values => { console.log(values); // [3, 1337, 'foo'] });
Promise.race(返回一個(gè)新的 promise,參數(shù)iterable中只要有一個(gè)promise對(duì)象'完成(resolve)'或'失敗(reject)',新的promise就會(huì)立刻'完成(resolve)'或者'失敗(reject)',并獲得之前那個(gè)promise對(duì)象的返回值或者錯(cuò)誤原因。)
var p1 = new Promise(function(resolve, reject) { setTimeout(resolve, 500, 'one'); });var p2 = new Promise(function(resolve, reject) { setTimeout(resolve, 100, 'two'); });Promise.race([p1, p2]).then(function(value) { console.log(value); // 'two' // 兩個(gè)都完成,但 p2 更快});
有趣的是如果你使用ES6的class,你是可以去派生Promise的。
class MePromise extends Promise{ // 處理 ...}
Generator
Generator可以輔助我們完成很多復(fù)雜的任務(wù),而這些基礎(chǔ)知識(shí),又與iterator息息相關(guān),舉一個(gè)很簡(jiǎn)單的例子,相信有很多朋友,應(yīng)該使用過(guò)co這個(gè)異步編程的庫(kù),它就是用Generator來(lái)實(shí)現(xiàn),當(dāng)然它的設(shè)計(jì)會(huì)比例子要復(fù)雜的多,我們先來(lái)看一個(gè)co簡(jiǎn)單的用法:
import co from ’co’co(function* () { var result = yield Promise.resolve(true); return result;}).then(function (value) { console.log(value);}, function (err) { console.error(err.stack);});
相應(yīng)的,我們來(lái)實(shí)現(xiàn)一個(gè)簡(jiǎn)化的版本:
function co(task){ let _task = task() let resl = _task.next(); while(!resl.done){ console.log(resl); resl = _task.next(resl.value); }}function sayName(){ return { name: ’icepy’ }}function assign *(f){ console.log(f) let g = yield sayName() return Object.assign(g,{age:f});}co(function *(){ let info = yield *assign(18) console.log(info)})
雖然,這個(gè)例子中,還不能很好的看出來(lái)“異步”的場(chǎng)景,但是它很好的描述了Generator的使用方式。
從最開(kāi)始的定義中,已經(jīng)和大家說(shuō)明了,Generator最終返回的依然是一個(gè)迭代器對(duì)象,有了這個(gè)迭代器對(duì)象,當(dāng)你在處理某些場(chǎng)景時(shí),你可以通過(guò)yield來(lái)控制,流程的走向。通過(guò)co函數(shù),我們可以看出,先來(lái)執(zhí)行next方法,然后通過(guò)一個(gè)while循環(huán),來(lái)判斷done是否為true,如果為true則代表整個(gè)迭代過(guò)程的結(jié)束,于是,這里就可以退出循環(huán)了。在Generator中的返回值,可以通過(guò)給next方法傳遞參數(shù)的方式來(lái)實(shí)現(xiàn),也就是遇上第一個(gè)yield的返回值。
有邏輯,自然會(huì)存在錯(cuò)誤,在Generator捕獲錯(cuò)誤的時(shí)機(jī)與執(zhí)行throw方法的順序有關(guān)系,一個(gè)小例子:
let hu = function *(){ let g = yield 1; try { let j = yield 2; } catch(e){ console.log(e) } return 34}let _it = hu();console.log(_it.next())console.log(_it.next())console.log(_it.throw(new Error(’hu error’)))
當(dāng)我能捕獲到錯(cuò)誤的時(shí)機(jī)是允許完第二次的yield,這個(gè)時(shí)候就可以try了。
async await
async function createNewDoc() { let response = await db.post({}); // post a new doc return await db.get(response.id); // find by id}
https://tc39.github.io/ecmascript-asyncawait/
根據(jù)規(guī)范規(guī)定一個(gè)asnyc函數(shù)總是要返回一個(gè)Promise,從代碼直觀上來(lái)說(shuō),雖然簡(jiǎn)潔了,但是async await并未萬(wàn)能,它有很大的局限性,比如:
因?yàn)槭琼樞驁?zhí)行,假設(shè)有三個(gè)請(qǐng)求,那么這里并沒(méi)有很好的利用到異步帶來(lái)的止損(再包裝一個(gè)Promise.all) 如果要捕獲異常,需要去包try catch 缺少控制流程,比如progress(進(jìn)度)pause,resume等周期性的方法 沒(méi)有打斷的功能主流的異步處理方案
我喜歡用co,而且社區(qū)使用也很廣泛, https://github.com/tj/co 。
co(function* () { var result = yield Promise.resolve(true); return result;}).then(function (value) { console.log(value);}, function (err) { console.error(err.stack);});
babel polyfill 支持,在瀏覽器環(huán)境中使用異步解決方案
如果你想使用全的polyfiil,直接npm install --save babel-polyfill,然后在webpack里進(jìn)行配置即可。
module.exports = { entry: ['babel-polyfill', './app/js']};
當(dāng)然由于我目前的開(kāi)發(fā)基于的瀏覽器都比較高,所以我一般是挑選其中的:
https://github.com/facebook/regenerator/tree/master/packages/regenerator-runtime https://github.com/facebook/regenerator/tree/master/packages/regenerator-transform
如果你要使用async await 配置上 http://babeljs.io/docs/plugins/transform-async-to-generator/ 即可
Node.js 環(huán)境中使用異步解決方案
由于本人的node使用的LTS已經(jīng)是8.9.3版本了,所以大部分情況下已經(jīng)不再使用babel去進(jìn)行轉(zhuǎn)換,而是直接使用co這樣的庫(kù)。當(dāng)然co也不是萬(wàn)能,一定要根據(jù)業(yè)務(wù)場(chǎng)景,與其他異步處理的方式,配合中使用。
總結(jié) 相信未來(lái)的JS編程,只會(huì)越來(lái)越簡(jiǎn)單,不要拘泥于語(yǔ)法,語(yǔ)言上的特性,不妨多看一看“外面的世界“。
來(lái)自:http://www.iteye.com/news/32831
相關(guān)文章:
1. React實(shí)現(xiàn)一個(gè)倒計(jì)時(shí)hook組件實(shí)戰(zhàn)示例2. CSS Hack大全-教你如何區(qū)分出IE6-IE10、FireFox、Chrome、Opera3. XML解析錯(cuò)誤:未組織好 的解決辦法4. ASP基礎(chǔ)入門(mén)第三篇(ASP腳本基礎(chǔ))5. CSS3實(shí)現(xiàn)動(dòng)態(tài)翻牌效果 仿百度貼吧3D翻牌一次動(dòng)畫(huà)特效6. Xml簡(jiǎn)介_(kāi)動(dòng)力節(jié)點(diǎn)Java學(xué)院整理7. 三個(gè)不常見(jiàn)的 HTML5 實(shí)用新特性簡(jiǎn)介8. 小技巧處理div內(nèi)容溢出9. html清除浮動(dòng)的6種方法示例10. html中的form不提交(排除)某些input 原創(chuàng)
