八、网络工作器和承诺

在前几章中,我们在处理一般 JavaScript 开发中出现的常见 JavaScript 问题时,讨论了一些常见的性能问题。 现在,假设我们的项目能够支持更新的 JavaScript 特性,那么我们就可以使代码的性能比以前更好。

这就是网络工作器和承诺发挥作用的地方。 在本章中,我们将看看这两种方法,以及如何以及何时使用它们。 我们还将发现它们的局限性,并了解它们在高性能 JavaScript 方面的好处。

首先了解局限性

在深入研究 web 工作器和承诺之前,我们需要了解一个与 JavaScript 语言本身有关的问题。 正如前几章所提到的,JavaScript 是单线程的,不能同时运行两个或多个方法。

多年来,作为 JavaScript 开发人员,我们从未真正关心过线程,更不用说我们在本书中讨论的 JavaScript 内存问题了。 我们的大多数代码存在于浏览器中,并在同一页面上运行,或者内联,或者链接到同一服务器上的文件,以实现基本的网页功能。

随着 Web 的发展,对于高性能的应用来说,最初的前端编码变得越来越必要,因此需要新的方法来处理更大的 JavaScript 应用。 今天,我们将这些新特性视为ECMAScript 5特性集的一部分。

在 ECMAScript 5 中,许多这些特性被整合到 HTML5 堆栈中。 这个堆栈由 HTML5(DOCTYPEHTML标签),CSS 版本 3.0 和 ECMAScript 5 组成。

这些技术使 Web 比 AJAX 和 XHTML 开发时代更加强大。 限制在于这些特性都是尖端的,可能适用于所有浏览器,也可能不适用。 因此,在项目中实现这些新特性之前,通常需要一些预先考虑。

我们已经讨论过这些特性从第二章,与 JSLint提高代码性能,包括use strict声明,强迫浏览器抛出一个错误如果在 JavaScript 并不是严格意义上的书面或编码正确。 现在你可能会问,如果不是所有浏览器都支持use strict语句,为什么还要使用它呢? use strict语句的技巧在于,当它在较旧的浏览器中编码时,它会以字符串的形式显示出来并被忽略。

这是一件很棒的事情,因为即使在旧的浏览器中它被忽略了,我们仍然可以使用这个新特性并编写更高效的代码。 不幸的是,这并不能转化为 ECMAScript 5 中的所有特性; 这包括网络工作器和承诺。

因此,在本章中,让我们记住,从今以后,在使用代码示例时,我们需要将测试和编码的重点放在更新的浏览器上,如谷歌 Chrome、Opera、Firefox 或苹果的 Safari,甚至是遵循相同标准的更新版本的 Internet Explorer。

网络工作器

Web workers 为像我们这样的 JavaScript 开发人员提供了一种构建多线程 JavaScript 应用的方法; 这在较新的浏览器中工作,因为我们有一个叫做worker的对象。 工作对象只是一个我们传递逻辑的外部 JavaScript 文件。

这看起来可能有点奇怪。 自从 JavaScript 诞生以来,我们就没有使用过外部 JavaScript 文件吗? 这很公平,但是在浏览器如何处理 DOM 中文件的执行方面,web 工作器还是个新手。 让我们看看下面的示例图,看看浏览器如何读取文件:

Web workers

所以这里我们有一个单线程的 JavaScript 应用,一个DOMContentLoaded事件,和一个window.onload事件触发后不久,紧随其后的是简单命名为function1function2function3的函数。 现在,如果我们让function2()函数执行一些复杂的for循环,比如在console.log(Shakespeare)检查时间的同时计算 pi 500 万次,会怎么样? 好吧,我们可以从下面的图表中看到:

Web workers

正如我们所看到的,一旦浏览器调用function2(),它就会锁定并挂起,直到完成它的执行(假设运行代码的系统有足够的内存来执行)。 解决这个问题的一个简单方法是,“嘿,也许我们不需要检查时间,或者我们只需要计算一次 pi 来提高性能。” 但如果我们别无选择,只能用这种方式编写代码呢? 也许我们的应用必须这样工作,所以我们不得不编写一个复杂的、执行缓慢的函数,从而降低了性能; 为了应用的成功,必须启动具有该逻辑的功能。

