构建一个应用程序总是会面对异步调用,不论是在 Web 前端界面,还是 Node.js 服务端都是如此,JavaScript 里面处理异步调用一直是非常恶心的一件事情。以前只能通过回调函数,后来渐渐又演化出来很多方案,最后 Promise 以简单、易用、兼容性好取胜,但是仍然有非常多的问题。其实 JavaScript 一直想在语言层面彻底解决这个问题,在 ES6 中就已经支持原生的 Promise,还引入了 Generator 函数,终于在 ES7 中决定支持 async 和 await。
基本语法
async/await 究竟是怎么解决异步调用的写法呢?简单来说,就是将异步操作用同步的写法来写。先来看下最基本的语法(ES7 代码片段):
[JavaScript]
纯文本查看
复制代码
01 02 03 04 05 06 07 08 09 10 11 12 13 14 | const f = () => { return new Promise((resolve, reject) => { setTimeout(() => { resolve(123); }, 2000); });};const testAsync = async () => { const t = await f(); console.log(t);};testAsync(); |
首先定义了一个函数 f,这个函数返回一个 Promise,并且会延时 2 秒,resolve 并且传入值 123。testAsync 函数在定义时使用了关键字 async,然后函数体中配合使用了 await,最后执行 testAsync。整个程序会在 2 秒后输出 123,也就是说 testAsync 中常量 t 取得了 f 中 resolve 的值,并且通过 await 阻塞了后面代码的执行,直到 f 这个异步函数执行完。
对比 Promise
仅仅是一个简单的调用,就已经能够看出来 async/await 的强大,写码时可以非常优雅地处理异步函数,彻底告别回调恶梦和无数的 then 方法。我们再来看下与 Promise 的对比,同样的代码,如果完全使用 Promise 会有什么问题呢?
[JavaScript]
纯文本查看
复制代码
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 | const f = () => { return new Promise((resolve, reject) => { setTimeout(() => { resolve(123); }, 2000); });};const testAsync = () => { f().then((t) => { console.log(t); });};testAsync(); |
从代码片段中不难看出 Promise 没有解决好的事情,比如要有很多的 then 方法,整块代码会充满 Promise 的方法,而不是业务逻辑本身,而且每一个 then 方法内部是一个独立的作用域,要是想共享数据,就要将部分数据暴露在最外层,在 then 内部赋值一次。虽然如此,Promise 对于异步操作的封装还是非常不错的,所以 async/await 是基于 Promise 的,await 后面是要接收一个 Promise 实例。
异常处理
通过使用 async/await,我们就可以配合 try/catch 来捕获异步操作过程中的问题,包括 Promise 中 reject 的数据。
[JavaScript]
纯文本查看
复制代码
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 | const f = () => { return new Promise((resolve, reject) => { setTimeout(() => { reject(234); }, 2000); });};const testAsync = async () => { try { const t = await f(); console.log(t); } catch (err) { console.log(err); }};testAsync(); |
代码片段中将
f
方法中的
resolve
改为
reject
,在
testAsync
中,通过
catch
可以捕获到
reject
的数据,输出 err 的值为 234。
try/catch
使用时也要注意范围和层级。如果
try
范围内包含多个
await
,那么
catch
会返回第一个
reject
的值或错误。
[JavaScript]
纯文本查看
复制代码
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | const f1 = () => { return new Promise((resolve, reject) => { setTimeout(() => { reject(111); }, 2000); });};const f2 = () => { return new Promise((resolve, reject) => { setTimeout(() => { reject(222); }, 3000); });};const testAsync = async () => { try { const t1 = await f1(); console.log(t1); const t2 = await f2(); console.log(t2); } catch (err) { console.log(err); }};testAsync(); |
如代码片段所示,testAsync 函数体中 try 有两个 await 函数,而且都分别 reject,那么 catch 中仅会触发 f1 的 reject,输出的 err 值是 111。
开始使用
无论是 Web 前端还是 Node.js 服务端,都可以通过预编译的手段实现使用 ES6 和 ES7 来写代码,目前最流行的方案是通过 [color=var(--link-color)]Babel
将使用 ES7、ES6 写的代码编译为 E6 或 ES5 的代码来执行。Node.js 服务端配置
服务端使用 Babel,最简单的方式是通过 require hook。
首先安装 Babel:
$
npm install babel-core --save
安装 async/await 支持:
$
npm install babel-preset-stage-3 --save
在服务端代码的根目录中配置 .babelrc 文件,内容为:
{
"presets"
: [
"stage-3"
]}
在顶层代码文件(server.js 或 app.js 等)中引入 Babel 模块:
require
(
"babel-core/register"
);
在这句后面引入的模块,都将会自动通过 babel 编译,但当前文件不会被 babel 编译。另外,需要注意 Node.js 的版本,如果是 4.0 以上的版本则默认支持绝大部分 ES6,可以直接启动。但是如果是 0.12 左右的版本,就需要通过 node -harmony 来启动才能够支持。因为 stage-3 模式,Babel 不会编译基本的 ES6 代码,环境既然支持又何必要编译为 ES5?这样做也是为了提高性能和编译效率。
配置 Web 前端构建
可以通过增加 Gulp 的预编译 task 来支持。
首先安装 gulp-babel 插件:
$
npm install gulp-babel --save-dev
然后编写配置:
[JavaScript]
纯文本查看
复制代码
1 2 3 4 5 6 7 8 | var gulp = require('gulp');var babel = require('gulp-babel');gulp.task('babel', function() { return gulp.src('src/app.js') .pipe(babel()) .pipe(gulp.dest('dist'));}); |
除了 Gulp-babel 插件,也可以使用官方的 Babel-loader 结合 Webpack 或 Browserify 使用。
要注意的是,虽然官方也有纯浏览器版本的 Babel.js,但是浏览器限制非常多,而且对客户端性能影响也较大,不推荐使用。
文章转载至:https://blog.leancloud.cn/3910/