Baike.dev
All toolsAI codingTrendingOpen sourceNewsSubmit
Log in
< Back to tools
A

asynchronous-flow-control

> 编程语言
Open source

StuQ分享专题《深入浅出js(Node.js)异步流程控制》完整版

194 stars0 likes3 views
WebsiteGitHub

About

StuQ分享专题《深入浅出js(Node.js)异步流程控制》完整版

asynchronous-flow-control

StuQ分享专题《深入浅出js(Node.js)异步流程控制》完整版

其他分享

  • InfoQ 前端之巅分享 精简版

摘要

目前在js流程控制领域越来越乱,各种派系。。。比如promise,generator,async函数,各种混战,在百花齐放的今天,作为前端或Node.js沾边工程师或全栈工程师,你知道该学哪种么?

  • 从下一代测试框架ava说起
  • 流程控制发展的前世今生概览
  • 从co引出的血案,到yieldable 5种,到aysnc函数,聊聊同步的流程控制
  • 最后推导一下学习重点、未来趋势

个人介绍

i5ting(桑世龙),空弦科技 CTO,StuQ 明星讲师,开源项目 Moajs 作者,Node.js 技术布道者,即将出版《更了不起的 Node 4:将下一代 Web 框架 Koa 进行到底》

曾就职在新浪、网秦,曾做过前端、后端、数据分析、移动端负责人、做过首席架构师、技术总监,全栈技术实践者,目前主要关注技术架构和团队梯队建设方向。

流程控制

  • 从下一代测试框架ava开始
  • co引出的血案
    • generator/yield
    • co源码解析
    • convert or compose
  • yieldable 5种
  • async/await
  • 推导出学习重点

涵盖

  • callback vs hell
  • Node.js的error-first和EventEmitter
  • thunk
  • promise/a+
  • generator/yield
  • async/await
  • 异常处理
  • 各种xxx-fy

从下一代测试框架ava开始

讲流程控制,你扯到测试框架干什么?看似无关,但实际上测试框架才是对流程控制提供最全的最简洁的集成的,如果通用性的测试框架都解决不好流程控制问题,那么,这样的东西不用也罢。

先简单的介绍一下:AVA是面向未来的测试运行器

简单的说ava是mocha的替代品,

  • es6语法支持更好,对aysnc/await有支持
  • 执行效率更高,使用io并发,就必须保证测试的原子性
  • 语义上更简单,集众家之长

虽然 JavaScript 是单线程,但在 Node.js 里由于其异步的特性使得 IO 可以并行。AVA 利用这个优点让你的测试可以并发执行,这对于 IO 繁重的测试特别有用。另外,测试文件可以在不同的进程里并行运行,让每一个测试文件可以获得更好的性能和独立的环境。在 Pageres 项目中从 Mocha切换 到 AVA 让测试时间从 31 秒下降到 11 秒。测试并发执行强制你写原子测试,意味着测试不需要依赖全局状态或者其他测试的状态,这是一件非常好的事情。

tj曾说:“如果他现在重写mocha,也会写成ava这样”

闲言少叙

  • 同步
  • callback
  • promise
  • generator
  • async function

举几个例子:

test('synchronization', t => {})
test.cb('callback', t => {})
test('promise', t => {})
test('generator function',  function * (t) {})
test('async function', async t => {})

大家了解这几种写法的区别么?

总结一下就是:

  • test和test.cb是两种:同步和callback处理。
  • 在test里,第二个参数有3种写法,普通函数、Generator函数和Async函数。

它们就是我们的主角,几乎所有的流程控制都在里面了。看一个模块或者框架,如果能这样比较,就非常容易看到它们的本质,一般都是写的人比用的人精,但用的人也可以变成写的人,是不是?

准备

$ npm init
$ npm i -D ava
$ npm i -D co-exec

测试

同步

js语言里除了ajax、setTimeout等大部分都是同步,写同步代码是一种幸福,稍后你就懂了

1.js

import test from 'ava';

test('synchronization', t => {
  const a = /foo/;
  const b = 'bar';
  const c = 'baz';
  t.false(a.test(b) || b === c);
});

异步

但是我们习惯回调,无论事件还是ajax,都是异步的。另外Node.js里又为了性能而异步,即所谓的天生异步,每个api都是异步的。

以Node.js为例

  • error-first callback(错误优先的回调机制)
  • EventEmitter (事件发射机制)

总结,callback是用的最多的,是绝大部分的api遵守的约定,而EventEmitter是辅助机制,通过继承EventEmitter,来解耦业务逻辑。

2.js

import test from 'ava';
const exec = require('child_process').exec