如果我们必须构建这样的应用,那么我们的解决方案就是 web worker。 让我们来看看它是如何与单线程图进行比较的:

Web workers

在我们的示例中,我们可以在图表中看到,我们创建了一个新的 worker,它指向一个名为worker.js的外部 JavaScript worker 文件。 该 worker 以消息的形式发送响应。 对于 web workers,消息是我们在主机脚本和 worker 数据之间传递数据的方式。 它的工作方式类似于 JavaScript 中使用onmessage事件的任何其他事件。

那么,这在编码的应用中是什么样子的呢? 好吧,让我们来看看!

我们有一个代码示例显示在下面的截图中,以类似于前面的图表的方式构建:

Web workers

正如我们所看到的,这是一个简单的 HTML5 页面,第 11 行有一个script标签。 在第 13 行,我们首先声明了function1(),它向控制台输出信息消息; 在第 15 行中,我们启动一个新的计时器来查看我们的 worker 有多快。 它被称为Worker

接下来,在第 18 行 wedeclarefunction2(); 这就是有趣的地方。 首先,在第 19 行,我们声明了一个名为func2_Worker的变量; 这个变量的命名并不重要,但指出变量的实际名称总是一个好主意。 在这种情况下,我添加后缀_Worker到变量,然后我创建一个新的 web worker,使用关键字Worker,大写W

在圆括号中,我们添加了一个字符串,文件名,使用的是工作文件08_01-worker.js的相对路径。 让我们看看 worker 文件的内部。

Web workers

正如我们所看到的,worker 文件非常简单。 我们在第 1 行中声明了一个全局对象onmessage,它被赋值为一个函数for循环。 同样值得注意的是,我们可以通过selfthis关键字(例如:self.onmessage)来引用这个上下文。 您可能已经注意到我们还有一个参数oEvent,这是一个占位符,用于传递给我们使用data属性调用的 worker 的任何数据。 我们可以在第 3 行的postMessage函数中看到这一点。

postMessage函数是 ECMAScript 的内置函数,它可以将数据发送给指定的 worker,或者,如果没有指定 worker,它会向任何可能正在监听的父 JavaScript worker 发送消息。 现在让我们回到根 HTML 页面脚本,看看第 20 行; 如下截图所示:

Web workers

我们可以看到,通过调用我们的func2_Worker工作变量,我们可以使用该工作变量的onmessage属性,并在根页面上调用函数; 在这种情况下,您需要使用 worker 中使用的oEvent参数将消息记录回控制台。

这一切都很好。 但是我们如何传递数据呢? 好吧,这很简单。 第 24 行使用了func2_Worker变量,但使用了postMessage函数来处理工作对象,如前所述。 当我们给这个postMessage函数赋了一个工作变量时,它将向worker.js文件中使用的oEvent参数传递一个数据参数; 在这个例子中,它是一个字符串,上面写着"Processing a high performance JavaScript worker..."

最后,在第 32 行和第 35 行,我们有两个事件监听器。 一个是在我们的图中显示的DOMContentLoaded事件,作为执行线程中调用的第一个函数,它只是输出 DOM 已加载的日志消息; 后面是我们的window.onload函数,它也打印日志消息,但是它也触发函数 1、2 和 3,当页面加载时按顺序。 让我们在浏览器中加载它,看看使用 Chrome 的开发者工具选项会发生什么。 看看控制台面板的输出,如下图所示:

Web workers

好吧,这不是一个好迹象,因为我们可以看到一个错误出现在我们的控制台。 DOM LoadedPage Loaded日志信息和function1(): Called.日志信息一样,然后我们得到Uncaught SecurityError: Failed to construct 'Worker': Script at (file:url) cannot be accessed from origin 'null'错误信息。

那么这个是什么意思呢? 首先,我们必须理解使用 web worker 与使用 AJAX 类似。 如果您的代码不在服务器上,那么在跨系统共享或收集数据时会存在安全风险。 这不是错误的,但是在测试代码时,我们需要在本地服务器(如 Apache 或 IIS)上进行测试,这些服务器可以使用 HTTP 保护我们的内容。 在 Chrome 中,也有另一种方法来禁用这种战斗,但这只是在有限的测试。

使用本地服务器测试工人

可以在 OS X 和 Linux 上使用 Python 快速创建一个本地服务器; 现在,如果你不是一个 Python专家,不要担心,因为这是一个快速的一行终端代码,可以在几秒钟内启动服务器。

