FrankKai / FrankKai.github.io

FE blog
https://frankkai.github.io/
362 stars 39 forks source link

[译]异步JavaScript之通用异步编程概念 #217

Open FrankKai opened 4 years ago

FrankKai commented 4 years ago

在这个模块我们将学习重要的与异步编程相关的概念,以及它们在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,我们阻塞了交互。

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