FrankKai.github.io
FrankKai.github.io copied to clipboard
[译]异步JavaScript之通用异步编程概念
在这个模块我们将学习重要的与异步编程相关的概念,以及它们在web浏览器和JavaScript中的表现。在学习其他文章之前,首先需要学会这些概念。
- 什么是异步?
- 阻塞的代码
- 线程
- 异步的代码
- 总结
什么是异步?
通常情况下,程序的代码是直接运行的,只运行一次。如果一个函数依赖于另一个函数的结果,它就需要等待另一个函数结束并返会返回值。在此之前,从用户的角度看,整个程序是停止的。
看一段代码感受一下:
function block(){
const start = Math.floor(Date.now()/1000);
let i = 0;
while(i<10000000000){
i++
}
const end = Math.floor(Date.now()/1000);
console.log('normal()结束执行');
console.log(`消耗了${end-start}秒`);
return i;
}
function normal(){
console.log('normal()开始执行');
return `最终结果为${block()}`;
}
normal();
console.log('normal执行完必后代码才能执行到这里');
console.log('Hello JavaScript');
// normal()开始执行
// normal()结束执行
// 消耗了9秒
// normal执行完必后代码才能执行到这里
// Hello JavaScript
Mac用户通常会看到下面这种彩虹。这个图标的意思是:当前你使用的程序需要停止或者需要等待一些事情的完成,花费很长时间的话会很想知道到底发生了什么。
这是一个很糟糕的体验,现在已经是计算机的多核时代了,这种体验很差劲。 如果我们可以让另一个任务在另一个处理器核心上运行,并且我们可以知道它何时完成的,坐着等待是没有意义的。 可以让我们同时完成很多工作的模式,叫做异步编程。 这取决于程序的环境,可以是web浏览器,它提供了很多去异步运行task的API。
阻塞的代码
异步技术是非常有用的,尤其是在web领域。当一个web app在浏览器中运行时,它会执行一段密集的代码,并且不会把控制权交还给浏览器,浏览器此时可以看做是冻结状态。 浏览器的这种冻结状态,就叫做阻塞(block)。 浏览器阻塞后,就不会处理用户输入和执行其他的任务了,直到web app将控制权转交给浏览器。
我们看一下代码去了解下阻塞。
同步阻塞的例子:https://mdn.github.io/learning-area/javascript/asynchronous/introducing/simple-sync.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Simple synchronous JavaScript example</title>
</head>
<body>
<button>Click me</button>
<script>
const btn = document.querySelector('button');
btn.addEventListener('click', () => {
let myDate;
for(let i = 0; i < 10000000; i++) {
let date = new Date();
myDate = date
}
console.log(myDate);
let pElem = document.createElement('p');
pElem.textContent = 'This is a newly-added paragraph.';
document.body.appendChild(pElem);
});
</script>
</body>
</html>
只有1千万次date创建完全完成后,段落才会出现。 代码的后半段只有前半段执行完才会去执行。 点击click me 之后,很明显可以感觉到过了一会儿才出现新的p DOM。
上面这种不常见,有没有常见一点的例子? UI同步阻塞:https://mdn.github.io/learning-area/javascript/asynchronous/introducing/simple-sync-ui-blocking.html
因为要渲染UI,我们阻塞了交互。
- fill canvas会渲染一百万个圆
- Click me for alert会弹出一个提示
function expensiveOperation() {
for(let i = 0; i < 1000000; i++) {
ctx.fillStyle = 'rgba(0,0,255, 0.2)';
ctx.beginPath();
ctx.arc(random(0, canvas.width), random(0, canvas.height), 10, degToRad(0), degToRad(360), false);
ctx.fill()
}
}
fillBtn.addEventListener('click', expensiveOperation);
alertBtn.addEventListener('click', () =>
alert('You clicked me!')
);
点了fill canvas以后,alert按钮点击后看不到按钮的效果(其实click event listener已经添加到message queue中了),会在fill canvas完成后在执行alert。
为什么会出现这种阻塞的情况? 这是因为JavaScript是单线程的。所以我们再来看看线程的知识点。
线程
线程指的是程序可以用来完成任务的一个单独的处理器。
每个线程每次可以做一个task:
Task A --> Task B --> Task C
每个task会顺序执行;一个任务完成后,下一个任务才会开始。 像我们之前所说的,许多计算机现在拥有多核,所以可以同时做很多事。 支持多线程的程序语言可以利用多核去同时完成多个任务:
Thread 1: Task A --> Task B
Thread 2: Task C --> Task D
JavaScript是单线程的
JavaScript是单线程的。 即使是多核,我们也只能在单线程上执行任务,它叫做main thread。
Main thread: Render circles to canvas --> Display alert()
js可以有一些工具去解决这个问题。比如web worker允许js开启一个新的独立的线程,我们可以在worker中去处理一些昂贵的计算,从而避免主线程的用户交互被阻塞。
Main thread: Task A --> Task C
Worker thread: Expensive task B
使用worker我们优化了1千万次的date计算,从而使得段落的渲染不被阻塞。
web worker解决阻塞:https://mdn.github.io/learning-area/javascript/asynchronous/introducing/simple-sync-worker.html
worker线程解决阻塞问题:
const btn = document.querySelector('button');
const worker = new Worker('worker.js');
btn.addEventListener('click', () => {
worker.postMessage('Go!');
let pElem = document.createElement('p');
pElem.textContent = 'This is a newly-added paragraph.';
document.body.appendChild(pElem);
});
worker.onmessage = function(e) {
console.log(e.data);
}
worker.js
onmessage = function() {
let myDate;
for(let i = 0; i < 10000000; i++) {
let date = new Date();
myDate = date
}
postMessage(myDate);
}
更多web worder的实践可查看:https://github.com/FrankKai/FrankKai.github.io/issues/181
异步的代码
异步代码是非常有用的,但是他们有自己的限制。主要的原因是 web worker不能访问到DOM。 获取不到DOM也就意味着无法直接更新UI。我们不能把绘制一百万次canvas的逻辑放到worker中,它只能做数值类的计算。
第二个问题是worker中的代码时不是不阻塞的,它也是同步的。当一个函数依赖多个函数的计算结果时,这样做就会有问题了。考虑下面的线程图:
Main thread: Task A --> Task B
假设Task A从服务器拉取了一张照片,任务B会为其增加一个类似图片的filter。如果在Task A还在运行的过程中去运行Task B的话,会报错,因为这个时候图片还是不能被访问到的。
Main thread: Task A --> Task B --> |Task D|
Worker thread: Task C -----------> | |
在这个情况中,任务D用到了B和C的结果。如果我们保证这些结果同时可用,我们可以得到预期结果,但是正常情况往往不是这样。如果任务D发现其中之一不可用,会抛一个错误出来。
为了解决这个问题,浏览器可以运行准确的执行异步的动作。类似Promise这样的特性,可以使得异步得到保证,直到Promise返回结果之后,再执行其他操作:
Main thread: Task A Task B
Promise: |__async operation__|
Promise是web worker的优化版本??? 是的。Promise既可以保证main thread不被阻塞,还能准确的保证异步代码的执行顺序。
为什么Promise没有阻塞main thread? 因为Promise是microtask。
总结
现代浏览器在大量使用异步编程,从而使得浏览器同时可以做很多事情。 当你使用新的和更强大的API时,你会发现更多异步的场景。刚开始写异步代码时痛苦的,但是写得多了就熟能生巧了。
参考资料:https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Asynchronous/Concepts