test.cb('error-first callback with setTimeout', t => {
    setTimeout(() => {
      t.pass();
      t.end();
    }, 2000);
});

test.cb('error-first callback with exec', t => {
  exec('cat *.js bad_file | wc -l',
    function (error, stdout, stderr) {
      t.pass();
      t.end();
  });
});

promise

为了让大家从回调的地狱中回到天堂,Promise你值得拥有

Promise是一种令代码异步行为更加优雅的抽象,有了它,我们就可以像写同步代码一样去写异步代码。它是从Common JS规范分裂出来的,目前主流是Promose/A+规范。

jQuery很早就引入了Promise的概念,主要是deffered和promise对象。而在Angularjs里也有类似的实现,叫$q,其实是promise库q的精简版。

先看测试,后面会详细讲

3.js

import test from 'ava';

// promise
test('promise', t => {
  return Promise.resolve(3).then(n => {
      t.is(n, 3);
  });
});

generator

generator是es6的一个特性,本身是用于计算的,通过generator和yield写的代码看起来像同步的,主要是yield来处理同步的事儿,但yield又只能在generator里。

在ava里是generator特别简单

4.js

import test from 'ava';

var exec = require('co-exec');

test('generatorFn with exec()', function * (t) {
    let commit = yield exec('ls -alt|grep .gitignore|wc -l');
    t.true(commit == 1);
});

async function

generator执行的时候,需要先生成对象,然后next进行下一步。这样做起来还是比较麻烦,能不能不需要执行器啊?于是async函数就应运而生了。

async函数es7 stage-3的特性,可惜差一点就进入到es7规范了。async函数里使用await可以做到和yield类似的效果,但await只能接promise对象。

5.js

import test from 'ava';

test('async function', async t => {
    const bar = Promise.resolve('bar');

    t.is(await bar, 'bar');
});

执行

➜  asynchronous-flow-control git:(master) ✗ ava -v *.js

  ✔ 1 › synchronization
  ✔ 2 › error-first callback with exec
  ✔ 3 › promise
  ✔ 4 › generatorFn with exec()
  ✔ 5 › async function
  ✔ 2 › error-first callback with setTimeout (2s)

  6 tests passed [09:48:27]

➜  asynchronous-flow-control git:(master) ✗ ava -v *.js -s

  ✔ 1 › synchronization
  ✔ 2 › error-first callback with setTimeout (2s)
  ✔ 2 › error-first callback with exec
  ✔ 3 › promise
  ✔ 4 › generatorFn with exec()
  ✔ 5 › async function

  6 tests passed [09:48:35]

说明

  • 第一个是并行的,所以顺序有点乱
  • 第二个是顺序执行 -s

流程控制的6个演进步骤

js流程控制的演进过程,分以下6部分:

  1. 同步代码
  2. 异步JavaScript: callback hell
  3. Thunk
  4. Promise/a+
  5. 生成器Generators/yield
  6. Async函数/Await(以前说是ES7 stage-3)

看起来挺简单的,作为*js(沾边)工程师的各位自测一下,当前是哪个阶段?

对比一下此图,是否有不了解的呢?下面一一进行介绍。

Promise

顺序执行的代码和错误优先的回调方式都是js引擎默认支持的,这部分大家会调用接口,无太多变化,而Promise是对callback的思考,或者说改良方案,目前使用非常普遍,这里详细讲解一下。

node里的Promise

promise最早是在commonjs社区提出来的,当时提出了很多规范。比较接受的是promise/A规范。后来人们在这个基础上。提出了promise/A+规范,也就是实际上的业内推行的规范。es6也是采用的这种规范。

The Promise object is used for asynchronous computations. A Promise represents an operation that hasn't completed yet, but is expected in the future.

Promise对象用于异步技术中。Promise意味着一个还没有完成的操作(许愿),但在未来会完成的(实现)。

在Node.js 0.12里实现9/11,在6.2和7实现100%,中间版本实现了10/11。所以Node.js对Promise的支持是非常好的,0.12之后的绝大部分版本都支持的不错。

Promise 的最大优势是标准,各类异步工具库都认同,未来的 async/await 也基于它,用它封装 API 通用性强,用起来简单。

要想知道node.js有哪些比较好的promise实现,最好的办法就是看一下最知名的bluebird库的benchmark里比较里哪些。

  • [email protected]
  • [email protected]
  • [email protected]
  • [email protected]
  • [email protected]
  • [email protected]
  • [email protected]
  • [email protected]
  • [email protected]
  • [email protected]
  • [email protected]
  • [email protected]
  • [email protected]
  • [email protected]
  • [email protected]

Promise是什么?

