JavaScript 网络模式详解

Node.js 的崛起证明了 JavaScript 在 web 服务器上的地位,即使是高吞吐量的服务器。 不可否认的是,JavaScript 的血统仍然存在于用于客户端编程的浏览器中。

在这一章中,我们将探讨一些模式,以提高 JavaScript 在客户端上的性能和实用性。 我不确定所有这些都可以被认为是模式在严格的意义上。 然而,它们很重要,值得一提。

我们将在本章中考察的概念如下:

将 JavaScript 传递给客户端似乎是一个简单的命题:只要你能将代码传递给客户端,如何实现并不重要,对吧? 也不完全是。 在向浏览器发送 JavaScript 时,实际上有许多事情需要考虑。

组合文件

回到第 2 章组织代码,我们研究了如何使用 JavaScript 构建对象,尽管对此有不同的看法。 我认为 JavaScript 或任何面向对象的代码有一个类对一个文件的组织是一种很好的形式。 通过这样做,可以很容易地找到代码。 没有人需要在一个 9000 行长的 JavaScript 文件中查找这个方法。 它还允许再次建立层次结构,从而实现良好的代码组织。 然而,对开发人员来说好的组织不一定是对计算机来说好的组织。 在我们的案例中,拥有大量小文件实际上是非常有害的。 要理解其中的原因,您需要了解一些浏览器是如何请求和接收内容的。

当你在浏览器的地址栏中输入 URL 并点击输入,一系列的级联事件就会发生。 首先,浏览器会要求操作系统将网站名称解析为 IP 地址。 在 Windows 和 Linux(和 OSX)上都使用标准 C 库函数gethostbyname。 这个函数将检查本地 DNS 缓存,看看从名称到地址的映射是否已知。 如果是,则返回该信息。 如果没有,则计算机向 DNS 服务器发出请求。 通常,这是 ISP 提供的 DNS 服务器,但在更大的网络中,它也可以是本地 DNS 服务器。 DNS 服务器之间的查询路径可以在这里看到:

Combining files

如果该服务器上不存在记录,则请求将沿着 DNS 服务器链向上传播,试图找到一个了解该域的服务器。 最终传播在根服务器处停止。 这些根服务器是查询的停止点-如果他们不知道谁负责一个域的 DNS 信息,那么查找被认为是失败的。

一旦浏览器有了站点的地址,它就会打开一个连接并发送一个文档请求。 如果没有提供文档,则发送/。 如果连接是安全的,那么此时将执行 SSL/TSL 的协商。 设置加密连接需要一些计算费用,但这正在慢慢得到解决。

服务器将响应一个 HTML blob。 当浏览器接收到这个 HTML 时,它开始处理它; 浏览器不会等待整个 HTML 文档下载后才开始工作。 如果浏览器遇到 HTML 之外的资源,它将启动一个新的请求,以打开另一个连接到 web 服务器并下载该资源。 一个域的最大连接数是有限的,这样网络服务器就不会被淹没。 还应该提到的是,建立到 web 服务器的新连接会带来开销。 web 客户端和服务器之间的数据流可以在这张图中看到:

Combining files

应限制与 web 服务器的连接,以避免重复支付连接设置成本。 这就引出了我们的第一个概念:组合文件。

如果您遵循了在 JavaScript 中利用名称空间和类的建议,那么将所有 JavaScript 放在一个文件中就是一个简单的步骤。 只需要将文件连接在一起,一切就会照常工作。 可能需要对纳入的顺序给予一些轻微的注意和照顾,但不是典型的。

我们已经编写的前面的代码基本上是每个模式一个文件。 如果需要使用多个模式,那么我们可以简单地将文件连接在一起。 例如,构建器和工厂方法的组合模式可能如下所示:

var Westeros;
(function (Westeros) {
  (function (Religion) {
      …
  })(Westeros.Religion || (Westeros.Religion = {}));
  var Religion = Westeros.Religion;
})(Westeros || (Westeros = {}));
(function (Westeros) {
  var Tournament = (function () {
    function Tournament() {
  }
  return Tournament;
})();
Westeros.Tournament = Tournament;
…
})();
Westeros.Attendee = Attendee;
})(Westeros || (Westeros = {}));

