從一個簡單的實例看JavaScript 的異步編程進化歷
很久沒有進行過創作了,也感覺到了自己的不足。這一篇文章是對於JavaScript 異步編程的一個整理希望自己更多的成為一個創造者,而不是只會看,會用,還需要深入理解到原理吧。 (雖然這個也很水..
例子如下:
我們有A, B, C, D 四個請求獲取數據的函數(函數自己實現)
C 依賴B 的結果,D 依賴ABC 的結果,最終輸出D 的結果
版本一
//偽代碼functionA(callbak){ajax(url,function(res){callbak(res);});}functionB(callbak){ajax(url,function(res){callbak(res);});}functionC(data,callback){ajax(url,data,function(res){callbak(res);});}functionD(data1,data2,data3,callback){ajax(url,{data1,data2,data3},function(res){callbak(res);});}A(function(resa){B(function(resb){C(resb,function(resc){D(resa,resb,resc,function(resd){console.log("this is D result:",resd);});});});});
emm...代碼還是能運行,但是寫法醜陋,回調地獄,如果還有請求依賴,得繼續回調嵌套性能太差,沒有考慮a 和b 實際上是可以並發的。
例子二
函數基礎實現如同例子一,但是考慮A,B 可以並發的
//偽代碼letresa=null;lettimer=null;A(res=>{resa=res;});B(resb=>{C(resb,resc=>{timer=setInterval(()=>{if(resa){D(resa,resb,resc,resd=>{console.log("this is D result:",resd);timer&&clearInterval(timer);});}},100);});});
考慮了A,B 的並發,使用setInterval 輪詢實現,並不一定實時。性能太差。
例子三
//偽代碼letcount=2;letresa=null;letresb=null;letresc=null;functiondone(){count--;if(count===0){D(resa,resb,resc,resd=>{console.log("this is D result:",resd);});}}A(res=>{resa=res;done();});B(datab=>{C(datab,datac=>{resb=datab;resc=datac;done();});});
使用計數器實現。性能沒什麼問題,但是封裝太差,寫法噁心
例子四
//實現並發functionparallel(tasks,callback){letcount=tasks.length;letall=[];tasks.forEach((fn,index)=>{fn(res=>{all[index]=res;count--;if(count===0){callback(all);}});});}//實現串行functionwaterfall(tasks,callback){letcount=tasks.length;functionloop(...args){lettask=tasks.shift();task.apply(null,args.concat([(...res)=>{count--;if(count===0){returncallback(res);}loop(...res);}]));}loop();}functionA(cb=()=>{}){setTimeout(()=>{cb("a");},2000);}functionB(cb=()=>{}){setTimeout(()=>{cb("b");},1000);}functionC(datab,cb=()=>{}){setTimeout(()=>{cb(datab,"c");},1000);}functionD(data,datab,datac,cb=()=>{}){cb("d");}parallel([A,cb=>{waterfall([B,C],(datab,datac)=>{cb(datab,datac);});}],data=>{const[resa,[resb,resc]]=data;D(resa,resb,resc,resd=>{console.log("this is D result:",resd);});});
模仿async.js 提煉出來了waterfall,parallel,兩個流程控制函數。還不錯。但是寫法還是麻煩,對於A,B,C 的實現有要求。得自己考慮好每一次callback 的值。
async.js是我認為在目前JavaScript callback的終極解決方案了(沒用過fib.js..
推薦查看github async.js源碼
waterfall 可以考慮使用函數式的形式實現:
functionpipe(...fnList){returnfunction(...args){constfn=fnList.reduceRight(function(a,b){returnfunction(...subArgs){returnb.apply(this,[].concat(subArgs,a));};});returnfn.apply(this,args);};}
例子五
functionA(){returnfetch("http://google.com");}functionB(){}functionC(){}functionD(){}Promise.all[(A(),B().then(b=>C(b)))].then(([resa,{resb,resc})=>{returnD(resa,resb,resc);}).then(resd=>{console.log("this is D result:",resd);});
使用Promise 來代替之前的callback。好評。用Promise.all 來控制並發,使用.then 串行請求,整體看起來非常舒服了,脫離了回調地獄
例子六
functionA(cb){setTimeout(()=>{cb("a");},2000);}functionB(cb){setTimeout(()=>{cb("b");},1000);}functionC(datab,cb){setTimeout(()=>{cb("c");},1000);}functionD(dataa,datab,datac,cb){setTimeout(()=>{cb("d");},1000);}functionthunk(fn){returnfunction(...args){returnfunction(callback){fn.call(this,...args,callback);};};}functionscheduler(fn){vargen=fn();functionnext(data){varresult=gen.next(data);if(result.done)return;//如果沒結束就繼續執行result.value(next);}next();}// generator實際代碼function*generatorTask(){constresa=yieldthunk(A)();constresb=yieldthunk(B)();constresc=yieldthunk(C)(resb);constresd=yieldthunk(D)(resa,resb,resc);console.log("this is D result:",resd);returnnull;}scheduler(generatorTask);
使用generator + callback 來控制流程順序,還是同步寫法,看起來還是挺牛逼的。但是generator 不會自動執行,需要自己手動寫一個執行器,並且依賴於thunk 函數。麻煩!等等。 。又全變成了串行?垃圾
例子七
functionA(){returnnewPromise(r=>setTimeout(()=>{r("a");},2000));}functionB(){returnnewPromise(r=>setTimeout(()=>{r("b");},1000));}functionC(datab){returnnewPromise(r=>setTimeout(()=>{r("c");},1000));}functionD(dataa,datab,datac){returnnewPromise(r=>setTimeout(()=>{r("d");},1000));}functionscheduler(fn){vargen=fn();functionnext(data){varresult=gen.next(data);if(result.done)return;//如果沒結束就繼續執行result.value.then(next);}next();}// generator實際代碼function*generatorTask(){const[resa,{resb,resc}]=yieldPromise.all([A(),B().then(resb=>C(resb).then(resc=>({resb,resc})))]);constresd=yieldD(resa,resb,resc);console.log("this is D result:",resa,resb,resc,resd);returnresd;}scheduler(generatorTask);
拋棄了thunk 函數,修改了一下A,B,C,D。的實現以及generator 執行函數scheduler。結合了Promise 重新實現了並發和串行。再等等? ?好麻煩啊。 。然後並發好像和generator 沒什麼關係吧。果然還是Promise 大法好。
關於generator的自動執行建議直接看github tj/co的源碼
例子八
functionA(){returnfetch("http://google.com");}// ...B,C,DasyncfunctionasyncTask(){constresa=awaitA();constresb=awaitB();constresc=awaitC(resb);constresd=awaitD(resa,resb,resc);returnresd;}asyncTask().then(resd=>{console.log("this is D result:",resd);});
使用Promise 結合async/await 的形式,看起來非常簡潔。也不用自己寫執行器了,舒服。但是和上面有幾個版本出現了一樣的問題,沒有考慮並發的情況,導致性能下降
例子九,終極方案?
// ...B,C,DasyncfunctionasyncBC(){constresb=awaitB();constresc=awaitc(resb);return{resb,resc};}asyncfunctionasyncTask(){// const [resa,{resb,resc}] = await Promise.all([A(), B().then(resb=>C(resb)]);const[resa,{resb,resc}]=awaitPromise.all([A(),asyncBC()]);constresd=awaitD(resa,resb,resc);returnresd;}asyncTask().then(resd=>{console.log("this is D result:",resd);});
使用Promise.all 結合async/await 的形式,考慮了並發和串行,寫法簡潔。應該算是目前的終極方案了。 async/await 作為generator 語法糖還是非常的甜的。
例子十使用RxJs
import { defer, forkJoin } from "rxjs"; import { mergeMap, map } from "rxjs/operators"; function A() { return fetch("https://cnodejs.org/api/v1/topics").then(res => res.json()); } function B() { return fetch("https://cnodejs.org/api/v1/topics").then(res => res.json()); } function C() { return fetch("https://cnodejs.org/api/v1/topics").then(res => res.json()); } function D(...args) { return fetch("https://cnodejs.org/api/v1/topics") .then(res => res.json()) .then(res => [...args, res]); } // A, B, C, D 函数必须返回Promise // 使用defer 产生一个Observable const A$ = defer(() => A()); // pipe 类型Promise 链中的then const BC$ = defer(() => B()).pipe( // mergeMap 映射成promise 并发出结果mergeMap(resB => { // 使用map 产生新值return defer(() => C(resB)).pipe(map(resC => [resB, resC])); }) ); // forkJoin 类似Promise.all 并发执行多个Observable forkJoin(A$, BC$) .pipe(mergeMap(([resa, [resb, resc]]) => D(resa, resb, resc))) .subscribe(resd => { console.log("this is D result:", resd); // <------- fnD 返回的结果});
使用rxjs 來構建流式的請求過程。結構還是非常清晰的,但是相對繁瑣,概念也比原生的Promise 和await 要多
不過rxjs 操作符巨多,掌握之後,可以做更多的事情
結語:
從上面幾個例子我們可以窺探到JavaScript 對於異步編程體驗的一個非常大的進步。
但是同時我們其實可以看到不論是generator 還是async/await。其實更多的是基於Promise 之上的一些語法簡化。沒有從callback 過渡到Promise 的時候那種真正心靈上的愉悅。
感謝@墨水之前在內部分享提供的demo 版本。