ADVANCED JS RETHINKING ASYNC

Rock with async/await

本篇文章是作为上一篇的续集,考虑到第一篇的篇幅,还有更重要的一点就是上一篇讲的内容已经可以直接应用在最新版本的Node.js和一些高级浏览器(Chrome,FF)中,具体兼容性可参考:https://kangax.github.io/compat-table/es6/

而这一篇讲的内容,是ECMAScript 2016(ES7)的async/await特性,目前的兼容性可参考:http://kangax.github.io/compat-table/esnext/#test-async_functions,虽然现在来看还不是非常乐观,但是我们可以通过第三方的代码转换工具(如TraceurBabel),将这些新特性的代码转换为当前环境可运行的代码。

一个简单的例子

实现同步的sleep,同步的代码看起来应该是下面的样子:

function sleep(timeout) {
    setTimeout(function() {}, timeout);
}

function main() {
    console.time('how long did I sleep');
    sleep(3000);
    console.timeEnd('how long did I sleep');
}

main();
// how long did I sleep: 0ms

但是在js中的执行结果却是0ms,这不是我们预期的呀。

改造

按照这种同步的代码流程,怎么样才能输出3000ms呢?看过上一篇文章的童鞋应该很快就能想到使用Generatoryield,没看过的童鞋建议先看完上一篇再回来。

sleep(5min)

好,我就当你们都回来了,接下来就说说如何使用async/await实现“同步”的sleep。

await期望的值是一个Promise对象,改造sleep方法:

function sleep(timeout) {
    return new Promise(function(resolve, reject) {
        setTimeout(resolve, timeout);
    });
}

除此之外,main方法还需要使用async显式声明成异步方法:

async function main() {
    console.time('how long did I sleep');
    await sleep(3000);
    console.timeEnd('how long did I sleep');
}

将以上代码保存为async-sleep.js。前面也说了需要借助第三方代码转换工具,那我们就安装Babel

> npm install --save-dev babel-cli

安装后执行:

> ./node_modules/babel-cli/bin/babel-node.js async-sleep.js

没出意外的话,我们应该看到...WTF,出错了

SyntaxError: async-sleep.js: Unexpected token (7:6)
   5 | }
   6 |
>  7 | async function main() {
     |       ^
   8 | 	console.time('how long did I sleep');
   9 | 	await sleep(3000);
  10 | 	console.timeEnd('how long did I sleep');

这时需要安装一个Babel的插件用于转换async

> npm install --save-dev babel-plugin-transform-async-to-generator

安装好后,需要在运行目录添加一个配置文件.babelrc

> vi .babelrc
{
    "plugins": ["transform-async-to-generator"]
}

或在命令中指定:

>  ./node_modules/babel-cli/bin/babel-node.js --plugins transform-async-to-generator async-sleep.js

没出意外的话,我们应该看到...WTF,又出错了

async-sleep.js:1
(function (exports, require, module, __filename, __dirname) { let main = (() => {
                                                              ^^^

SyntaxError: Block-scoped declarations (let, const, function, class) not yet supported outside strict mode

好吧,提示也很明显,我们使用strict模式,完整的代码如下:

'use strict';

function sleep(timeout) {
    return new Promise(function(resolve, reject) {
        setTimeout(resolve, timeout);
    });
}

async function main() {
	console.time('how long did I sleep');
	await sleep(3000);
	console.timeEnd('how long did I sleep');
}

main();
// how long did I sleep: 3003ms

再执行,终于看到我们期望的3003ms。呃,怎么不是3000ms,不要在意这些细节。难道QQ空间曾经用这个实际延迟的误差来判断客户端CPU的繁忙程度也要告诉你。

看完这个例子,是不是发现async类似于Generator中的*,而await类似于yield,但是现在不需要再额外封装一个run方法了,这还是很方便的。

重写回调地狱的例子

继续重写那个回调地狱的例子:

  1. 读取当前目录的package.json
  2. 检查backup目录是否存在,如果不存在就创建backup目录
  3. 将文件内容写到备份文件

readPackageFilecheckBackupDirbackupPackageFile直接使用上一篇中的定义:

var readPackageFile = new Promise(function(resolve, reject) {
    fs.readFile('./package.json', function(err, data) {
        if (err) {
            reject(err);
        }

        resolve(data);
    });
});

var checkBackupDir = new Promise(function(resolve, reject) {
    fs.exists('./backup', function(exists) {
        if (!exists) {
            resolve(mkBackupDir);
        } else {
            resolve();
        }
    });
});

var mkBackupDir = new Promise(function(resolve, reject) {
    // throw new Error('unexpected error');
    fs.mkdir('./backup', function(err) {
        if (err) {
            return reject(err);
        }

        resolve();
    });
});

function backupPackageFile(data) {
    return new Promise(function(resolve, reject) {
        fs.writeFile('./backup/package.json', data, function(err) {
            if (err) {
                return reject(err);
            }

            resolve();
        });
    });
};

Let's Rock:

(async function() {
    try {
        // 1. 读取当前目录的package.json
        var data = await readPackageFile;
        
        // 2. 检查backup目录是否存在,如果不存在就创建backup目录
        await checkBackupDir;

        // 3. 将文件内容写到备份文件
        await backupPackageFile(data);

        console.log('backup successed');
    } catch (err) {
        console.error(err);
    }
}());

总结

js正朝着越来越好的方向发展,不是吗?

参考地址

题图引自:http://forwardjs.com/img/workshops/advancedjs-async.jpg