问题可能会出现:一次应该组合和加载多少 JavaScript。 这是一个难以回答的问题。 一方面,当用户第一次到达站点时,最好预先加载整个站点的所有 JavaScript。 这意味着用户一开始将支付一定的费用,但在站点中旅行时不需要下载任何额外的 JavaScript。 这是因为浏览器会缓存脚本并重用它,而不是再次从服务器下载它。 然而,如果用户只访问了网站页面的一小部分,那么他们就会加载大量不必要的 JavaScript。

另一方面,拆分 JavaScript 意味着额外的页面访问会因检索额外的 JavaScript 文件而受到惩罚。 在这两种方法的中间有一个最佳点。 脚本可以被组织成块,映射到网站的不同部分。 在这里,使用合适的名称间距将再次派上用场。 每个名称空间可以组合成一个文件,然后在用户访问站点的该部分时加载。

最后,唯一有意义的方法是维护关于用户如何在网站上移动的统计数据。 在此基础上,建立了寻找最佳甜点点的优化策略。

小型化

将 JavaScript 组合到单个文件中解决了限制请求数量的问题。 然而,每个请求可能仍然很大。 我们又一次在什么使人类代码快速易读和什么使计算机代码快速易读之间产生了分歧。

我们人类喜欢描述性的变量名、丰富的空格和适当的缩进。 计算机不关心描述性名称、空格或适当的缩进。 事实上,这些东西增加了文件的大小,从而降低了代码读取的速度。

最小化是一个编译步骤,它将人类可读的代码转换为更小但等效的代码。 外部变量名保持不变,因为最小化器无法知道其他代码可能依赖于变量名保持不变。

例如,如果我们从第 4 章结构模式中的复合代码开始,简化后的代码如下:

var Westros;(function(Westros){(function(Food){var SimpleIngredient=(function(){function SimpleIngredient(name,calories,ironContent,vitaminCContent){this.name=name;this.calories=calories;this.ironContent=ironContent;this.vitaminCContent=vitaminCContent}SimpleIngredient.prototype.GetName=function(){return this.name};SimpleIngredient.prototype.GetCalories=function(){return this.calories};SimpleIngredient.prototype.GetIronContent=function(){return this.ironContent};SimpleIngredient.prototype.GetVitaminCContent=function(){return this.vitaminCContent};return SimpleIngredient})();Food.SimpleIngredient=SimpleIngredient;var CompoundIngredient=(function(){function CompoundIngredient(name){this.name=name;this.ingredients=new Array()}CompoundIngredient.prototype.AddIngredient=function(ingredient){this.ingredients.push(ingredient)};CompoundIngredient.prototype.GetName=function(){return this.name};CompoundIngredient.prototype.GetCalories=function(){var total=0;for(var i=0;i<this.ingredients.length;i++){total+=this.ingredients[i].GetCalories()}return total};CompoundIngredient.prototype.GetIronContent=function(){var total=0;for(var i=0;i<this.ingredients.length;i++){total+=this.ingredients[i].GetIronContent()}return total};CompoundIngredient.prototype.GetVitaminCContent=function(){var total=0;for(var i=0;i<this.ingredients.length;i++){total+=this.ingredients[i].GetVitaminCContent()}return total};return CompoundIngredient})();Food.CompoundIngredient=CompoundIngredient})(Westros.Food||(Westros.Food={}));var Food=Westros.Food})(Westros||(Westros={}));

您会注意到,所有的空格都被删除了,所有的内部变量都被替换为更小的版本。 同时,您可以发现一些众所周知的变量名保持不变。

最小化使这段代码节省了 40%。 使用 gzip(一种流行的方法)压缩来自服务器的内容流是无损压缩。 这意味着在压缩和未压缩之间有一个完美的双射。 另一方面,缩小是一种有损压缩。 一旦缩小了,就没有办法从缩小了的代码返回到未缩小的代码。

注释

你可以在http://betterexplained.com/articles/how-to-optimize-your-site-with-gzip-compression/上阅读更多关于 gzip 压缩的内容。

如果需要返回到原始代码,那么可以使用源映射。 源映射是一个文件,提供了从一种代码格式到另一种格式的转换。 它可以由现代浏览器中的调试工具加载,从而允许您调试原始代码,而不是难以理解的简化代码。 可以组合多个源映射,以实现从最小化代码到未最小化 JavaScript 再到 TypeScript 的转换。