首先,打开终点站,设置它的路径; 这应该是您的文件所在的路径。 您可以使用 change directory 命令或cd来实现这一点。 下面是一个使用波浪号键设置活动用户桌面路径的示例:

cd ~/Desktop

一旦完成,我们可以用这个简单的一行 python 命令启动服务器,它调用一个内置的简单服务器方法,如下所示:

python -m SimpleHTTPServer

一旦我们按键,输入键,我们就可以启动服务器了。 我们可以通过在 Chrome 中输入http://127.0.0.1:8000查看服务器根; 然后我们应该看到要访问的文件列表。 此外,如果您需要关闭服务器,您可以退出终端或使用CTRL+Z手动关闭服务器。

Testing workers with a local server

现在继续,从页面中的.js文件中打开调用 worker 脚本的 HTML 文件。 我们应该看到 Chrome 的开发工具中的控制台面板显示了 1000 行从我们的工人 JavaScript 文件中通过“for loop”迭代的代码。

我们还可以看到,在第 5 行控制台中,console.timeEnd函数停止约 0.5 毫秒,表明它在处理循环之前被调用。 如下截图所示:

Testing workers with a local server

在我们继续之前,让我们检查下一个代码示例中的 worker 处理需要多长时间。 我们在没有使用 web worker 的情况下在页面本身中重新创建了循环的逻辑。 我们仍然使用console.time函数来测试线程运行多长时间,直到function3()被触发。 让我们来看看下面的代码:

Testing workers with a local server

因此,在第 19 行,我们删除了对 worker 文件的引用,它是一个.js文件,并将for循环移动到页面中。 这里它将循环 1000 次并打印到控制台。 现在在第 32 行,我们有了window.load 事件监听器,然后依次调用函数 1、2 和 3。

然后我们再次使用console.time函数来跟踪过程发生的时间。 由于这个代码示例现在是单线程的,我们应该会看到触发timeEnd函数需要更长的时间。 让我们运行我们的代码,看看下面的截图:

Testing workers with a local server

那不是坏! 在这里,我们的时间比多线程 Worker 示例要长得多,后者大约比 web Worker 慢 70 毫秒。 这并不是一个糟糕的性能提升; 它很小,但仍然有帮助。 现在,workers 的一个问题是,它们要花很多时间来触发存在于与主线程分开的线程中的下一个函数。 当函数异步完成时,我们需要某种方法来调用函数,为此我们有 JavaScript 的承诺。

承诺

JavaScript 承诺也是一种优化 JavaScript 代码的新方法; 承诺的概念是,你有一个链接到主函数的函数,并在它被写入时按顺序触发。 这是它的结构。 首先,我们使用Promise对象创建一个新对象,然后在圆括号内编写 main 函数,并将新的 promise 对象赋给一个变量。

在继续之前需要注意的一点是,JavaScript 承诺是特定于 EcmaScript 6 的。 因此,在本节中,我们需要在一个 EcmaScript 6-ready 浏览器(如谷歌 Chrome)中测试我们的代码。

接下来,对于我们的promise变量,我们使用then关键字,它实际上就像一个函数一样工作。 但是它只在我们的根承诺函数完成时触发。 此外,我们还可以一个接一个地链接then关键字,并顺序异步触发 JavaScript,以确保 promise 中的变量作用域将向下一个then函数承诺这些变量将具有设置值。 让我们来看看一个示例承诺,看看它是如何工作的:

Promises

在我们的代码示例中,我们有一个嵌入script标签的 HTML5 页面。 在我们的页面上有两个元素,我们使用button标签与makeAPromise()函数进行交互或查看,该makeAPromise()函数作为第 13 行附加的onclick事件。 在第 15 行,我们有一个div标记,其中idresults,其内部 HTML 为空。

接下来,在第 19 行创建makeAPromise函数,并在第 20 行设置一个名为promiseCountcount变量。 这就是我们创造承诺的地方。 在第 22 行中,我们创建了一个名为promiseNo1的变量,并使用一个新的Promise对象为它赋值。 在这里,你可以注意到我们如何开始用一个function作为参数的圆括号,从第 23 行开始,我们在函数中有一个resolve参数。 我们稍后再讨论。

