从大学到工作,写了差不多4年的javascript了,经常听到人们谈论起js时,总离不开几个常规吐槽:”写一个类真是太痛苦了,连继承的函数都得自己写”,”我靠,怎么这个地方的this指的不是当前对象啊??”,”回调写起来简直就是噩梦”。前几个问题要么在新的ES规范中得到了解决,要么是需要自己认真了解js的相关特性,不过最后关于回调的吐槽是每个jser确实难以忍受的噩梦。
回调地狱:
什么是回调地狱?这个词可能是js的专属了,我们来看一个例子:
1 |
|
这样的情况想必大家都是遇到不少了的,回调里面走回调,然后继续回调,如果还遇上一些复杂的判断或者其他的什么运算,能让这个回调过程惨不忍睹,一旦出现错误,或者有新的需求要改代码,由于其耦合度太高,更是要大动刀子。所以,在js中便有了回调地狱这样的说法。
这里真的要赞扬一下ES6的出现,为js做出了太多给力的改进,很多之前反人类的设计在ES6里面都得到了很棒的修正,这里我们就来看看promise与async函数带来的便捷。
Promise
先来看看阮一峰的《ECMAScript6入门》中对promise的介绍吧:
Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了Promise对象。
所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。
首先,promise是一个对象,他为异步操作提供了一个容器,可以在其中进行相关的异步操作。
他有三个状态:pending(进行中),fulfilled(已成功,可以进行下一步操作),rejected(已失败)。通过异步操作的结果来确定最后的状态是什么,并且,一旦状态确定,其他的操作是无法改变其状态的。
话不多少,先上代码:
1 |
|
把setTimieout当做一个异步操作,当返回相关结果时,将promise的状态设置为resolve,并将相关结果传入resolve函数。然后触发myPromise的then()方法,最终输出相关的结果。
这里要注意一点,new Promise()里面的函数是在你声明完成之后立即执行的。
像下面的:
1 |
|
虽然你没有执行myPromise函数,但是控制台还是输出了run immediately字样。
这里再看看then方法,它有两个参数:
1 |
|
第一个resolve是函数正确执行时候的返回,第二个reject是回调失败后的返回。就像我们发起一个ajax请求,总有成功和失败的情况一样,以前我们需要自己来定义失败后执行的流程,而promise已经为我们准备好了失败后的方法。then函数里面如果有返回对象,那么这个对象一定是Promise对象。
看看一个真正的ajax请求吧:
1 |
|
客户端发起请求,当响应结果时调用result函数,根据结果将promise的状态设置为resolve或者reject,然后再触发then函数来输出最后的结果。
so,promise的大致用法就是这样,看着并不是很难。接下来我们要来解决下之前我们遇到的回调地狱问题。首先我们得把之前的request1… … request4用promise的写法来写,就像我们之前的getAjax函数一样:
1 |
|
这样看起来是不是比之前无限嵌套的回调函数要舒服多了呢。
这里要注意,如果要形成链式调用,那么return是必不可少的。他会将返回值作为下一个resolve函数的参数。
我们在函数的末尾加了一个catch函数,它能捕捉到之前then中所抛出的错误,包括reject中的错误。值得注意的是,如果抛出了错误,整个then的调用链就会在错误抛出的函数中被终止,不会再接下里执行了,也就是,如果request2中的异步结果被reject,那么后续的request3,request4都不会执行。
关于promise最后我们再聊一下Promise的静态方法。
- Promise.resolve()
当需要将现有对象转换为Promise对象时,我们就可以用Promise.resolve这个方法了。
1 |
|
这里的Promise.resolve()可以接受一个参数,如果该参数是一个非对象,则返回一个新的Promise对象,状态为resolve。
1 |
|
如果这个参数是一个包含了then方法的对象,立即执行这个对象的then方法,将其转换为resolve状态。
- Promise.all()
这个方法是将多个Promise实例包装成为一个新的Promise实例。
1 |
|
只有当所有的Promise都返回resolve的时候,Promise.all才会返回resolve,然后其结果会被返回为一个数组。如果有一个Promise状态是reject,那么Promise.all的状态就会变成reject,最先返回reject的返回值,传递给Promise.all的回调函数。
promise还有很多不错的特性,我这里推荐大家去看阮一峰的《ECMAScript 6入门》。大神讲的非常详细,我这里只简单说下上手用法。
async
说完了promise,我们对ES6中新的回调写法算是有了一个初步了解。接下来我们得继续聊一聊和回调非常相关的,js的另一个特性:异步。
都知道js是一门单线程的语言,所以当需要处理多任务的时候,就离不开异步这个东西了。试想一下,我们需要操作某个文件,首先执行了打开文件的指令,可能这个文件有点大,需要5秒的时间来打开,那么,如果不做异步处理,用户就得等着5秒过去,才能执行下一步操作,如果下一步操作又需要5秒……由此可见,异步的使用,在js开发中是一个非常重要的技术项。
那么我们是怎么处理异步任务的呢。
方法也是简单粗暴:回调
回到之前的例子
1 |
|
这里有两个函数,readFile是一个非常消耗性能的函数,可能需要很久的时间,而processFile函数需要等待readFile所带回的结果才能执行,那么这就非常尴尬了。为什么我们要白白的去浪费时间去等待上一个任务的执行呢??
于是,
1 |
|
聪明的js开发者想到了用回调的方法,我们用setTimeout来让他在主线程(不是真的线程)执行后开始执行,然后通过回调,来获取耗时操作所返回的结果。
这样确实可以解决一些问题,但是回调这种搞法稍不注意就会变成上面的地狱模式,虽然有了Promise,一大长条的then函数让人也看着不是很舒服吧。
ok,于是聪明的js开发者又想到了新的方法,我们可以用发布订阅模式呀。我们在processFile中做好订阅监听,同样是同样利用setTimeout,把最后返回的结果发布出去不就可以了吗?
1 | // consume a lot of time |
(这里的dispatch和registEvent,需要自己去实现。)
你说都到了ES6 ES7 ES8的年代了,还没有个稍微能让人看着舒服得异步处理的解决方法吗?
async与await出现了。
其实在async之前还有generator这个函数,只是在目前为止,大家都倾向于将async函数作为js异步的最终解决方案。所以这里我就不再展开说generator函数了,这里有详细的关于generator的介绍。
但是,我们必须知道,async和awiat是以generator为基础封装的,只是为使用generator提供了更便捷的方法,是generator的语法糖,所以,有时间去了解下generator函数也是很有必要的。
好了话不多少,看看关于async这个家伙该怎么用。
回到之前的那个读取大文件的例子,如果我们用async来写:
1 |
|
这里我们首先将readFile函数改造成一个返回值为Promise的异步函数,然后在之后声明了 async function main。
function之前加上了async关键字,表明这个函数为一个异步函数,这个是必须的,只有异步函数内部,才能使用await关键字来获取返回值。
这里main函数在执行的时候,会等待readFile()所带回来的返回值,也就是说,当执行到这里时,程序的执行权交给了readFile方法,当readFile返回结果后,才会进行下一步操作。await就是等待的意思,await后面所跟的函数,是一个Promise对象(如果不是Promise对象,会直接调用Promise.resolve方法转为一个立即执行的Promise)。
来看看一个可以执行的案例,和上面的结构基本一致,最后会在两秒后输出hello world。
1 |
|
多个异步函数:
1 |
|
进一步说,async函数完全可以看作多个异步操作,包装成的一个 Promise 对象,而await命令就是内部then命令的语法糖。——《ECMASCRIPT 6 入门》
在这里,了解过generator的同学应该可以看出一些相似点了。
async就和generator函数中的*相似,而await和yield又是相似的。只是关于gererator的执行需要一个单独的执行函数,这里async方法在此给我们做了精简,使用起来更加方便了一些。
最后说下async的错误处理,第一种方式,是在执行async函数时,在函数的最后catch错误:
1 |
|
这种方式比较便捷,能处理所有的async函数中,await所抛出的错误,包括promise中的reject状态。但是这样也是有局限性,如果我的async中有多个异步,那么只要有一个异步抛出了错误或者被reject掉,那么整个async中的函数都不能进行了,直接跳到了最后的catch中,而很多时候,我们希望哪怕有两个异步是失败了,但是还是可以继续执行我需要的功能。那么我么可以这么写:
1 |
|
我们在await函数之前使用try…catch语句,让错误直接在async中就得到处理,这样,不论前一个await返回什么样的结果,我们最后的await都能得以返回,不会被终止。
最后,说下关于两个await同时进行的写法。写代码总是有先后顺序的,而await又是对先后顺序非常敏感的(需要等待前一个await执行完),那么,如果我们需要将两个await同时进行该怎么做呢?
还记得我们之前的Promise.all方法吗,这里同样可以发挥作用:
1 |
|
最后
关于回调与异步,说到这里也差不多了。文章给出了ES6中promise与async函数的基本使用,如果之前从没使用过,那么现在应该有个基本的概念了。其中还有很多细节的方法与技巧,我这里就不多说了,大家如果有需要,自然会去查找更高阶的教程。
- 参考资料:
《ECMASCript 6入门》 – 作者:阮一峰