A promise is an abstraction for asynchronous programming. It’s an object that proxies for the return value or the exception thrown by a function that has to do some asynchronous processing. — Kris Kowal on JSJ

Promise表示一个异步操作的最终结果。与Promise最主要的交互方法是通过将函数传入它的then方法从而获取得Promise最终的值或Promise最终最拒绝(reject)的原因。

  • 递归,每个异步操作返回的都是promise对象
  • 状态机:三种状态转换,只在promise对象内部可以控制,外部不能改变状态
  • 全局异常处理

定义

var promise = new Promise(function(resolve, reject) {
  // do a thing, possibly async, then…

  if (/* everything turned out fine */) {
    resolve("Stuff worked!");
  }
  else {
    reject(Error("It broke"));
  }
});

术语

  • Promises Promise规范自身
  • promise对象 promise对象指的是 Promise 实例对象
  • ES6 Promises 如果想明确表示使用 ECMAScript 6th Edition 的话,可以使用ES6作为前缀(prefix)
  • Promises/A+ Promises/A+。 这是ES6 Promises的前身,是一个社区规范,它和 ES6 Promises 有很多共通的内容。
  • Thenable 类Promise对象。 拥有名为.then方法的对象。

hello promise

给出一个最简单的读写文件的api实例,它是error-first风格的典型api

async/promise/hello.js

// callbacks
var fs = require("fs");

fs.readFile('./package.json', (err, data) => {
  if (err) throw err;
  console.log(data.toString());
});

下面,我们把它变成promise的简单示例

async/promise/hellopromise.js

// callbacks to promise
var fs = require("fs");

function hello (file) {
  return new Promise(function(resolve, reject){
    fs.readFile(file, (err, data) => {
    	if (err) {
    		reject(err);
    	} else {
    		resolve(data.toString())
    	}
    });
  });
}

hello('./package.json').then(function(data){
  console.log('promise result = ' + data)
}).catch(function(err) {
  console.log(err)
})

这二段代码执行效果是一模一样的,唯一的差别是前一种写法是Node.js默认api写法,以回调为主,而后一种写法,通过返回promise对象,在fs.readFile的回调函数,将结果延后处理。

这就是最简单的promise实现

形式

new Promise(function(resolve, reject){
  
})

参数

  • resolve 解决,进入到下一个流程
  • reject 拒绝,跳转到捕获异常流程

调用

hello('./package.json').then(function(data){

})

全局处理异常

hello('./package.json').then(function(data){

}).catch(function(err) {

})

结论

Promise核心:将callback里的结果延后到then函数里处理或交给全局异常处理

封装api的过程

还是以上面的fs.readFile为例

fs.readFile('./package.json', (err, data) => {
  if (err) throw err;
  console.log(data.toString());
});

参数处理:除了callback外,其他东西都放到新的函数的参数里

function hello (file) {
  ...
}

返回值处理:返回Promise实例对象

function hello (file) {
  return new Promise(function(resolve, reject){
    ...
  });
}

结果处理:通过resolve和reject重塑流程

function hello (file) {
  return new Promise(function(resolve, reject){
    fs.readFile(file, (err, data) => {
    	if (err) {
    		reject(err);
    	} else {
    		resolve(data.toString())
    	}
    });
  });
}

我们知道所有的Node.js都是error-first的callback形式,通过上面的例子,我们可以肯定是所有的Node.js的API都可以这样来处理,只要它们遵守Promise规范即可。

每个函数的返回值都是Promise对象

为了简化编程复杂性,每个函数的返回值都是Promise对象,这样的约定可以大大的简化编程的复杂。

它可以理解为是递归的变种思想应用,只要是Promise对象,就可以控制状态,就可以支持then方法,参数还是Promise对象,这样就可以无限个Promise对象链接在一起。

…

这里可以看出

  • hello、world、log 返回单个Promise对象
  • hello('./each.js').then(log) 返回流程链

无论是单个,还是流程链的返回值都是Promise对象,那么它就是一样的。

链式的thenable

每个promose对象都有then方法,也就是说,then方法是定义在原型对象Promise.prototype上的。它的作用是为Promise实例添加状态改变时的回调函数。

一般实现,类似于

Promise.prototype.then = function(sucess, fail) {
    this.done(sucess);
    this.fail(fail);
    return this;
};

它的返回值是this,这就是为什么then可以链式操作的原因。

then的2个参数

  • sucess是fulfilled状态的回调函数
  • fail是rejected状态的回调函数

一般都是穿sucess回调函数即可。

状态转换

一个Promise必须处在其中之一的状态:pending, fulfilled 或 rejected.

  • pending: 初始状态, 非 fulfilled 或 rejected.
  • fulfilled: 完成(成功)的操作.
  • rejected: 拒绝(失败)的操作.

