再谈JavaScript异步编程
随着前端的发展,异步这个词真是越来越常见了。假设我们现在有这么一个异步任务:
向服务器发起数次请求,每次请求的结果作为下次请求的参数。
来看看我们都有哪些处理方法:
Callbacks
最先想到也是最常用的便是回调函数了,我们来进行简单的封装:
letmakeAjaxCall=(url,cb)=>{ //dosomeajax //callbackwithresult } makeAjaxCall('http://url1',(result)=>{ result=JSON.parse(result) })
嗯,看起来还不错!但是当我们尝试嵌套多个任务时,代码看起来会是这样的:
makeAjaxCall('http://url1',(result)=>{ result=JSON.parse(result) makeAjaxCall(`http://url2?q=${result.query}`,(result)=>{ result=JSON.parse(result) makeAjaxCall(`http://url3?q=${result.query}`,(result)=>{ //... }) }) })
天哪!快让那堆})见鬼去吧!
于是,我们想尝试借助JavaScript事件模型:
1、Pub/Sub
在DOM事件的处理中,Pub/Sub是一种很常见的机制,比如我们要为元素加上事件监听:
elem.addEventListener(type,(evt)=>{ //handler })
所以我们是不是也可以构造一个类似的模型来处理异步任务呢?
首先是要构建一个分发中心,并添加on/emit方法:
letPubSub={ events:{}, on(type,handler){ letevents=this.events events[type]=events[type]||[] events[type].push(handler) }, emit(type,...datas){ letevents=this.events if(!events[type]){ return } events[type].forEach((handler)=>handler(...datas)) } }
然后我们便可以这样使用:
consturls=[ 'http://url1', 'http://url2', 'http://url3' ] letmakeAjaxCall=(url)=>{ //dosomeajax PubSub.emit('ajaxEnd',result) } letsubscribe=(urls)=>{ letindex=0 PubSub.on('ajaxEnd',(result)=>{ result=JSON.parse(result) if(urls[++index]){ makeAjaxCall(`${urls[index]}?q=${result.query}`) } }) makeAjaxCall(urls[0]) }
比起回调函数好像没有什么革命性的改变,但是这样做的好处是:我们可以将请求和处理函数放在不同的模块中,减少耦合。
2、Promise
真正带来革命性改变的是Promise规范。借助Promise,我们可以这样完成异步任务:
letmakeAjaxCall=(url)=>{ returnnewPromise((resolve,reject)=>{ //dosomeajax resolve(result) }) } makeAjaxCall('http://url1') .then(JSON.parse) .then((result)=>makeAjaxCall(`http://url2?q=${result.query}`)) .then(JSON.parse) .then((result)=>makeAjaxCall(`http://url3?q=${result.query}`))
好棒!写起来像同步处理的函数一样!
别着急,少年。我们还有更棒的:
3、Generators
ES6的另外一个大杀器便是Generators[2]。在一个generatorfunction中,我们可以通过yield语句来中断函数的执行,并在函数外部通过next方法来迭代语句,更重要的是我们可以通过next方法向函数内部注入数据,动态改变函数的行为。比如:
function*gen(){ leta=yield1 letb=yielda*2 returnb } letit=gen() it.next()//output:{value:1,done:false} it.next(10)//a=10,output:{value:20,done:false} it.next(100)//b=100,output:{value:100,done:true}
通过generator将我们之前的makeAjaxCall函数进行封装:
letmakeAjaxCall=(url)=>{ //dosomeajax iterator.next(result) } function*requests(){ letresult=yieldmakeAjaxCall('http://url1') result=JSON.parse(result) result=yieldmakeAjaxCall(`http://url2?q=${result.query}`) result=JSON.parse(result) result=yieldmakeAjaxCall(`http://url3?q=${result.query}`) } letiterator=requests() iterator.next()//geteverythingstart
哦!看起来逻辑很清楚的样子,但是每次都得从外部注入iterator感觉好不舒服……
别急,我们让Promise和Generator混合一下,看会产出什么黑魔法:
letmakeAjaxCall=(url)=>{ returnnewPromise((resolve,reject)=>{ //dosomeajax resolve(result) }) } letrunGen=(gen)=>{ letit=gen() letcontinuer=(value,err)=>{ letret try{ ret=err?it.throw(err):it.next(value) }catch(e){ returnPromise.reject(e) } if(ret.done){ returnret.value } returnPromise .resolve(ret.value) .then(continuer) .catch((e)=>continuer(null,e)) } returncontinuer() } function*requests(){ letresult=yieldmakeAjaxCall('http://url1') result=JSON.parse(result) result=yieldmakeAjaxCall(`http://url2?q=${result.query}`) result=JSON.parse(result) result=yieldmakeAjaxCall(`http://url3?q=${result.query}`) } runGen(requests)
runGen函数看起来像个自动机一样,好厉害!
实际上,这个runGen的方法是对ECMAScript7asyncfunction的一个实现:
4、asyncfunction
ES7中,引入了一个更自然的特性asyncfunction[3]。利用asyncfunction我们可以这样完成任务:
letmakeAjaxCall=(url)=>{ returnnewPromise((resolve,reject)=>{ //dosomeajax resolve(result) }) } ;(async()=>{ letresult=awaitmakeAjaxCall('http://url1') result=JSON.parse(result) result=awaitmakeAjaxCall(`http://url2?q=${result.query}`) result=JSON.parse(result) result=awaitmakeAjaxCall(`http://url3?q=${result.query}`) })()
就像我们在上文把Promise和Generator结合在一起时一样,await关键字后同样接受一个Promise。在asyncfunction中,只有在await后的语句完成后剩下的语句才会被执行,整个过程就像我们用runGen函数封装Generator一样。
以上就是本文总结的几种JavaScript异步编程模式,希望对大家的学习有所帮助。