Open FrankKai opened 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 A --> Task B --> Task C
每个task会顺序执行;一个任务完成后,下一个任务才会开始。 像我们之前所说的,许多计算机现在拥有多核,所以可以同时做很多事。 支持多线程的程序语言可以利用多核去同时完成多个任务:
Thread 1: Task A --> Task B Thread 2: Task C --> Task D
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
在这个模块我们将学习重要的与异步编程相关的概念,以及它们在web浏览器和JavaScript中的表现。在学习其他文章之前,首先需要学会这些概念。
什么是异步?
通常情况下,程序的代码是直接运行的,只运行一次。如果一个函数依赖于另一个函数的结果,它就需要等待另一个函数结束并返会返回值。在此之前,从用户的角度看,整个程序是停止的。
看一段代码感受一下:
Mac用户通常会看到下面这种彩虹。这个图标的意思是:当前你使用的程序需要停止或者需要等待一些事情的完成,花费很长时间的话会很想知道到底发生了什么。
这是一个很糟糕的体验,现在已经是计算机的多核时代了,这种体验很差劲。 如果我们可以让另一个任务在另一个处理器核心上运行,并且我们可以知道它何时完成的,坐着等待是没有意义的。 可以让我们同时完成很多工作的模式,叫做异步编程。 这取决于程序的环境,可以是web浏览器,它提供了很多去异步运行task的API。
阻塞的代码
异步技术是非常有用的,尤其是在web领域。当一个web app在浏览器中运行时,它会执行一段密集的代码,并且不会把控制权交还给浏览器,浏览器此时可以看做是冻结状态。 浏览器的这种冻结状态,就叫做阻塞(block)。 浏览器阻塞后,就不会处理用户输入和执行其他的任务了,直到web app将控制权转交给浏览器。
我们看一下代码去了解下阻塞。
同步阻塞的例子:https://mdn.github.io/learning-area/javascript/asynchronous/introducing/simple-sync.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以后,alert按钮点击后看不到按钮的效果(其实click event listener已经添加到message queue中了),会在fill canvas完成后在执行alert。
为什么会出现这种阻塞的情况? 这是因为JavaScript是单线程的。所以我们再来看看线程的知识点。
线程
线程指的是程序可以用来完成任务的一个单独的处理器。 每个线程每次可以做一个task:
Task A --> Task B --> Task C
每个task会顺序执行;一个任务完成后,下一个任务才会开始。 像我们之前所说的,许多计算机现在拥有多核,所以可以同时做很多事。 支持多线程的程序语言可以利用多核去同时完成多个任务:
JavaScript是单线程的
JavaScript是单线程的。 即使是多核,我们也只能在单线程上执行任务,它叫做main thread。
js可以有一些工具去解决这个问题。比如web worker允许js开启一个新的独立的线程,我们可以在worker中去处理一些昂贵的计算,从而避免主线程的用户交互被阻塞。
使用worker我们优化了1千万次的date计算,从而使得段落的渲染不被阻塞。
web worker解决阻塞:https://mdn.github.io/learning-area/javascript/asynchronous/introducing/simple-sync-worker.html
worker线程解决阻塞问题:
worker.js
更多web worder的实践可查看:https://github.com/FrankKai/FrankKai.github.io/issues/181
异步的代码
异步代码是非常有用的,但是他们有自己的限制。主要的原因是 web worker不能访问到DOM。 获取不到DOM也就意味着无法直接更新UI。我们不能把绘制一百万次canvas的逻辑放到worker中,它只能做数值类的计算。
第二个问题是worker中的代码时不是不阻塞的,它也是同步的。当一个函数依赖多个函数的计算结果时,这样做就会有问题了。考虑下面的线程图:
假设Task A从服务器拉取了一张照片,任务B会为其增加一个类似图片的filter。如果在Task A还在运行的过程中去运行Task B的话,会报错,因为这个时候图片还是不能被访问到的。
在这个情况中,任务D用到了B和C的结果。如果我们保证这些结果同时可用,我们可以得到预期结果,但是正常情况往往不是这样。如果任务D发现其中之一不可用,会抛一个错误出来。
为了解决这个问题,浏览器可以运行准确的执行异步的动作。类似Promise这样的特性,可以使得异步得到保证,直到Promise返回结果之后,再执行其他操作:
Promise是web worker的优化版本??? 是的。Promise既可以保证main thread不被阻塞,还能准确的保证异步代码的执行顺序。
为什么Promise没有阻塞main thread? 因为Promise是microtask。
总结
现代浏览器在大量使用异步编程,从而使得浏览器同时可以做很多事情。 当你使用新的和更强大的API时,你会发现更多异步的场景。刚开始写异步代码时痛苦的,但是写得多了就熟能生巧了。
参考资料:https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Asynchronous/Concepts