有许多工具可以用来构造精简的和组合的 JavaScript。 Gulp 和 Grunt 是基于 JavaScript 的工具,用于构建管理 JavaScript 资产的管道。 这两种工具都需要外部工具(如 Uglify)来完成实际工作。 Gulp 和 Grunt 相当于 GNU Make 或 Ant。

内容交付网络

最后一个传递技巧是利用内容传递网络(CDNs)。 cdn 是由主机组成的分布式网络,其唯一目的是提供静态内容。 就像浏览器会在网站的页面之间缓存 JavaScript 一样,它也会缓存多个 web 服务器之间共享的 JavaScript。 因此,如果你的网站使用 jQuery,从著名的 CDN(如https://code.jquery.com/或微软的 ASP.net CDN)中拉 jQuery 可能会更快,因为它已经缓存了。 从 CDN 中提取也意味着内容来自不同的域,并且不会影响到服务器的有限连接。 引用 CDN 就像设置脚本标记的源指向 CDN 一样简单。

同样,需要收集一些指标,以确定是使用 CDN 更好,还是简单地将库滚动到 JavaScript 包中。 这类指标的例子可能包括执行额外 DNS 查找所需的额外时间以及下载大小的差异。 最好的方法是在浏览器中使用定时 api。

将 JavaScript 分发到浏览器的利弊在于需要进行试验。 测试一个数量的方法和测量结果将给最终用户最好的结果。

世界上有很多令人印象深刻的 JavaScript 库。 对我来说,改变我对 JavaScript 看法的库是 jQuery。 对于其他人来说,它可能是其他流行的库之一,如 MooTool、Dojo、Prototype 或 YUI。 然而,jQuery 已经迅速流行起来,并在撰写本文时赢得了 JavaScript 库之战。 在互联网上流量排名前一万的网站中,有 78.5%使用了 jQuery 的某些版本。 其他的图书馆连 1%都没有。

许多开发人员认为在这些基础库之上以插件的形式实现他们自己的库是合适的。 插件通常会修改库中暴露的原型,并添加额外的功能。 语法是这样的,对于最终开发人员来说,它似乎是核心库的一部分。

如何构建插件取决于你想要扩展的库。 尽管如此,让我们看看如何为 jQuery 和我最喜欢的库之一 d3 构建插件。 我们看看能不能找出一些共同点。

jQuery

jQuery 的核心是名为sizzle.js的 CSS 选择器库。 jQuery 使用 CSS3 选择器在页面上选择条目的所有漂亮方法都是由它实现的。 使用 jQuery 来选择页面上的元素,如下所示:

$(":input").css("background-color", "blue");

这里返回一个 jQuery 对象。 jQuery 对象的行为很像(尽管不是完全像)数组。 这是通过在 jQuery 对象上创建一系列编号为 0 到 n-1 的键来实现的,其中 n 是选择器匹配的元素数量。 这实际上是非常聪明的,因为它启用了数组访问器:

$($(":input")[2]).css("background-color", "blue");

虽然提供了一堆额外的函数,但索引处的项是纯 HTML 元素,没有用 jQuery 包装,因此使用了第二个$()

对于 jQuery 插件,我们通常希望插件扩展这个 jQuery 对象。 因为它是在每次触发选择器时动态创建的,所以我们实际上扩展了一个名为$.fn的对象。 这个对象被用作创建所有 jQuery 对象的基础。 因此,创建一个插件,将页面上所有输入的文本转换成大写,名义上很简单,如下所示:

$.fn.yeller = function(){
  this.each(function(_, item){
    $(item).val($(item).val().toUpperCase());
    return this;
  });
};

这个插件对于在公告板上发帖和老板填写表格特别有用。 插件遍历由选择器选择的所有对象,并将其内容转换为大写。 它也返回这个。 通过这样做,我们允许了附加函数的链接。 你可以这样使用这个函数:

$(function(){$("input").yeller();});

它更依赖于被赋给 jQuery 的$变量。 但情况并非总是如此,因为$是 JavaScript 库中的一个流行变量,可能是因为它是唯一一个不是字母或数字的字符,而且没有特殊含义。

为了解决这个问题,我们可以使用一个立即计算的函数,就像我们在第二章组织代码中所做的那样:

(function($){
  $.fn.yeller2 = function(){
    this.each(function(_, item){
      $(item).val($(item).val().toUpperCase());
      return this;
    });
  };
})(jQuery);

这里的额外好处是,如果我们的代码需要 helper 函数或私有变量,它们可以在同一个函数中设置。 您还可以传递任何必需的选项。 jQuery 提供了一个非常有用的$.extend函数,它可以在对象之间复制属性,这使它成为扩展传入的默认选项集的理想方法。 我们在前一章详细讨论了这个问题。

jQuery 插件文档建议尽可能少地使用插件污染 jQuery 对象。 这是为了避免多个想要使用相同名称的插件之间的冲突。 他们的解决方案是使用单个函数,该函数根据传入的参数具有不同的行为。 例如,jQuery UI 插件在对话框中使用了这种方法:

$(«.dialog»).dialog(«open»);
$(«.dialog»).dialog(«close»);

我更愿意这样称呼它们:

$(«.dialog»).dialog().open();
$(«.dialog»).dialog().close();

对于动态语言来说,两者之间并没有太大的区别,但我更希望有能够被工具发现的命名良好的函数,而不是神奇的字符串。

【正确答案】d

d3 是一个伟大的 JavaScript 库,用于创建和操作可视化。 在大多数情况下,人们将 d3 与可伸缩的矢量图形结合使用来生成图形,比如 Mike Bostock 的 hexbin 图:

d3

D3 试图对它所创造的视觉效果保持中立。 因此,不支持创建柱状图之类的东西。 但是,有一组插件可以添加到 d3 中,以支持各种各样的图形,包括前面的图中所示的 hexbin 图形。

更重要的是,jQuery d3 强调创建可链接函数。 例如,这段代码是创建列图的代码片段。 你可以看到所有的属性都是通过链接来设置的:

var svg = d3.select(containerId).append("svg")
var bar = svg.selectAll("g").data(data).enter().append("g");
bar.append("rect")
.attr("height", yScale.rangeBand()).attr("fill", function (d, _) {
  return colorScale.getColor(d);
})
.attr("stroke", function (d, _) {
  return colorScale.getColor(d);
})
.attr("y", function (d, i) {
  return yScale(d.Id) + margins.height;
})

d3的核心是d3对象。 在该对象之外挂起了许多用于布局、缩放、几何和许多其他名称空间。 除了完整的名称空间之外,还有用于执行数组操作和从外部源加载数据的函数。

d3创建插件首先要决定我们要插入代码的地方。 让我们构建一个插件来创建一个新的颜色比例。 颜色比例尺用于将一个值域映射到一个颜色范围。 例如,我们可能希望将以下四个值的域映射到四种颜色的范围:

d3

让我们插入一个函数来提供一个新的颜色比例,在本例中,这个颜色比例支持对元素进行分组。 比例尺是一个将域映射到范围的函数。 对于颜色刻度,范围是一组颜色。 例如,一个函数将所有偶数映射为红色,将所有奇数映射为白色。 在桌子上使用这个比例会导致斑马条纹:

d3.scale.groupedColorScale = function () {
  var domain, range;

  function scale(x) {
    var rangeIndex = 0;
    domain.forEach(function (item, index) {
      if (item.indexOf(x) > 0)
        rangeIndex = index;
    });
    return range[rangeIndex];
  }

  scale.domain = function (x) {
    if (!arguments.length)
      return domain;
    domain = x;
    return scale;
  };

  scale.range = function (x) {
    if (!arguments.length)
      return range;
    range = x;
    return scale;
  };
  return scale;
};

我们只需将这个插件附加到现有的d3.scale对象。 这可以通过简单地给出一个数组的数组作为域,一个数组作为范围使用:

var s = d3.scale.groupedColorScale().domain([[1, 2, 3], [4, 5]]).range(["#111111", "#222222"]);
s(3); //#111111
s(4); //#222222

这个简单的插件扩展了 d3 缩放的功能。 我们可以替换现有的功能,甚至包装它,这样就可以通过我们的插件来代理对现有功能的调用。

插件通常不难构建,但不同的库会有不同的插件。 密切关注库中现有的变量名是很重要的,这样我们就不会破坏它们,甚至破坏其他插件提供的功能。 有些人建议用字符串作为函数的前缀,以避免重复。

如果库在设计时就考虑到了这一点,那么就可能会有更多的地方可供我们使用。 一种流行的方法是提供一个包含可选字段的 options 对象,以便在我们自己的函数中作为事件处理程序挂钩。 如果没有提供任何内容,该函数将正常运行。

一次做两件事很难。 多年来,计算机世界的解决方案是使用多个进程或多个线程。 由于在不同的操作系统上实现的不同,两者之间的区别是模糊的,但是线程通常是进程的轻量级版本。 浏览器上的 JavaScript 不支持这两种方法。

从历史上看,浏览器上并不真正需要多线程。 JavaScript 用于操作用户界面。 在操作 UI 时,即使是在其他语言和窗口环境中,一次也只允许一个线程执行操作。 这避免了对用户来说非常明显的竞态条件。

然而,随着 JavaScript 越来越流行,越来越多复杂的软件被编写出来在浏览器中运行。 有时候,软件确实可以从后台执行复杂的计算中获益。

Web worker 提供了一种在浏览器中同时做两件事的机制。 尽管这是一个相当近期的创新,网络工作者现在已经在主流浏览器中得到了很好的支持。 实际上,工作线程是一个后台线程,它可以使用消息与主线程通信。 Web workers 必须在单个 JavaScript 文件中是自包含的。

使用 web 工作者是相当容易的。 我们将重新讨论几章前讨论斐波那契数列时的示例。 worker 进程监听的消息如下:

self.addEventListener('message', function(e) {
  var data = e.data;
  if(data.cmd == 'startCalculation'){
    self.postMessage({event: 'calculationStarted'});
    var result = fib(data.parameters.number);
    self.postMessage({event: 'calculationComplete', result: result});
  };
}, false);

在这里,每当我们得到一个startCalculation消息时,我们就启动一个新的fib实例。 fib只是先前的简单实现。

主线程从它的外部文件加载工作进程,并附加一些监听器:

function startThread(){
  worker =  new Worker("worker.js");
  worker.addEventListener('message', function(message) {
    logEvent(message.data.event);
    if(message.data.event == "calculationComplete"){
      writeResult(message.data.result);
    }
    if(message.data.event == "calculationStarted"){
      document.getElementById("result").innerHTML = "working";
    }
  });
};

为了开始计算,所需要做的就是发送一个命令:

worker.postMessage({cmd: 'startCalculation', parameters: { number: 40}});

在这里,我们传递我们想要计算的序列中的项数。 当计算在后台运行时,主线程可以自由地做任何它想做的事情。 当从工作人员那里收到消息时,它就被放在正常的事件循环中进行处理:

Doing two things at once – multithreading

如果您必须在 JavaScript 中执行任何耗时的计算,那么 Web worker 可能对您很有用。

如果你是通过使用 Node.js 来使用服务器端 JavaScript,那么就有一种不同的方法可以同时做多个事情。 Node.js 提供了分叉子进程的能力,并提供了一个类似于 web worker 的接口来在子进程和父进程之间进行通信。 但是,该方法对整个进程进行分叉,这比使用轻量级线程要消耗更多的资源。

还有一些工具可以在 Node.js 中创建轻量级的后台工作人员。 这些可能比派生子进程更接近于 web 端存在的内容。

系统,即使是最好的设计的系统,也会失败。 系统越大、分布越广,发生故障的概率就越高。 许多大型系统,如 Netflix 或谷歌,都有大量的内置冗余。 冗余不会降低组件发生故障的几率,但它们确实提供了备份。 切换到备份对最终用户来说通常是透明的。

断路器模式是提供这种冗余的系统的常见组件。 假设您的应用每 5 秒钟查询一次外部数据源,也许您正在轮询希望更改的一些数据。 当轮询失败时会发生什么? 在许多情况下,失败被忽视,投票继续进行。 在客户端,这实际上是一个很好的行为,因为数据更新并不总是至关重要的。 在某些情况下,失败将导致应用立即重试请求。 在紧循环中重新尝试服务器请求对客户机和服务器都是有问题的。 客户端可能会变得无响应,因为它在循环中花费更多的时间来请求数据。

在服务器端,试图从故障中恢复的系统每 5 秒钟就会受到可能是数千个客户机的猛烈攻击。 如果失败是由于系统过载,那么继续查询只会使情况变得更糟。

一旦达到一定数量的故障,断路器模式就会停止试图与正在故障的系统通信。 基本上,重复的故障会导致电路断开,应用停止查询。 你可以在这张图中看到断路器的一般模式:

Circuit breaker pattern

对于服务器来说,随着故障的堆积,客户端数量的减少为恢复提供了一些喘息的空间。 大量请求涌入并导致系统宕机的可能性被降到最低。

当然,我们希望断路器在某些点复位,以便恢复服务。 这有两种方法,一种是客户端定期轮询(比以前更少)并重置断路器,另一种是外部系统与服务已恢复的客户端通信。

退后

断路器模式的一个变体是使用某种形式的后退,而不是完全切断与服务器的通信。 这是许多数据库供应商和云提供商建议的一种方法。 如果我们最初的轮询间隔是 5 秒,那么当检测到故障时,将间隔更改为每 10 秒。 用越来越长的间隔重复这个过程。

当请求再次开始工作时,更改时间间隔的模式就会反转。 请求的发送距离越来越近,直到恢复原来的轮询间隔。

监视外部资源可用性的状态是使用后台工作者角色的理想场所。 这项工作并不复杂,但它完全脱离了主事件循环。

这再次减少了外部资源的负载,给了它更多的喘息空间。 它还使客户机不受太多轮询的负担。

使用 jQuery 的ajax函数的示例如下:

$.ajax({
  url : 'someurl',
  type : 'POST',
  data :  ....,
  tryCount : 0,
  retryLimit : 3,
  success : function(json) {
    //do something
  },
  error : function(xhr, textStatus, errorThrown ) {
    if (textStatus == 'timeout') {
      this.tryCount++;

if (this.tryCount <= this.retryLimit) {

 //try again

 $.ajax(this);

 return;

      }
      return;
    }
    if (xhr.status == 500) {
      //handle error
    } else {
      //handle error
    }
  }
});

您可以看到突出显示的部分重试查询。

这种回退方式实际上在以太网中使用,以避免重复的包冲突。

应用行为降级

您的应用调用外部资源可能有一个非常好的理由。 不查询数据源是完全合理的,但仍然希望用户能够与站点进行交互。 这个问题的一个解决方案是降低应用的行为。

例如,如果您的应用显示实时股票报价信息,但是用于传递股票信息的系统出现故障,那么可以交换一个小于实时的服务。 现代浏览器有一整套不同的技术,允许在客户端计算机上存储少量数据。 如果最新版本不可用,此存储空间是缓存某些数据的旧版本的理想空间。

即使是在应用向服务器发送数据的情况下,也有可能降低行为。 将数据更新保存在本地,然后在服务恢复时一并发送,这是通常可以接受的。 当然,一旦用户离开页面,任何后台工作都会终止。 如果用户不再返回站点,那么他们排队发送到服务器的任何更新都将丢失。

注释

一个警告:如果您采用这种方法,那么最好警告用户他们的数据是旧的,特别是如果您的应用是一个股票交易应用。

我之前说过 JavaScript 是单线程的。 这并不完全准确。 在 JavaScript 中有一个单一的事件循环。 用长时间运行的进程阻塞此事件循环被认为是一种糟糕的形式。 当你的贪婪算法占用了所有的 CPU 周期时,什么也不会发生。

当在 JavaScript 中启动一个异步函数(比如从远程服务器获取数据)时,大部分活动都发生在不同的线程中。 成功或失败处理程序函数在主事件线程中执行。 这是成功处理程序被写成函数的部分原因:它允许它们在上下文中轻松地来回传递。

因此,有些活动确实是以异步、并行的方式发生的。 当async方法完成后,结果被传递到我们提供的处理程序中,该处理程序被放入事件队列中,以便在下一次事件循环重复时提取。 通常,事件循环每秒运行数百次或数千次,这取决于在每个迭代中要做多少工作。

在语法上,我们将消息处理程序写成函数,并将它们连接起来:

var xmlhttp = new XMLHttpRequest(); 
xmlhttp.onreadystatechange = function() { 
  if (xmlhttp.readyState === 4){ 
    alert(xmlhttp.readyState); 
  }
;};

如果情况简单,这是合理的。 但是,如果您想使用回调的结果执行一些额外的异步操作,那么您将使用嵌套的回调。 如果你需要添加错误处理,也可以使用回调来完成。 等待多个回调返回和协调响应的复杂性迅速上升。

promise 模式提供了一些语法帮助来解决异步困难。 如果我们使用一个通用异步操作,比如使用 jQuery 通过 XMLHttp Request 检索数据,那么代码将同时接受一个错误和一个成功函数。 它可能看起来像以下内容:

$.ajax("/some/url",
{ success: function(data, status){},
  error: function(jqXHR, status){}
});

相反,使用 promise 会将代码转换成如下所示:

$.ajax("/some/url").then(successFunction, errorFunction);

在本例中,$.ajax方法返回一个 promise 对象,该对象包含一个值和一个状态。 该值在异步调用完成时填充。 状态提供了关于请求当前状态的一些信息:是否已完成,是否成功?

promise 也有许多函数被调用。 then()函数接受一个成功函数和一个错误函数,并返回一个额外的承诺。 如果 success 函数同步运行,则 promise 返回为已完成。 否则它将保持工作状态,即挂起状态,直到触发异步成功。

在我看来,jQuery 实现承诺的方法并不理想。 它们的错误处理不能正确地区分未能实现的承诺和已失败但已被处理的承诺。 这使得 jQuery 承诺与承诺的一般概念不兼容。 例如,不可能做到以下几点:

$.ajax("/some/url").then(
  function(data, status){},
  function(jqXHR, status){
    //handle the error here and return a new promise
  }
).then(/*continue*/);

即使已经提交了错误并返回了新的承诺,处理也将停止。 如果函数可以写成如下形式就更好了:

$.ajax("/some/url").then(function(data, status){})
.catch(function(jqXHR, status){
  //handle the error here and return a new promise
})
.then(/*continue*/);

关于在 jQuery 和其他库中实现承诺的讨论很多。 讨论的结果是,当前提出的承诺规范与 jQuery 的承诺不同,而且不兼容。 promise /A+是众多 promise 库(如when.js和 q)所满足的认证,它还构成了 ECMAScript-2015 中 promise 规范的基础。

promise 在同步函数和异步函数之间架起了一座桥梁,实际上把异步函数变成了可以像同步函数一样操作的东西。

如果 promise 听起来很像我们在前几章看到的懒惰评估模式,那么你是完全正确的。 承诺是使用惰性求值构造的,调用它们的动作会在对象内部排队,而不是一次求值。 这是功能模式的一个很好的应用,甚至支持以下场景:

when(function(){return 2+2;})
.delay(1000)
.then(function(promise){ console.log(promise());})

它极大地简化了 JavaScript 中的异步编程,对于任何本质上是高度异步的项目都应该加以考虑。

ECMAScript 2015 承诺在大多数浏览器上都得到很好的支持。 如果你需要支持一个较旧的浏览器,那么有一些很棒的垫片可以以最小的开销增加功能。

在检查从远程服务器检索 JavaScript 的性能时,大多数现代浏览器都提供了查看资源加载时间轴的工具。 这个时间轴将显示浏览器何时等待脚本下载以及何时解析脚本。 使用这个时间轴可以尝试找到加载一个脚本或一系列脚本的最佳方法。

在本章中,我们讨论了许多改善 JavaScript 开发体验的模式或方法。 我们研究了一些与浏览器交付有关的问题。 我们还研究了如何针对几个库实现插件,并推断了一般实践。 接下来,我们看看如何在 JavaScript 中使用后台进程。 断路器被建议作为一种保持远程资源获取正常的方法。 最后,我们研究了承诺如何改进异步代码的编写。

在下一章中,我们将花更多的时间研究消息传递模式。 我们看到了一些关于与 web 工作者打交道的内容,但我们将在下一节中详细介绍。

教程来源于Github,感谢apachecn大佬的无私奉献,致敬!

技术教程推荐

朱赟的技术管理课 -〔朱赟〕

邱岳的产品手记 -〔邱岳〕

代码精进之路 -〔范学雷〕

TypeScript开发实战 -〔梁宵〕

DDD实战课 -〔欧创新〕

Go 并发编程实战课 -〔晁岳攀(鸟窝)〕

玩转Vue 3全家桶 -〔大圣〕

中间件核心技术与实战 -〔丁威〕

快手 · 移动端音视频开发实战 -〔展晓凯〕