这里从pending状态可以切换到fulfill状态,也可以从pengding切换到reject状态,这个状态切换不可逆,且fulfilled和reject两个状态之间是不能互相切换的。

一定要注意的是,只有异步操作的结果,才可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。

Promise对象可以理解为一个乐高积木,它对下一个流程,传送状态和具体结果。

如果是pending状态,则promise:

  • 可以转换到fulfilled或rejected状态。

如果是fulfilled状态,则promise:

  • 不能转换成任何其它状态。
  • 必须有一个值,且这个值不能被改变。

如果是rejected状态,则promise可以:

  • 不能转换成任何其它状态。
  • 必须有一个原因,且这个值不能被改变。

”值不能被改变”指的是其identity不能被改变,而不是指其成员内容不能被改变。

reject和resove流程再造

前面讲了,每个函数的返回值都是Promise对象,每个Promise对象都有then方法,这是它可以递归思路的解决办法。

那么问题来了,如何在连续的操作步骤里,完成流程再造呢?这其实才是异步流程控制最核心的问题。

我们知道Promise的使用形式如下:

new Promise(function(resolve, reject){
  
})

下面仍然使用fs的例子,见reflow.js

way 1:简单模式

hello('./package.json').then(function(data){
  console.log('way 1:\n')
  return new Promise(function(resolve, reject){
    console.log('promise result = ' + data)
    resolve(data)
  });
}).then(function(data){
  return new Promise(function(resolve, reject){
    resolve('1')
  });
}).then(function(data){
  console.log(data)
  
  return new Promise(function(resolve, reject){
    reject(new Error('reject with custom err'))
  });
}).catch(function(err) {
  console.log(err)
})

这是一个常规的例子,就是在then里面的promise对象里,通过resolve将流程进行到下一步,在reject的时候抛出异常。这里面的每一个promise对象里都可以这样做,那么是不是这个操作流程就是可控的了?

way 2:嵌套模式

hello('./package.json').then(function(data){
  console.log('\n\nway 2:\n')
  return new Promise(function(resolve, reject){
    console.log('promise result = ' + data)
    resolve(data)
  }).then(function(data){
    return new Promise(function(resolve, reject){
      resolve('1')
    });
  }).catch(function(err) {
    console.log(err)
  })
}).then(function(data){
  console.log(data)
  
  return new Promise(function(resolve, reject){
    reject(new Error('reject with custom err'))
  });
}).catch(function(err) {
  console.log(err)
})

这里的做法是,把第一个then和第二个then合并到一个流程里。这样做的好处是,这个流程也可以考虑单独处理异常。为了某些粒度更新的异步处理,是非常有好处的。

way 3:嵌套模式的refact清晰版

var step1 = function(data){
  console.log('\n\nway 3:\n')
  return new Promise(function(resolve, reject){
    console.log('promise result = ' + data)
    resolve(data)
  }).then(function(data){
    return new Promise(function(resolve, reject){
      resolve('1')
    });
  }).catch(function(err) {
    console.log(err)
  })
}

var step2 = function(data){
  console.log(data)
  
  return new Promise(function(resolve, reject){
    reject(new Error('reject with custom err'))
  });
}

hello('./package.json').then(step1).then(step2).catch(function(err) {
  console.log(err)
})

把每个独立的操作抽成函数,然后函数的返回值是Promise对象,这样就可以在真正的流程链里随意组织了。

它们就好比是积木一样,可以让逻辑更清楚,让代码更具可读性和可维护性。如果再极端点,每个操作都放到独立文件里,变成模块,是不是更爽呢?

way 4:final版,把每个独立的操作放到独立文件里,变成模块

原理: 使用require-directory

根据commonjs规范,require只能引用某一个文件,当一个文件夹里有很多文件,每一个都去require是很麻烦的,require-directory就是一个便捷模块,可以把某个文件夹内的多个文件挂载到一个对象。

原理,递归遍历文件,读取具体文件,如果是遵循commonjs规范的模块,就挂载在它的返回值对象上。

比如reflow/tasks/index.js

var requireDirectory = require('require-directory');
module.exports = requi

Issues· 0 open

View all issuesOpen on GitHub

No open issues yet, or sync has not completed.

> Tags

JavaScript

No comments yet. Be the first to share.

> Details

PublishedAug 1, 2026
UpdatedSep 18, 2026
Category编程语言
PricingOpen source

> Related tools

T
TypeScript
JavaScript 的超集,为前端与全栈提供静态类型
P
Python
通用编程语言,广泛用于 Web、数据与 AI
G
Go
Google 推出的简洁高效系统语言