写点什么

编程能力 —— 异步编程

用户头像
wendraw
关注
发布于: 2020 年 07 月 10 日

异步编程的能力对于一门语言来说是非常重要的,但是我们的 JS 在 ES5 之前是没有这个能力的。我们只能通过宿主(浏览器)的全局对象 window 提供的超时回调(setTimeout)和定时器(setInterval)来实现并行执行。还有一种就是用事件(Event)来实现。



到了 ES5 给我们提供了 Promise,ES6 提供了 async / await。从此我们的 JavaScript 就真的有了异步编程的能力。



红绿灯问题

为了能够更直观的体验的异步编程的好处,我们来看一个实际的问题:



完成一个路口的红路灯,会按照绿灯亮 10 秒,黄灯亮 2 秒,红灯亮 5 秒的顺序无限循环。请编写 JavaScript 代码来控制这个红路灯。



traffic-light.gif



首先来实现这三个灯的 UI,并且每个灯的控制逻辑都隔离开来,最后的代码就是下面这样:

<style></style>
<div class="green"></div>
<div class="yellow"></div>
<div class="red"></div>
<script>
function green() {
let lights = document.getElementsByTagName("div");
for (let i = 0; i < lights.length; i++) {
lights[i].classList.remove("light");
document.getElementsByClassName("green")[0].classList.add("light");
}
}
function yellow() {
let lights = document.getElementsByTagName("div");
for (let i = 0; i < lights.length; i++) {
lights[i].classList.remove("light");
document.getElementsByClassName("yellow")[0].classList.add("light");
}
}
function red() {
let lights = document.getElementsByTagName("div");
for (let i = 0; i < lights.length; i++) {
lights[i].classList.remove("light");
document.getElementsByClassName("red")[0].classList.add("light");
}
}
</script>

green()、yellow()、red() 函数都是用 class 选择器来控制灯亮,并且都是点亮自己颜色的灯关闭别的颜色的灯。



接下来就是控制灯方式,JavaScript 在不同的时代使用的技术是不同的。



Event

首先最简单的是用 event,给 3 个按钮对应着 3 种颜色的灯,手动切换灯的颜色。

<div class="green"></div>
<div class="yellow"></div>
<div class="red"></div>
<button onclick="green()">green</button>
<button onclick="yellow()">yellow</button>
<button onclick="red()">red</button>

这种方式有种偷奸耍滑的感觉。



setTimeout

在 setTimeout 的时代,我们只能利用 setTimeout 来设置固定的时间,时间到了之后就执行对应的回调函数。

<script>
// ......
void function go() {
green();
setTimeout(function () {
yellow();
setTimeout(function () {
red();
setTimeout(function () {
go();
}, 5000)
}, 2000)
}, 10000)
}()
</script>

那么最后的代码就要用嵌套来实现逻辑,用递归来实现无限循环。这样就能实现题目中的效果。



当然不想写嵌套代码也可以精确计算每次的时间:

void function go() {
green();
setTimeout(yellow, 10000);
setTimeout(red, 12000);
setTimeout(go, 17000);
}()

但是这样的代码不是很直观,可能需要想一会儿才知道绿灯亮 10s,黄灯亮 2s,红灯亮 5s。



Promise

接下来就到了 ES5 的时代给我们带来了 Promise 这个核武器,我们终于可以摆脱 setTimeout 这种回调地狱的恐怖。如果对 Promise 的用法不怎么熟悉,先去看 MDN 的 Promise



很多语言里面有 sleep() 函数可以直接使用,而我们只能用 setTimeout 这种丑陋的回调函数。现在这个问题也得意解决了,我们可以用 Promise 来包装 setTimeout,从而构造出我们的 sleep 函数。

function sleep(duration) {
return new Promise((resolve, reject) => {
setTimeout(resolve, duration)
})
}



有了 sleep 之后我们可以来改造 go 函数了,Promise 给我们提供了链式调用的能力,每次 then 的回调函数中都可以返回一个 Promise 作为下一次 then 的输入。

void function go() {
green();
sleep(10000).then(() => {
yellow();
return sleep(2000);
}).then(() => {
red();
return sleep(5000);
}).then(() => {
go();
})
}()

到这里代码已经变得比较易读,所要表达的意思也非常明确了。



async / await

接下来就到了 ES6 的时代,很多人说这是一个划时代的版本,将 JavaScript 带上了一个新的高度。在这个版本中 ECMA 继续完善了异步编程的能力,给我们带来了 async / await。这两个新特性提供了用 for、if 等代码结构来编写异步代码的方式,它的运行时基础是 Promise。



在 function 关键字之前加上 async 关键字,这样就定义了一个 async 函数。我们可以在 async 函数中用 await 关键字来等待一个 Promise,只有当 await 的 Promise 被返回是,后面的代码才会被执行。



因此 go 函数就可以被修改的相当优雅:

void async function go() {
while (true) {
green();
await sleep(10000);
yellow();
await sleep(2000);
red();
await sleep(5000);
}
}()

我们在一个 while(true) 的无限循环里面不断的亮灯、sleep 一段时间、再亮灯。



我们前面利用 Event 机制,用 3 个按钮来控制 3 种不同的灯亮。其实我们还可以修改一下这部分的代码,有了 async 我们就可以用一个按钮来一次切换不同的灯。就像交警手里的无限遥控器,随时根据交通路况来切换灯。



// ......
<div class="green"></div>
<div class="yellow"></div>
<div class="red"></div>
<button id="next">next</button>
<script>
// ......
function happen(element, eventName) {
return new Promise((resolve, reject) => {
document.addEventListener(eventName, resolve, { once: true });
})
}
void async function manual() {
while (true) {
green();
await happen(document.getElementById("next"), "click");
yellow();
await happen(document.getElementById("next"), "click");
red();
await happen(document.getElementById("next"), "click");
}
}()
</script>

这样我们就可以用一个按钮来顺序切换不同的交通灯了。



Generator / iterator



在没有 async/await 的年代,一些优秀的前辈们想到了用 generator/iterator 来模拟 async/await 的功能。不过必须要知道的是 generator/iterator 并不是异步代码。其中最著名的就是 co 框架。



function* go() {
while (true) {
green();
yield sleep(10000);
yellow();
yield sleep(2000);
red();
yield sleep(5000);
}
}
function run(iterator) {
let { value, done } = iterator.next();
if (done) return;
if (value instanceof Promise) {
value.then(() => {
run(iterator);
})
}
}
function co(generator) {
return function () {
return run(generator())
}
}
go = co(go);
go();

最后的代码就是这样,我们看到 go 部分的代码与 async 差不多。不过 go 是一个 generator 函数,并非是一个有异步的能力函数。



而且这部分的代码写起来非常复杂,也不利于阅读。在我们有了 async/await 之后,用 generator/iterator 来模拟异步的方法应该被废弃。



总结

本篇文章就是根据一个实际问题 —— 交通灯,来体验了不同阶段 JavaScript 的异步编程的能力。可以说有了async 的加持,Promise 已经完全满足我们异步编程的需求。这些新特性也是 ES6 中最重要的能力,我们应该熟练的运用它们。



最后所有的代码我都上传到 GitHub  traffic-light 上了。



参考

winter 前端进阶训练营

JavaScript执行(一):Promise里的代码为什么比setTimeout先执行?



发布于: 2020 年 07 月 10 日阅读数: 80
用户头像

wendraw

关注

还未添加个人签名 2018.05.07 加入

还未添加个人简介

评论

发布
暂无评论
编程能力 —— 异步编程