JavaScript中的异步编程 上

本文启发于阮一峰老师的深入掌握 ECMAScript 6 异步编程

下篇传送门JavaScript中的异步编程 下

JavaScript是一门单线程的语言。这样的设计减少了线程间同步和统筹的代价。但是,这也意味着,同一时刻只能完成一项工作,不能“多面手”。多个任务出现时,后一个任务需要等待前一个任务完成才可执行。

当一项任务耗时较长时,后继者往往需要等待很久。直观体现在浏览器白屏,假死等。异步执行模式便因此而生。

异步和回调

异步模式区分于同步模式,任务的执行顺序和排列顺序并不完全一致。在前一个任务开始执行时,将之交给环境中其他辅助线程处理,之后立即执行下一个任务。当任务完成后,再以回调的形式执行回调函数。这种执行方式实际上正是Event Loop 的体现。

阮老师博客中提到的回调函数事件驱动发布订阅都能很直观的看到回调的概念。

回调函数

这是最常见的实现异步编程的方式。它的大体形式是将回调函数作为输入参数传入到需要异步完成的任务中。在任务体函数内利用全局环境下内建的异步函数实现异步的目的。

大概是这样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//这里是一个需要异步完成的函数asy和回调函数cal
asy(), cal();
//它们的定义和使用像下面这样
function asy (callback) {
setTimeout(function () {
//异步工作代码
callback();
}, 0);
}

function cal () {
console.log('Hello!'); //或其它什么
}

asy(cal);

asy()函数会在完成自己工作后自动调用回调函数cal()。而这个过程是异步完成的。

事件驱动

严格来说,事件驱动是一种异步编程思想。通过事件的触发来执行特定任务的代码。使得代码的执行并不按照顺序来。

使用时,最典型的用法就是DOM2级事件绑定。为DOM元素绑定监听函数,在事件触发时,执行特定代码。推广开来,可以实现JavaScript中自定义事件的监听。

大概是这样

1
2
target.on('someevent', handler); // 注册事件
target.emit('someevent'); //触发事件

在实现时,target实现了(或继承)类似于下图中EventUtil类的定义。内部维护一个对象,存储事件和回调函数数组的键值对对象。在使用on,emit时,向管理器中写入事件和读取事件对应的回调数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var EventUtil = {
// 全局事件管理
var events = {},
// 注册事件
on = function (type, handler) {
if (events[type]) {
events[type].push(handler);
} else {
events[type] = [handler];
}
},
// 触发事件
emit = function (type) {
if (!events[type]) return;
for (var i = 0, len = events[type].length; i < len; i++) {
events[type][i];
}
};
};

发布订阅模式

上面这种事件驱动的方式在React和Vue等MVVM框架中经常用来在组件间传递信息。当组件关系复杂时,发布订阅模式会更有利于管理信息和将信息集中化管理。

也就是ReduxVuex所做的事情。任务状态改变时,向中心传递信号,其他订阅这个信号的任务函数都会受到这个信号。

promise

异步回调好是好,很好理解。但是处理错误的“回调地狱”也为人诟病。

1
2
3
4
5
6
7
8
9
10
11
12
13
function async(request, callback) {
// Do something.
asyncA(request, function (data) {
// Do something
asyncB(request, function (data) {
// Do something
asyncC(request, function (data) {
// Do something
callback(data);
});
});
});
}

这种在回调中嵌套其他异步函数的场景下,错误的捕获变得异常头痛。代码也会变得难以阅读和维护。

ES6中的Promise对象优雅地解决了回调地狱的问题。它由CommonJS工作组提出,通过thencatch方法指定回调函数和错误的捕获函数,同时返回一个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 promise = new Promise(function (resolve, reject){
if (/* 异步操作成功 */) {
resolve(val);
} else {
reject(err)
}
});

promise.then(function (val) {
//success handler
}, function (val) {
//failure handler
});

// 或是
promise.then(function (val) {
//success handler
}).catch(err) {
//error handler
}

// 处理一批次的异步任务
var p = Promise.all([p1, p2, p3]),
q = Promise.race([p1, p2, p3]);

在实现上,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
25
26
27
28
29
30
//constructor
var Promise = function() {
this.callbacks = [];
}

Promise.prototype = {
construct: Promise,
resolve: function(result) {
this.complete("resolve", result);
},

reject: function(result) {
this.complete("reject", result);
},

complete: function(type, result) {
while (this.callbacks[0]) {
this.callbacks.shift()[type](result);
}
},

then: function(successHandler, failedHandler) {
this.callbacks.push({
resolve: successHandler,
reject: failedHandler
});

return this;
}
}

Promise在回调函数较少时,then方法的链式调用无伤大雅。当出现较多异步回调场景下,如异步陆续加载100张图片,then方法的使用仍显得不那么自然。

有自然的使用方法么?答案是有的。下篇中介绍的Generator和async将实现异步编程的更高境界。

参考

异步编程 promise模式 的简单实现