本文是我翻译《JavaScript Concurrency》书籍的第一章,该书主要以Promises、Generator、Web workers等技术来说解JavaScript并发编程方面的实践。
完整书籍翻译地址: https://github.com/yzsunlei/j... 。因为能力有限,确定存在翻译不清楚甚至翻译错误的地方,欢迎朋友们提issue指出,感谢。
JavaScript并非一门与并发有关联的语言。事实上,它的特性还与并发应用是彻底不相符的。近几年来,它已经改变了不少,特别是ES2015新的语言特性。Promises已经在JavaScript中运用了好几年了; 只是如今,它成为JavaScript语言的
一种原生类型。 javascript
Generators是JavaScript的另外一个特性,它改变了咱们对JavaScript语言中并发的思考方式。Web workers也已经被浏览器支持好几年了,然而,咱们却运用的并很少。也许,是由于它与并发关系不大,并且更多的缘由是它在咱们的应用程序中关于并发扮演的角色的理解。 前端
本章的目标是探讨一些通用的并发思想,从并发是什么开始讲起。若是您在工做或学习中没有任何的并发编程经验,那很好,本章对您来讲是一个很不错的起点。若是您之前使用JavaScript或其余语言完成过并发编程相关的项目,那能够将本章做为复习,并使用JavaScript来回顾下。 java
咱们将以一些重要的并发原则来贯穿本章。这些原则是有价值的编程工具,咱们应该在编写并发代码时牢记在脑海中。一旦咱们学会应用这些原则,它们会告诉咱们咱们的并发设计是否正确,或者须要退后一步,问问本身真正想要实现的目标。这些原则采用自上而下的方法来设计咱们的应用程序。这意味着它们从一开始就是适用的,甚至在咱们开始编写代码以前。在整本书中,咱们将引用这些原则,所以若是您只阅读本章的一节,那最好是并发原则那部分。node
在咱们开始构建大规模并发JavaScript体系结构以前,让咱们先将注意力转移到咱们熟悉的、老旧的同步JavaScript代码上。这些JavaScript代码块,它们做为单击事件的回调结果,或者做为加载网页的运行结果。一旦它们开始执行,它们就不会中止。也就是说,它们是一直运行到完成的。在接下来的章节中,咱们将进一步深刻研究它们。git
咱们在整个章节中偶尔会看到术语“同步”和“串行”,它们可互换使用。它们都是指代一个接一个地运行代码语句,直到没有其余代码能够运行。
尽管JavaScript被设计为单线程,运行直到完成的,但Web的特性不得不使其复杂化。想一想Web浏览器及其全部可应用的模块。有用于渲染用户界面的文档对象模型(DOM),有用于获取远程数据的XMLHttpRequest(XHR)对象。如今让咱们先来看看JavaScript的同步特性和Web的异步特性。github
当代码是同步的,它很容易被理解。将咱们在屏幕上看到的指令集映射到头脑中的有序步骤至关容易; 这样作,而后那样作;判断一下,若是是,则执行此操做,依此类推。这种串行类型的代码处理很容易理解,由于没有什么特别的,假想代码的运行并不可怕。如下是一大块同步代码的示例:编程
相反的,并发编程并不容易理解。这是由于代码在编辑器并不能线性的去追踪。相反,咱们不断跳转,试图映射这段代码相对于那段代码所作的事情。时间是并行设计的重要因素; 这是违背大脑以天然方式来理解代码的东西。当咱们阅读代码时,咱们天然会在脑海中假想去执行它。这是咱们弄清楚它在作什么的方式。segmentfault
当代码实际执行不符合咱们的假想时,这时就会很崩溃。一般状况下,看代码就像看一本书 - 而看并发代码就像看一本虽然被编号,可是不按顺序编号的书。咱们来看一些简单的伪JavaScript代码:后端
var collection = ['a', 'b', 'c', 'd']; var results = []; for (let item of collection) { results.push(String.fromCharCode(item.charCodeAt(0))); } // ['b','c','d','e' ]
在传统的多线程编程环境中,线程与线程之间是异步运行。咱们使用多线程来充分利用当今大多数系统中的多核CPU,从而得到更好的性能。可是,这须要付出一些代价的,由于它迫使咱们去从新思考代码在运行时的执行方式。它再也不是一般的一步一步的去执行。一段代码能够与另外一个CPU中的其余代码一块儿运行,也能够在同一CPU上与其余线程一块儿运行。promise
当咱们将并发引入同步代码时,不少简易性就消失了 - 它就会是很烧脑的代码。这就是咱们编写并发代码的缘由:提出并发的前期假设的代码。随着本书的进展,咱们将详细阐述这一律念。使用JavaScript,并发设计很重要,由于这就是Web的工做方式。
JavaScript中的并发是一个很重要的方法,缘由是无论是从很是高的层次,仍是实现细节水平上来讲,Web是一个并发的东西。换句话说,网络是并发的,由于在任何一个时间点,都有大量的数据流过数英里的光纤,这些光纤包围着全球。它与部署到Web浏览器的应用程序自己以及后端服务器如何处理一连串的数据请求有关。
让咱们仔细看看浏览器以及在那里发生的各类异步操做。当用户加载网页时,页面执行的第一个操做就是下载和运行页面JavaScript代码。这自己就是一个异步操做,由于咱们的代码在下载时,浏览器会继续执行其余操做,例如渲染页面元素。
经过网络传输的异步数据是应用程序数据自己。加载页面并开始运行JavaScript代码后,咱们须要为用户展现数据。这其实是咱们的代码将要作的第一件事,以便用户能够尽快看到。一样,当咱们等待这些数据返回时,JavaScript引擎会将咱们的代码移动到它的下一组指令。对远程数据的请求,在继续执行代码以前不会等待响应:
页面元素所有渲染并填充数据后,用户开始与咱们的页面进行交互。这意味着事件被触发 - 单击元素将触发click事件。发送这些事件的DOM环境是一个沙盒环境。这意味着在浏览器中,DOM是一个子系统,与JavaScript解释器是分离的,后者运行咱们的代码。这种分离使某些JavaScript并发方案很难进行。咱们将在下一章深刻介绍这些内容。
有了全部这些异步的来源,毫无疑问,咱们的页面会因特殊的状况处理而变得臃肿,以应对不可避免地出现的特殊状况。异步思考是不符合逻辑的,所以这种类型的动态修补多是同步思考的结果。最好采用Web的异步特性。可是,同步网络可能会致使令用户没法忍受的体验。如今,让咱们进一步了解咱们在JavaScript体系结构中可能遇到的并发类型。
JavaScript是一种运行直到完成的语言。尽管在运行上存在并发机制,但并无解决它。换句话说,咱们的JavaScript代码不会在if语句中间转而去控制另外一个线程。这很重要的缘由是咱们能够选择一个有助于咱们思考JavaScript并发的抽象层次。让咱们看看在JavaScript代码中并发操做的两种类型。
异步操做的一个特征是它们不会阻止其余后续操做。异步操做并不必定意味着“一劳永逸”。相反,当那部分咱们等待的操做完成时,咱们会运行一个回调函数。这个回调函数与咱们的其余代码不一样步; 所以,这被称为异步。
在Web前端中,常常从远程服务器获取数据。这些请求操做相对较慢,由于它们必须经过网络链接。这些操做是异步的,由于咱们的代码会等待一些数据返回以便触发回调函数,这并不意味着用户必须停下来等待。此外,用户当前正在查看的任何页面都不太可能仅依赖于一个远程资源。所以,串行处理多个远程数据请求会产生很是糟糕的用户体验。
如下是异步代码的简单示例:
var request = fetch('/ foo'); request.addEventListener((response) => { //如今它已经返回了,可使用“response”作些事情了 }); //不要等待响应,当即更新DOM updateUI();
下载示例代码您能够从http://www.packtpub.com上的账户下载所购买的全部Packt Publishing书籍的示例代码文件。
若是您在其余地方购买了本书,能够访问http://www.packtpub.com/support并注册以直接经过电子邮件发送给您。
咱们不只限于获取远程数据,而是将其做为异步操做的一个案例。当咱们发出网络请求时,这些异步控制流实际上会离开浏览器。可是,限制在浏览器中的异步操做呢?以setTimeout()函数为例。它遵循与网络请求使用同样的回调模式。该函数已经过回调,将在稍后执行。然而,没有任何东西离开浏览器。相反,该操做排在任何的其余操做后面。这是由于异步操做仍然只是一个控制线程,由一个CPU执行。这意味着随着咱们的应用程序在规模和复杂性方面的增加,咱们就会面临并发扩展问题。可是,也许异步操做并不意味着只是解决单一CPU问题。
考虑在单个CPU上执行异步操做的更好方法多是想象一下杂技师抛球的场景。杂技师的大脑比做CPU,协调他的动做。被抛出的球是咱们操做的数据。咱们关心的只有两个基本动做 - 抛球和接球:
因为杂技师只有一个大脑,因此他不可能将本身的精力用于一次执行多项任务。然而,杂技师经验丰富,而且知道他不须要分出一小部分精力用于投掷或捕捉动做。一旦球到空中,他能够自由地将注意力转移到即将降落的球上。
别人在看这个杂技师的动做时,觉得他全神贯注于全部抛出的六个球,而实际上,他在同一个时间点会忽视其余五个在空中的球。
与异步同样,并行容许控制流继续而无需等待操做完成。与异步不一样,并行要取决于硬件。这是由于咱们不能在单个CPU上并行运行两个或更多个控制流程。然而,将并行与异步区分开来的主要是使用它的合理性方面。这两种并发方式解决了不一样的问题,而且须要不一样的设计原则。
有时,咱们但愿并行执行操做,不然若是同步执行则会耗费时间。想一想正在等待完成三项复杂操做的用户。若是每一个操做都须要10秒钟才能完成,那么这意味着用户必须等待30秒。若是咱们可以并行执行这些任务,咱们可使得总等待时间接近10秒。咱们以更少的成本得到更多,从而实现高效的用户交互体验。
这些都不是免费的。与异步操做同样,并行操做会将回调做为通讯机制。一般,设计并行很难,由于除了与worker线程进行通讯以外,咱们还要担忧手头的任务,也就是说,咱们但愿经过使用worker线程来实现什么?咱们如何将问题分解为更小的操做?如下是咱们开始引入并行代码的示例:
var worker = new Worker('worker.js'); var myElement = document.getElementById('myElement'); worker.addEventListener('message', (e) => { myElement.textContent = 'Done working!'; }); myElement.addEventListener('click', (e) => { worker.postMessage('work'); });
不要担忧这段代码运行时的机制,由于它们将在后面深刻讨论。须要注意的是,当咱们将一些线程放入工做环境时,咱们会向已经混乱的环境添加更多回调。这就是为何在咱们的代码中须要并发设计,这是本书的主要话题,从“第5章,使用Web workers”开始。
让咱们考虑下前一节中杂技师的比方。抛掷和捕获动做由杂技师异步执行; 也就是说,他只有一个脑(CPU)。可是假设咱们周围的环境在不断变化。咱们指望的杂技动做愈来愈多,一个杂技师不可能所有完成:
解决方案是为该表演中加入更多的杂技师。经过这种方式,咱们能够添加更多的计算能力,在同一时刻执行屡次抛掷和捕获操做。对于单个异步运行的杂技师来讲,这是不可能的。
咱们尚未解决好问题,由于咱们不能只让新添加的杂技师站在一个地方,并按照一个杂技师玩杂技的方式执行他们的动做。观众不少,更多样化,都须要被逗乐。杂技师须要可以有不一样的动做。他们须要在地板上不断的四处移动以让每个观众都能感受开心。他们甚至可能开始互相玩杂技。该由咱们来作一个可以实现这些杂技动做的设计。
既然咱们已经了解了并发的基础知识,以及它在前端Web开发中的做用,那么让咱们看一下JavaScript开发的一些基本并发编程原则。这些原则仅仅是咱们在编写并发JavaScript代码时为咱们的设计选择提供信息的工具。
当咱们应用这些原则时,它们迫使咱们退后一步,在咱们推动实施以前提出适当的问题。特别的,是关于为何和如何作的问题:
这是每一个并发原则的参考示图,在开发过程当中相互依赖。有了这个,咱们将把注意力转向每一个原则,以便进一步探究:
并发原则意味着利用现代CPU功能在更短的时间内计算结果。如今能够在任何现代浏览器或NodeJS环境中使用。在浏览器中,咱们可使用Web workers实现真正的并发。在NodeJS中,咱们能够经过生成新进程来实现真正的并发。从浏览器的角度来看,下图这就是CPU的大体样子:
因为目标是在更短的时间内进行更多的计算,咱们如今必须问本身为何要这样作?除了性能自己很是酷的事实以外,还必须对用户产生一些切实的影响。这个原则让咱们看着咱们的并行代码并想一想 - 用户从中得到了什么?答案是咱们可使用较大的数据集做为输入进行计算,而且不多可能因为JavaScript长时间运行,给用户带来无响应的体验。
重要的是仔细想一想并发的实际好处,由于当咱们这样作时,咱们会增长代码的复杂性,不然就没多大意义了。所以,若是用户看到相同的结果,获得一样的体验,那不管咱们作什么,并发原则可能都不适用。另外一方面,若是可扩展性很重要且数据集大小增长的可能性很大,那么并发的代码简单性的折衷多是值得的。在考虑并发原则时,这里有一个要遵循的检查清单:
同步原则是有关用于协调并发操做和抽象这些机制的一些方式。回调函数是一个具备深远根源的JavaScript概念。这是个很不错的方式选择,当咱们须要运行一些代码,但咱们不但愿立刻就运行它。咱们但愿当一些条件符合时再运行它。往大的方面讲,这种方式没有什么内在的问题。回调函数在单独使用时,是一种很简洁、方便、可读性强的一种并发模式。但在大量使用回调,而且在回调之间存在有大量的依赖时,就很使人崩溃了。
Promise API是ECMAScript 6中引入的核心JavaScript语法,用于解决当前应用程序所面临的同步问题。这是一个在实际使用回调时更简单的API(是的,咱们正在与嵌套回调作斗争)。Promise的目的不是要消除回调,而是要移除没必要要的回调。
如下是用于同步两个网络请求调用的Promise示例:
Promise的关键在于它们是一种通用的同步机制。这意味着它们不是专门针对网络请求,Web workers或DOM事件而产生的。咱们必须使用promises包装咱们的异步操做,并在必要时处理它们。这看起来不错的缘由是依赖promise接口的调用者并不关心promise中的内容。顾名思义,Promise是在某个时刻完成的。这可能须要5秒或更快。数据能够来自网络资源或Web用户。调用者并不关心,由于它假设并发,这意味着咱们能够在不破坏应用程序的状况下以任何方式实现它。这是上图的修改版本,它将为咱们提供实现promises的可能性:
当咱们学会用它来实现时,并发代码忽然变得更加易于理解了。Promise和相似的机制可用于同步网络请求,或仅仅是Web用户事件。但它们真正有能力使用它们来编写并发应用程序,其中默认是并发的。在考虑同步原则时,这里有一个能够参考的检查清单:
保护原则是关于节省计算和内存资源。这是经过使用惰性计算技术完成的。惰性的名称源于咱们在肯定咱们确实须要它以前不会实际计算新值的方法。想象一下渲染页面元素的应用程序组件。咱们能够传递此组件给它须要渲染的确切数据。这意味着在组件实际须要以前会进行屡次计算。它还意味着所使用的数据须要分配到内存中,以便咱们能够将它传递给组件。这种方法并无错。实际上,它是在JavaScript组件中传递数据的通用方法。
使用惰性计算的替代方法来实现相同的结果。不是计算要渲染的值,而是在要传递的结构中分配它们,咱们计算一项,而后渲染它。将此视为一种合做的多任务,其中较大的操做被分解为较小的任务,来回传递控制的焦点。
这是一种快速的计算数据方法,并将其传递给渲染UI元素的组件:
这种方法有两个很差的地方。首先,转换是预先进行的,这多是一项成本高昂的计算。若是组件发生了什么问题,没法以任何方式渲染它 - 因为某种限制?而后咱们执行了这个计算来转换不须要的数据。做为必然结果,咱们为转换后的数据分配了一个新的数据结构,以便咱们能够将它传递给咱们的组件。这种瞬时存储的结构实际上并无用于任何目的,由于它会当即被垃圾收集。让咱们来看看惰性方法是什么样子的:
使用惰性方法,咱们能够删除预先进行的成本昂贵的转换计算。相反,咱们一次只转换一项。咱们还可以删除转换后的数据结构前期分配的存储空间。相反,只有转换后的项将传递到组件中。而后,组件能够请求另外一项或中止。保护原则是使用并发做为仅计算所需内容,并仅分配所需内存的方法。
如下检查清单将帮助咱们在编写并发代码时考虑保护原则:
在本章中,咱们介绍了JavaScript中并发的一些目标。虽然同步JavaScript易于维护和理解,但异步JavaScript代码在Web上是不可避免的。所以,在编写JavaScript应用程序时,将并发做为默认的很是重要。
咱们感兴趣的有两种主要的并发类型 - 异步操做和并行操做。异步是关于操做在时间上排序,这给人一种事情都发生在同一时间的感受。若是没有这种类型的并发,对用户体验会形成很大的影响,由于它会不断地等待其余操做完成。并行是另外一种类型的并发,解决了另外一个不一样类型的问题,咱们但愿经过更快地计算结果来提升性能。
最后,咱们研究了JavaScript并发编程中的三种原则。并发原则是利用现代系统中的多核CPU。同步原则是关于建立抽象机制,使咱们可以编写并发代码,从咱们的功能代码中隐藏并发机制。保护原则使用惰性计算来仅计算所需内容并避免没必要要的内存分配。
在下一章中,咱们将把注意力转向JavaScript执行环境。为了有效地使用JavaScript并发,咱们须要对代码运行时实际发生的事情有充分的理解。
另外还有讲解两章nodeJs后端并发方面的,和一章项目实战方面的,这里就再也不贴了,有兴趣可转向https://github.com/yzsunlei/javascript_concurrency_translation查看。