V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
推荐关注
Meteor
JSLint - a JavaScript code quality tool
jsFiddle
D3.js
WebStorm
推荐书目
JavaScript 权威指南第 5 版
Closure: The Definitive Guide
LeanCloudRRY
V2EX  ›  JavaScript

「大概也许是」目前最好的 JavaScript 异步方案 async/await

  •  
  •   LeanCloudRRY · 2015-11-23 11:00:40 +08:00 · 9322 次点击
    这是一个创建于 3323 天前的主题,其中的信息可能已经有所发展或是发生改变。

    构建一个应用程序总是会面对异步调用,不论是在 Web 前端界面,还是 Node.js 服务端都是如此, JavaScript 里面处理异步调用一直是非常恶心的一件事情。以前只能通过回调函数,后来渐渐又演化出来很多方案,最后 Promise 以简单、易用、兼容性好取胜,但是仍然有非常多的问题。其实 JavaScript 一直想在语言层面彻底解决这个问题,在 ES6 中就已经支持原生的 Promise ,还引入了 Generator 函数,终于在 ES7 中决定支持 async 和 await 。

    基本语法

    async/await 究竟是怎么解决异步调用的写法呢?简单来说,就是将异步操作用同步的写法来写。先来看下最基本的语法( ES7 代码片段):

    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 取得了 fresolve 的值,并且通过 await 阻塞了后面代码的执行,直到 f 这个异步函数执行完。

    对比 Promise

    仅仅是一个简单的调用,就已经能够看出来 async/await 的强大,写码时可以非常优雅地处理异步函数,彻底告别回调恶梦和无数的 then 方法。我们再来看下与 Promise 的对比,同样的代码,如果完全使用 Promise 会有什么问题呢?

    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 实例。

    对比 RxJS

    RxJS 也是非常有意思的东西,用来处理异步操作,它更能处理基于流的数据操作。举个例子,比如在 Angular2 中 http 请求返回的就是一个 RxJS 构造的 Observable Object ,我们就可以这样做:

    $http.get(url)
      .map(function(value) {
        return value + 1;
      })
      .filter(function(value) {
        return value !== null;
      })
      .forEach(function(value) {
        console.log(value);
      })
      .subscribe(function(value) {
        console.log('do something.');
      }, function(err) {
        console.log(err);
      });
    

    如果是 ES6 代码可以进一步简洁:

    $http.get(url) 
      .map(value => value + 1) 
      .filter(value => value !== null) 
      .forEach(value => console.log(value)) 
      .subscribe((value) => { 
        console.log('do something.'); 
      }, (err) => { 
        console.log(err); 
      });
    

    可以看出 RxJS 对于这类数据可以做一种类似流式的处理,也是非常优雅,而且 RxJS 强大之处在于你还可以对数据做取消、监听、节流等等的操作,这里不一一举例了,感兴趣的话可以去看下 RxJS 的 API 。

    这里要说明一下的就是 RxJS 和 async/await 一起用也是可以的, Observable Object 中有 toPromise 方法,可以返回一个 Promise Object ,同样可以结合 await 使用。当然你也可以只使用 async/await 配合 underscore 或者其他库,也能实现很优雅的效果。总之, RxJS 与 async/await 不冲突。

    异常处理

    通过使用 async/await ,我们就可以配合 try/catch 来捕获异步操作过程中的问题,包括 Promise 中 reject 的数据。

    const f = () => {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          reject(234);
        }, 2000);
      });
    };
    
    const testAsync = () => {
      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 的值或错误。

    const f1 = () => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
          reject(111);
        }, 2000);
      });
    };
    
    const f2 = () => {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          reject(222);
        }, 3000);
      });
    };
    
    const testAsync = () => {
      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 中仅会触发 f1reject,输出的 err 值是 111 。

    开始使用

    无论是 Web 前端还是 Node.js 服务端,都可以通过预编译的手段实现使用 ES6 和 ES7 来写代码,目前最流行的方案是通过 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 — harmory 来启动才能够支持。因为 stage-3 模式, Babel 不会编译基本的 ES6 代码,环境既然支持又何必要编译为 ES5 ?这样做也是为了提高性能和编译效率。

    配置 Web 前端构建

    可以通过增加 Gulp 的预编译 task 来支持。

    首先安装 gulp-babel 插件:

    $ npm install gulp-babel --save-dev
    

    然后编写配置:

    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 ,但是浏览器限制非常多,而且对客户端性能影响也较大,不推荐使用。

    LeanEngine Full Stack

    LeanEngine (云引擎)是 LeanCloud 推出的服务器端运行环境,支持 Node.js 和 Python 环境,功能强大而且目前免费,结合 LeanCloud JavaScript SDK ,使原本复杂的开发工作变得简单高效。目前也支持 Redis 和海外节点,轻松满足你的业务需求。


    LeanCloud 使用自已的服务编写出了很多的应用和 Web 产品。为了方便各位开发者基于 LeanEngine 来开发应用, LeanCloud 整理了目前开发 Web 端产品的技术栈,并结合 LeanEngine 特点,推出了一套完整实用的技术解决方案:LeanEngine-Full-Stack,它已经配置了 Babel ,可以在 LeanEngine 中结合 JavaScript SDK 使用 async/await 处理异步操作。所以,还等什么?快来下载编写新项目吧。

    Enjoy!

    结语

    LeanCloud 希望能够通过构建最简单易用的技术产品,帮助各位开发者和创业者加速产品开发,尽可能地节约资源成本、时间成本和机会成本,希望本文能够帮助到你。有什么问题,可以在 LeanEngine-Full-Stack @GitHub 仓库 中提交 issue ,或者直接去 LeanCloud 社区 提问。

    [参考资料] EcmaScript async/await 详细规范

    15 条回复    2015-11-24 14:15:27 +08:00
    shuson
        1
    shuson  
       2015-11-23 11:05:31 +08:00
    消灭 callback hell ,然后消灭 then chain hell 。
    当我们谈论 javascript 的时候,我在各种 hell ,你却在 heaven 。
    serenader
        2
    serenader  
       2015-11-23 11:22:44 +08:00
    > 从代码片段中不难看出 Promise 没有解决好的事情,比如要有很多的 then 方法,整块代码会充满 Promise 的方法,而不是业务逻辑本身,而且每一个 then 方法内部是一个独立的作用域,要是想共享数据,就要将部分数据暴露在最外层,在 then 内部赋值一次。

    确实如此。每次写 Promise 的时候感觉还是不太优雅。

    感谢楼主的分享,又学到了新知识。
    cheneydog
        3
    cheneydog  
       2015-11-23 11:41:41 +08:00
    leancloud 能用来做普通的网站么?有 80 端口么?能绑定域名么?
    iamppz
        4
    iamppz  
       2015-11-23 12:55:31 +08:00
    Rx 主要做的还是对事件流的过滤吧,感觉 subscribe 和 then 区别不是太大
    RoshanWu
        5
    RoshanWu  
       2015-11-23 13:02:24 +08:00
    昨天刚用 async/await 重构了一个项目,不过我用的不是 Babel ,而是 TypeScript ,感觉更方便一点: https://github.com/roshanca/gitlab-autochangelog
    oott123
        6
    oott123  
       2015-11-23 13:23:05 +08:00
    @serenader 其实这个问题可以看 /t/198533 的讨论,有几种方法还是很顺手的。
    minsheng
        7
    minsheng  
       2015-11-23 14:09:05 +08:00
    最好的方法当然是用 PureScript 的 do-notation 。这个问题二十年前就被解决了,简单的 Continuation Monad 而已。

    PureScript 的特性多了去了,比如说强类型系统,比 TypeScript 的还方便。比如说 algebraic data types ,可以方便的定义枚举:
    data Color = Red | Green | Blue

    也可以定义 higher kinded types ,也就是 Swift 里枚举的那个 associated value :
    data Maybe a = Just a | Nothing
    定义了一个 optional 的类型。

    方便的 pattern matching ,无论是定义简单的函数:
    fac :: Number -> Number
    fac 1 = 1
    fac n = fac (n - 1) * n
    或者处理上面的 Maybe :
    nextStep :: Maybe String -> Maybe String
    nextStep (Just x) = performOperation x
    nextStep _ = Nothing

    当然,几乎所有的类型都可以省略:
    let fib 0 = 1; fib 1 = 1; fib n = fib (n-1) + fib (n-2) in fib 10

    类似泛型的 type class :
    class ToJson a where
    toJson :: a -> String

    然后只要针对每一种类型定义相关的函数就可以直接使用 toJson 函数了,多么模块化!当然,可以专门针对数组 [a] 针对一个 toJson 函数,只要 SomeType 定义了 toJson ,数组 [SomeType] 也就都可以用 toJson 了。

    还支持两个参数的 type class
    class Convertible a b where
    convert :: a -> b

    然后就可以类型安全的转换数据了。

    担心 Maybe 穿起来特别复杂?有一串操作,每一步都可能失败?没关系,我们有 do notation
    operations :: Number -> Maybe Number
    operations x = do x1 <- op1 x; x2 <- op2 x1; x3 <- op3 x2; return x3

    对比原来的写法:
    operations x = case op1 x of
    Nothing -> Nothing
    Just x1 -> case op2 x1 of
    Nothing -> Nothing
    Just x2 -> case op3 x2 of
    Nothing -> Nothing
    Just x3 -> Just x3

    有没有感觉这个看起来很像 callback hell ?对了!这就是我说的 do notation 如何轻松的解决 callback hell ,只需要定义一个专门的 Continuation Monad 就可以用 do notation 解决 callback hell 。而且这玩意具有可扩展性,而不像楼主提到的 await async 处理的问题比较死。 Node.js 一个常见的 pattern 就是用第一个参数传 error ,而在这里稍微修改一下 Continuation Monad 的定义就可以写出:
    operations x = do x1 <- mayFailOp1 x; …

    只要其中挂了一个整个操作自动中断。

    Do-notation 和 monad 的各种奇技淫巧各大社区已经研究了很多年,无限的可能性。

    然而需要和 JSON 打交道的人可能要吐槽强类型系统的不方便,但不用担心 PureScript 原生支持 extensible records !换句话说可以定义这样的函数:
    fullName :: forall t. { firstName :: String, lastName :: String | t } -> String
    然后任何类型!任何类型!只要包含了 firstName 和 lastName !就可以传给 fullName ,返回的就是 String 。这个是多么方便啊!这可是有静态检查的!你只要确保项目里那几个小小的与外界打交道的函数包上了 PureScript 的皮就好了。

    最后再来一个重磅的, generics ,泛型编程。这可不是 Java 里的泛型,上面的 type class 已经支持那个特性了。这里的泛型是根据数据类型的形状自动生成代码。比如说,自动生成等于函数 eq ,或者 toJson 。配合起 type class 简直爽爆了,再也不用写 boilerplate/冗余代码了。

    然后, PureScript 的 FFI ——也就是跟普通 JavaScript 交互的接口——也是特别简单,我就不说了,具体可以自行参考 purescript.org

    这,才是 GHCJS 之外最好的, callback hell 解决方案。
    zhujinliang
        8
    zhujinliang  
       2015-11-23 14:23:11 +08:00
    之前看到有个 wind.js ,觉得思路很好,但现在没再听说了

    整体思路就是写还是按照同步的写,但通过一个“编译器”变成异步的代码,编译器自动处理异步调用过程
    malcolmyu
        9
    malcolmyu  
       2015-11-23 15:31:40 +08:00
    就是给 tj 的那套 co 弄个一个很甜的语法糖
    ChefIsAwesome
        10
    ChefIsAwesome  
       2015-11-23 15:40:40 +08:00
    rx 不是语法糖,目的不是让你把异步写的跟同步一样。我一直觉得“把异步写的跟同步一样”没什么意义,能充分利用异步才是关键。
    举个例子:
    同时发五个请求去取一篇文章的五个章节。因为网络的原因,得到相应的顺序可能是 1,3,2,5,4 。
    现在要求得到 1 的时候立即把 1 渲染出来,得到 3 的时候不渲染,等到得到 2 之后再把渲染 2 和 3 一起渲染了。

    这种复杂的情况用 rx 就很容易实现。
    bramblex
        11
    bramblex  
       2015-11-23 15:44:45 +08:00
    @minsheng

    Haskell 大法好 /w\

    就一个简单的 continuation 的问题嘛~
    zhouyg
        12
    zhouyg  
       2015-11-23 16:21:21 +08:00
    服务端环境下用 babel 作大死啊,如果出现莫名其妙的问题,排查起来就是个灾难。
    iwege
        13
    iwege  
       2015-11-23 22:24:06 +08:00
    @zhujinliang 和那个差不多,只是上升到语法标准了, wind.js 我记得是用 eval 来做的。
    kamushin
        14
    kamushin  
       2015-11-24 14:05:38 +08:00
    和 yield 的区别是?
    edward1992
        15
    edward1992  
       2015-11-24 14:15:27 +08:00
    赞一个。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2803 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 22ms · UTC 03:36 · PVG 11:36 · LAX 19:36 · JFK 22:36
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.