JavaScript 异步编程

单线程

我们知道 JavaScript 是单线程的,在同一时间只能做一件事(虽然 HTML 5 提出了 Web Work 标准,允许 Javascript 创建多个线程,但子线程完全受主线程控制,且不得操作 DOM,一定意义上并没有改变 Javascript 单线程的本质)。如果有多个任务,就必须等待前面的任务完成后再执行下一个任务。

单线程好处是实现简单,执行环境相对单纯;坏处是如果有一个任务耗时很长或者出错了,后面的任务都处于等待中,无法执行。为了解决这个问题,JavaScript 将任务执行模式分成了两种:同步和异步。

同步模式就像上面描述都一样,每个任务都要等待前面都任务执行完成后再执行。而异步模式则是为每个任务添加一个或者多个回调函数,此时这些任务都是同步执行的(由于 Javascript 的事件机制,回调函数并不会立即执行),而他们的回调函数/任务则会在条件触发时执行。

异步模式解决了 Javascript 单线程模式的同步等待问题,也是因为这样才是客户端(浏览器)以及 Node 中才会大量运用。在浏览器中,大量的 Dom 交互以及异步请求,这个时候我们肯定不希望影响客户的使用和操作,都会使用异步编程的特性既不影响用户的操作又实现了我们的功能。

回调函数

回调函数,顾名思义。这个函数要等待之前的任务执行完成后会头来调用它。如:

1
2
3
4
5
6
7
8
const fun1 = function (callback) {
console.log("i am fun1");
callback && callback();
};
const fun2 = function () {
console.log("i am fun2");
};
fun1(fun2);

如果我们有一个任务依赖许多的前置任务,就会写大量的回调函数,这样的写法我们称之为回调地狱,回调地狱并不能解决,只能优化代码,使其可读性更高,如上面的示例代码,实际上还是回调函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// import $ from 'jQuery'
const hell = function () {
$.get("pathA", function (res) {
if (res) {
$.get("pathB", function (res) {
if (res) {
$.get("pathC", function (res) {
// ...
});
}
});
}
});
};

回调函数的优点是简单、容易理解和部署,缺点是不利于代码的阅读和维护,各个部分之间高度耦合(Coupling),流程会很混乱,而且每个任务只能指定一个回调函数。

事件监听

事件驱动也是异步编程的一个解决方案,任务的执行有事件来触发。实际上事件驱动可以算做一种设计模式,具体实现还是回调函数,最常见于 Dom 事件绑定,当然也包括其他事件,如:

1
2
3
4
// Dom 事件
window.addEventListener("load", function () {
console.log("onload");
});

这种方法的优点是比较容易理解,可以绑定多个事件,每个事件可以指定多个回调函数,而且可以”去耦合”(Decoupling),有利于实现模块化。缺点是整个程序都要变成事件驱动型,运行流程会变得很不清晰。

发布订阅

发布订阅模式的实现其实也是基于回调函数来实现的,首先订阅一个/多个事件,然后再其他地方再发布这个事件,然后事件总线收到这个事件再去执行订阅的处理函数。如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class EventBus {
events = {};

on(eventName, handler) {
this.events[eventName] = handler;
}

dispatch(eventName) {
const params = Array.prototype.slice.call(arguments);
params.shift();
this.events[eventName] && this.events[eventName](...params);
}
}

const eventBus = new EventBus();

eventBus.on("call", function () {
console.log("eventBus on call");
});

eventBus.dispatch("call");

Promise

Promise 是 ES6 的新 api,目的是为异步编程提供统一接口。它的设计思想是一个 Promise 只具有 pending、fulfilled、rejected 三种状态,当 pending 结束后,就必须 reslove 或者 reject,这时会返回一个 Promise 对象,具有 then、catch、final 等方法,允许指定后续的回调函数,如:

1
2
3
4
5
6
7
fetch.get("pathA").then((resA) =>
fetch.get("pathB").then((resB) =>
fetch.get("pathC").then((resC) => {
console.log(resC);
})
)
);

看起来与上面的回调地狱非常相似,不过流程控制上却要优雅许多,更何况 Promise 还有许多高级方法,如 all、race 等。

[越努力,越幸运!]