Js异步流程控制

说明

目前 Js 流程控制技术多种多样,而async/await是es7的新标准
那像 callback, events, promise,generator,co 又是什么样,为什么更推荐使用async呢?

回调函数

先来看看传统的回调,这也是最简单最容易理解的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function sleep(callback) {
setTimeout(function() {
console.log('执行到这里了');
callback('hello world');
}, 1000);
}

sleep(function(message) {
console.log(message);
});

// 执行结果:

// 执行到这里了
// hello world

上面的代码比较容易理解,调用sleep, 把匿名函数作为参数传递出去,当sleep执行完成后调用回调参数

但如果有多层调用就会变成这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
sleep(function(message) {
console.log(message);

sleep(function(message) {
console.log(message);

sleep(function(message) {
console.log(message);
});

});

});

这也就是所谓的 “回调地狱(callback hell)”。

很明显,这种写法层级嵌套很深,比较反人类,维护起来相当麻烦,出错了很难以排查,

事件监听

任务的执行不取决于代码的顺序,而取决于某个事件是否发生。

下面代码采用 nodejs 的 events 模块,该模块可以实现事件绑定触发 (事件机制其实是一种观察者模式)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

function sleep() {
var emitter = new require('events')();
emitter.emit('one', '回调: 1');
setTimeout(function() {
emitter.emit('two', '回调: 2');
emitter.emit('done', '完成');
}, 1000);
}

var emit = sleep();
emit.on('one',function (message) {
console.log(message);
});

emit.on('two',function (message) {
console.log(message);
});

emit.on('done',function (message) {
console.log(message);
});

// 执行结果:

// 回调: 1
// 回调: 2
// 完成

使用 events 的好处是可以比较方便的添加多个回调,有利于实现模块化

但并没有解决回调嵌套的问题

如果有多层嵌套,需要在emit.on匿名函数里添加,如下:

1
2
3
4
5
6
emit.on('done', function() {
var emit2 = sleep();
emit2.on('done',function (message) {
console.log(message);
});
});

而且整个程序都要变成事件驱动型,运行流程会变得不清晰。

Promise

Promise 对象是 CommonJS 提出的一种规范,目的是为异步编程提供统一接口。

并且 ES6 原生提供了 Promise 对象。

直接上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var delay = function(ret) {
return new Promise(function(resolve, reject) {
setTimeout(function() {
if (ret) {
resolve('delay 执行完了');
} else {
reject('这是出错信息.');
}
}, 1000);
});

};

var callback = function(message) {
console.log(message);
};

var error = function(error) {
console.log(error);
};

delay(true).then(callback).catch(error); // delay 执行完了

delay(false).then(callback).catch(error); // 这是出错信息.

使用 promise 后回调函数变成了链式写法,程序的运行流程可以看得很清楚,而且有一整套的配套方法实现许多强大的功能。

比如:

1
2
3
4
5
// 实现多个回调
delay().then(callback1).then(callback2);

// 发生错误时
delay().catch(error);

这种写法的缺点是编写和理解比较困难。

Generator

Generator(生成器) 是 ES6 的新特性,具体用法请看我的另一篇文章

co

那么怎么利用 Generator 进行异步流程控制呢?

先实现一个 co 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50

// 先定义一个 sleep 函数,这个函数的作用是返回一个匿名函数
function sleep(ms) {
return function(callback) {
setTimeout(function() {
callback('test');
}, ms);
}
}

// 把一个生成器作为参数传递给 co
co(function* (){
var now = Date.now();
var i = yield sleep(100);
console.log(Date.now() - now); // 100 (因为执行效率的原因可能会大于100)
console.log(i); // test
});

function co(func) {

// 执行 生成器 得到 迭代器
var gen = func();

//调用下面的 next 方面
next();

function next(arg) {

// 执行 迭代器
var ret = gen.next(arg);

// 生成器结束直接返回
if (ret.done) {
return;
}

// 执行回调
// 这里的 ret.value 就是上面的 sleep 返回的 匿名函数
if (typeof ret.value == 'function') {
ret.value(function() {

// 当 异步操作 (这里是上面的setTimeout) 完成后
// 递归调用 next,并携带上参数
next.apply(this, arguments);

});
return;
}
}
}

把 Promise、generator 和 co 结合起来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

function co(gen) {
var iterator = gen();

next();

function next(arg) {
var ret = iterator.next(arg);
if (ret.done) {
return;
}

if (typeof ret.value == 'object') {
ret.value.then(next).catch(function(error) {
next(iterator.throw(error));
});
}
}
}

var delay = function(ret) {
return new Promise(function(resolve, reject) {
setTimeout(function() {
if (ret) {
resolve('delay 执行完了');
} else {
reject('这是出错信息.');
}
}, 1000);
});
};

var gen = function* () {
try {
var one = yield delay(true);
console.log(one);
var two = yield delay(false);
console.log(two);
} catch(error) {
console.log('出错了:' + error);
}
}

co(gen)

// 执行结果

// delay 执行完了
// 出错了:这是出错信息.

上面仅仅是一个简单的 co 实现方式,想体验更加强大的 co,可以去看看下面 TJ 大神开发的 co 模块

ps: co (coroutine) 即协同程序。

async/await

async/await 是es7的新标准,可以说是异步流程控制的终极解决方案,它使用起来非常简单,而且不需要知道底层的运行原理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 把上面的例子拿过来

var gen = function* () {
try {
var one = yield delay(true);
console.log(one);
var two = yield delay(false);
console.log(two);
} catch(error) {
console.log('出错了:' + error);
}
}

co(gen)


// 上面的代码,改用 async/await 可以写成这样

var test = async function() {
try {
var one = await delay(true);
console.log(one);
var two = await delay(false);
console.log(two);
} catch(error) {
console.log('出错了:' + error);
}
}

test();

// 执行结果

// delay 执行完了
// 出错了:这是出错信息.

其实经过比较就会发现,async 就是将 Generator 函数的星号(*)换成 async,将 yield 换成 await。

也就是说 async/await 仅仅是 Generator/yield 的一个语法糖罢了。

-------------本文结束感谢您的阅读-------------