Promise函数中,我们有一个简单的for循环,它将for循环的值乘以5then函数将其赋值给promiseCount变量。 为了完成我们的Promise对象的功能,注意一个新的关键字类型,resolve! resolve关键字是一种专门用于承诺的返回类型; 它设置 promise 的返回值。 还有其他承诺返回类型,如reject,允许我们返回一个失败的值。 然而,对于本例,我们保持了简单,只使用了resolve

现在,如果你还记得第 23 行,我们的Promise函数有一个带有resolve参数的内部函数。 虽然这看起来有点奇怪,但这是使我们的承诺生效的必要条件; 通过向函数中添加resolve,我们告诉我们的承诺,我们需要在Promise函数中使用 resolve 函数。 例如,如果我们需要resolvereject,我们会写为function (resolve, revoke) {}

回到第 29 行,我们用一个字符串赋值resolve,该字符串输出的值带有一些 copy 来填充div,但我们没有在这里赋值innerHTML属性; 这是用我们的promiseNo1.then函数完成的。 它的工作原理类似于 promise 的resolve函数。

最后在第 32 行,我们调用promiseNo1变量的实例,使用then函数,并再次用自己的内部函数包装圆括号。 我们可能注意到,在第 33 行,我们传入了一个名为promiseCount的参数。 这是结束在第 22 行中声明的Promise函数的resolve值。 然后我们在第 33 行再次使用了这个方法,在这里我们用它的innerHTML属性赋值results div元素。

测试一个真正的异步承诺

对于这个简单的例子,我们可以看到一个承诺是如何构造的,以及在链接时如何需要每个触发; 当我们链化承诺时,我们可以看到即使我们创建了一些导致执行延迟的单线程 JavaScript 代码,承诺仍然可以触发链化函数。 在本例中,它是一个setTimeout函数; 让我们看看这个新代码示例,如下面的截图所示:

Testing a true asynchronous promise

对于这个简单的例子,我们可以看到承诺链是如何在不破坏线程的情况下运行的。 这里,我们在第 20 行设置了一个timerCount变量; 然后打印到第 15 行找到的空results``div元素。 接下来,通过重用我们的promiseNo1变量有自己的承诺,我们创建一个随机的for循环使用Math.random()``timerCount,可以生成一个随机数,然后增加到 10000 年的for循环结束的时候。

最后,我们使用 resolve 函数返回我们的 promise,它被链接到第 31 行上的then函数; 这里我们有一个称为 response 的参数作为我们的resolve函数的值。 现在在第 33 行,我们有一个名为totalCount的变量,其中我们将响应参数和timerCount函数加在一起。

接下来,我们创建一个setTimeout函数,该函数在results``div元素后面加上第二行,打印由我们声明的totalCountvariable变量设置的时间,同时仍然使用timerCount函数作为超时值。 现在,我们链的最后一部分是第 40 行上的另一个then函数。 这里,我们再次添加了results``div元素,但您需要注意的是,我们是从第二个链接的then函数打印的。 让我们看看它是如何在 Chrome 中工作的,如下面的截图所示:

Testing a true asynchronous promise

看一下的输出。 在这里,我们可以看到,每次点击按钮,我们都会得到承诺链上每个点的数值计数。 我们在First count上有一个0的值,在Third count上有一个更大的随机数字。 等等! 这是第三项吗? 是的,请注意,第三次计数是在第一次之后; 这表明,即使在等待 for 循环处理时,第三个承诺仍在继续。

在下一行,我们看到一个更大的数值,在我们的行中标注了Second count; 如果我们继续单击按钮,应该会看到一致的模式。 使用承诺可以帮助我们编写多线程代码,只要我们不需要立即在链中使用特定的值。 通过使用 promise 将一些代码移出主 JavaScript 线程,我们还可以获得性能上的好处。

小结

在这一章中,我们回顾了如何使用 web worker,以及 web worker 在现实应用中在技术和概念上的局限性。 我们还与 JavaScript 承诺合作,学习了与承诺相关的常用关键字,如respondrevoke。 我们看到了如何使用then函数将我们的承诺与我们的主应用线程同步,以创建一个多线程的 JavaScript 函数。

在下一章中,我们将看到在移动设备(如 iOS 和 Android)上工作如何影响我们的性能,以及如何调试设备上的性能。