二、JavaScript 异步模型

在本章中,我们将研究异步编程背后的模型,为什么需要它,以及如何在 JavaScript 中实现它。

我们还将学习什么是编程模型及其重要性,从简单的编程模型到同步模型再到异步模型。 由于我们主要关注的是 JavaScript,它采用了异步编程模型,因此我们将比其他模型更详细地讨论它。

让我们从模型是什么和它们的重要性开始。

模型是在编程语言的编译器/解释器中设计和制造逻辑的基本模板,以便软件工程师可以使用这些逻辑逻辑地编写他们的软件。 我们使用的每一种编程语言都是根据特定的编程模型设计的。 由于软件工程师被要求解决一个特定的问题或自动化任何特定的服务,他们会根据需要采用编程语言。

没有指定特定语言来创建产品的固定规则。 工程师可以根据需要使用任何语言。

编程模型

理想情况下,我们将重点关注三种主要的编程模型,它们如下:

  • 第一个是单线程同步模型
  • 第二种是多线程模型
  • 第三个是异步编程模型

由于 JavaScript 采用异步模型,我们将更详细地讨论它。 但是,让我们先解释一下这些编程模型是什么,以及它们如何方便他们的终端用户。

单线程同步模型

单线程同步模型是一种简单的编程模型或单线程同步编程模型,其中一个任务跟着另一个任务。 如果有一个任务队列,则给第一个任务优先级,以此类推。 这是完成任务最简单的方法,如下图所示:

The single-threaded synchronous model

单线程同步编程模型是Queue数据结构的最好例子之一,它遵循First In First Out(FIFO)规则。 该模型假设,如果当前正在执行任务 2,则必须是在任务 1无错误完成后执行的,所有输出都是预期的或需要的。 这种编程模型仍然支持为简单的设备编写简单的程序。

多线程同步模型

与单线程编程不同,多线程编程中,每个任务都在单独的线程中执行,所以多个任务需要多个线程。 线程由操作系统管理,可以在具有多个进程或多核的系统上并发运行。

多个线程由操作系统或执行线程的程序来管理,这看起来非常简单; 这是一个复杂且耗时的任务,需要线程之间进行多级通信,才能在没有死锁和错误的情况下完成任务,如下图所示:

The multithreaded synchronous model

有些程序使用多进程而不是多线程来实现并行,尽管编程细节是不同的。

异步编程模型

在异步编程模型中,任务在单个控制线程中相互交错。

这个单个线程可能有多个嵌入式线程,每个线程可能包含几个相互关联的任务。 与线程的情况相比,这个模型更简单,因为程序员总是知道在内存中给定时间槽中执行的任务的优先级。

考虑一个任务,其中一个操作系统(或操作系统中的应用)使用某种场景来决定分配给一个任务多少时间,然后再给其他任务相同的机会。 操作系统从一个任务获取控制权并将其传递给另一个任务的行为称为抢占

注释

多线程同步模型也被称为抢占式多任务。 当它是异步的,它被称为合作多任务

The asynchronous programming model

在线程系统中,优先挂起一个线程并将另一个线程放入线程的操作不在程序员的手中; 控制它的是基本程序。 一般来说,它是由操作系统本身控制的,但异步系统不是这样。

在异步系统中,线程的执行和挂起的控制完全是由程序员决定的,线程不会改变它的状态,直到它被明确地要求这样做。

密度与异步编程模型

基于异步编程模型的所有这些特性,它需要处理它的密度。

由于对执行和优先级分配的控制掌握在程序员手中,他/她必须将每个任务组织为一系列立即执行的小步骤。 如果一个任务使用了另一个任务的输出,那么依赖的任务必须被设计成它可以接受输入的比特序列,而不是在一起; 这就是程序员如何编造他们的任务并设定他们的优先级。 异步系统的核心是当任务被迫等待或阻塞时,它的性能几乎可以显著地超过同步系统。

为什么我们需要阻塞任务?

任务被强制阻塞的一个更常见的原因是它正在等待执行一个 I/O 或从一个外部设备传输数据。 一个普通的 CPU 处理数据传输的速度比任何网络链路都快,这使得花费大量时间在 I/O 上的同步程序阻塞。 这样的程序也被称为阻塞程序

异步模型背后的整个思想是避免浪费 CPU 时间和避免阻塞位。 当一个异步程序遇到一个在同步程序中通常会被阻塞的任务时,它会执行一些仍然可以取得进展的其他任务。 因此,异步程序又称为非阻塞程序

由于异步程序花费更少的等待时间,并且给每个任务大致相同的时间,所以它取代了同步程序。

与同步模型相比,异步模型在以下场景中表现最好:

  • 有大量的任务,所以很可能总有至少一个任务可以取得进展
  • 这些任务执行大量的 I/O,导致同步程序在运行其他任务时浪费大量的时间阻塞
  • 这些任务在很大程度上彼此独立,因此几乎不需要任务间通信(因此一个任务需要等待另一个任务)。

记住前面所有的点,它将完美地突出一个典型繁忙的网络,例如客户机-服务器环境中的 web 服务器,其中每个任务代表一个从服务器请求一些信息的客户机。 在这种情况下,异步模型不仅会增加总体响应时间,而且还会通过一次服务更多的客户机(请求)来增加性能的价值。

为什么不使用更多的线程?

在这个点上,您可能会问,为什么不依赖单个线程而添加另一个线程呢? 答案很简单。 线程越多,它消耗的内存就越多,这反过来又会导致性能低下和周转时间延长。 使用更多的线程不仅会带来内存成本,还会影响性能。 对于每个线程,都会链接一定的开销来维护特定线程的状态,但是当绝对需要多线程时,就会使用它们,而不是每一个线程。

学习 JavaScript 异步模型

记住的知识,如果我们看到 JavaScript 异步模型是什么,我们现在可以清楚地联系到 JavaScript 中的异步模型,并理解它是如何实现的。

在非网络语言中,我们编写的大多数代码是同步的,也就是说,阻塞。 JavaScript 以不同的方式完成它的工作。

JavaScript 是一种单线程语言。 为了简单起见,我们已经知道单线程的实际含义——同一个脚本的两个字节不能同时运行。 在浏览器中,JavaScript 与其他进程的加载共享一个线程。 这些“内联过程”可以不同的浏览器从一个到另一个,但通常,JavaScript(JS)是在同一队列作为绘画,更新款式,和处理用户操作(一个活动在这些过程延迟之一)。

正如下图所示,每当异步(非阻塞)脚本在浏览器中执行时,它都会按照执行模式从上到下执行。 从页面加载开始,脚本转到创建 JavaScript 对象的文档对象。 然后脚本进入解析阶段,其中添加了所有节点和 HTML 标记。 解析完成后,整个脚本将作为异步(非阻塞)脚本加载到内存中。

Learning the JavaScript asynchronous model

JavaScript 如何实现异步模型

JavaScript 使用一个循环事件,它的循环被称为“tick”(就像时钟一样),因为它运行在 CPU 限定的时间段内。 解释器负责检查每一个 tick 是否为要执行的异步回调。 所有其他同步操作都发生在同一个 tick 内。 传递的时间值是不能保证的——没有办法知道下一次滴答需要多长时间,所以我们通常说回调将运行“尽快”; 尽管,一些电话甚至可能被掉线。

在 JavaScript 中,有四种实现异步模型的核心方法。 这四种方法不仅有助于提高程序的性能,还有助于提高代码的可维护性。 这四种方法如下:

  • 一个回调函数
  • 事件监听器
  • 发布者/订阅者
  • 承诺的对象

JavaScript 的回调

在 JavaScript 中,函数是头等公民,这意味着它们可以被视为对象,因为它们本身就是对象。 它们能做普通对象能做的事情,比如:

  • 存储在变量
  • 作为其他函数的扩展传递
  • 中创建函数
  • 在某些已处理数据机制的有效负载之后从函数返回

回调函数,也称为高阶函数,是一个作为参数传递给另一个函数(让我们把另一个函数称为otherFunction)的函数,回调函数在otherFunction内部被调用(执行)。

回调函数本质上是一种模式(一种针对常见问题的既定解决方案),因此使用回调函数也被称为回调模式。 因为函数是第一类对象,所以我们可以在 JavaScript 中使用回调函数。

既然函数是第一类对象,我们可以在 JavaScript 中使用回调函数,但是什么是回调函数呢? 回调函数背后的思想源自函数编程,它使用函数作为参数,因为实现回调函数就像将常规变量作为参数传递给函数一样简单。

回调函数的常见用法可以在以下代码行中看到:

$("#btn_1).click().click.function() {
alert ("Button one was clicked");
});

代码解释如下:

  • 我们将一个函数作为参数传递给click函数
  • 函数将调用(或执行)我们传递给它的回调函数

这是 JavaScript 中典型的回调函数用法,实际上,它在 jQuery 中得到了广泛使用。 我们将在第 8 章中详细介绍 jQuery 承诺。

阻塞功能

当我们正在讨论什么是 JavaScript 中的阻塞函数以及如何实现它时,我们中的许多人真的不清楚 JavaScript 中的阻塞函数是什么意思。

作为人类,我们有一个思想,设计以这样一种方式,它可以做很多工作,比如在阅读这本书的时候,你意识到你周围的环境,同时你能想到和类型,你可以跟别人当你开车。

这些例子是针对多线程模型的,但是在我们的人体中有任何阻塞功能吗? 答案是肯定的。 我们有一种阻塞功能因为我们的大脑和身体都有其他活动; 它只停留了一纳秒。 这种阻塞功能被称为打喷嚏。 当任何人打喷嚏时,所有与大脑和身体有关的功能都会在毫微秒内受阻。 这是人们很少注意到的。 JavaScript 的阻塞功能也是如此。

JavaScript 中的回调函数机制

这里的问题是,回调函数到底是如何工作的?

正如我们所知,函数就像 JS 中的第一类对象,我们可以以类似于变量的方式传递它们,并将它们作为函数返回,并在其他函数中使用它们。

当我们将回调函数作为参数传递给另一个函数时,我们只是传递了函数定义。 我们没有在参数中执行函数。 我们也没有像在执行函数时那样,在函数的末尾带有一对执行括号()

因为包含函数的参数中有回调函数作为函数定义,所以它可以在任何时候执行回调。

重要的是要注意回调函数不会立即执行。 它被“回调”,稍后仍然可以被包含函数通过 arguments 对象访问。

实现回调的基本规则

这里有一些基本的规则,你需要记住,当你实现回调。

回调通常很简单,但如果你正在制作自己的回调函数,你应该熟悉这个规则。 以下是一些你在处理回调函数时必须考虑的关键指针:

  • 使用命名或匿名函数作为回调函数
  • 向回调函数传递参数
  • 在执行 callback 之前确保它是一个函数

处理回叫地狱

由于 JavaScript 使用回调函数来处理异步控制流,使用嵌套的回调函数可能会变得混乱,并且在大多数情况下会失去控制。

在编写回调函数或从任何其他库中使用它时,需要非常小心。

如果回调函数没有被正确处理,会发生以下情况:

func1(param, function (err, res)) {
    func1(param, function (err, res)) {
        func1(param, function (err, res)) {
            func1(param, function (err, res)) {
                func1(param, function (err, res)) {
                    func1(param, function (err, res)) {
                        //do something
                    });
                });
            });
        });
    });
});

上述的情况通常被称为回调地狱。 这在 JavaScript 中很常见,这让工程师的生活很痛苦。 这也使得代码难以让其他团队成员理解,也难以维护以供进一步使用。 最极端的是,它让工程师感到困惑,使他/她很难记住在哪里传递控制权。

以下是回调地狱的快速提醒:

  • 永远不要让函数未命名。 给你的函数一个可以理解和有意义的名字。 名称必须显示它是一个正在执行某些操作的回调函数,而不是在 main 函数的参数中定义一个匿名函数。
  • 让你的代码看起来不那么可怕,更容易编辑、重构和修改。 大多数工程师都是按照自己的思路来编写代码,很少关注代码的美化,这使得后期维护代码变得很困难。 使用在线工具,如:http://www.jspretty.com来增加代码的可读性。
  • 将代码分成模块; 不要在单个模块中编写所有逻辑。 相反,编写简短有意义的模块,以便您可以导出执行特定任务的一段代码。 然后可以将该模块导入到更大的应用中。 这种方法还可以帮助您在类似的应用中重用代码,从而生成模块的整个库。

提示

下载示例代码

您可以从您的帐户http://www.packtpub.com下载您所购买的所有 Packt Publishing 书籍的示例代码文件。 如果您在其他地方购买了这本书,您可以访问http://www.packtpub.com/support并注册,直接将文件通过电子邮件发送给您。

The events

事件是特定动作发生时产生的信号。 JavaScript 知道这些信号并相应地做出响应。

事件是在用户工作时在常量流中触发的消息。 事件通常是基于用户的操作,如果编程得当,它们就会按照指令进行操作。 如果没有处理事件的处理程序,任何事件都是无用的。

由于 JavaScript 为程序员/工程师提供了一种很好的控制方式,这是他们处理事件、监视和响应事件的能力。 处理事件的能力越强,应用的交互性就越强。

事件处理机制

在 JavaScript 中,有两种常规方法来实现事件。 第一种是通过使用属性的 HTML,第二种是通过脚本。

为了让你的应用响应用户的操作,你需要做以下事情:

  1. 决定应该监视哪个事件。
  2. 设置事件处理程序,在事件发生时触发函数。
  3. 编写对事件提供适当响应的函数。

事件处理程序始终是 on 所感知的事件的名称,例如,由事件处理程序onClick()处理的单击事件。 此事件处理程序导致一个函数运行,该函数提供对事件的响应。

DOM -事件捕获和事件冒泡

文档对象模型(DOM)使得检测事件和分配相关事件处理程序来响应事件变得更加容易。 这将使用两个概念来实现的目的:事件捕获和事件冒泡。 让我们看看每种方法如何帮助检测和为正确的事件分配正确的处理程序。

在事件转换到目标文档时,捕获事件被称为事件的过程。 此外,它还具有捕获或拦截此事件的能力。

这使得整个往返过程递增地向下到它所包含的树元素,直到它到达它自己。

相反,事件冒泡是事件捕获的逆过程。 使用冒泡,事件首先由最内层的元素捕获和处理,然后传播到外部元素。

最常见的事件处理器列表

有完整的事件处理程序数组供不同的需求和情况使用,但让我们添加一些更常见和常规的事件处理程序。

注释

请记住,不同的浏览器可能会有不同的事件处理程序,当涉及到微软的 Internet Explorer 或 Mac 的 Safari 时,这个规范就会变得更加有限。

下面的列表非常方便并且不言自明。 为了更有效地使用这个列表,我建议开发人员/工程师将其方便地记录下来以供参考。

|

事件类别

|

什么时候触发事件

|

事件处理程序

| | --- | --- | --- | | 浏览器事件 | 页面完成加载 | Onload | |   | 从浏览器窗口移除该页面 | Onunload | |   | JavaScript 抛出错误 | Onerror | | 鼠标事件 | 用户点击元素 | onclick | |   | 用户在元素上双击 | ondblclick | |   | 在元素上按下鼠标按钮 | onmousedown | |   | 在元素上释放鼠标按钮 | onmouseup | |   | 鼠标指针移动到一个元素上 | onmouseover | |   | 鼠标指针离开一个元素 | Onmouseout | | 键盘事件 | 按下键 | onkeydown | |   | 释放键 | onkeyup | |   | 按下键并释放 | Onkeypress | | 形成事件 | 元素通过指针或制表符导航接收焦点 | onfocus | |   | 元素失去焦点 | onblur | |   | 用户在文本或文本区域框中选择类型 | onselect | |   | 用户提交表单 | onsubmit | |   | 用户重置窗体 | onreset | |   | 领域失去了焦点,内容在接收焦点后发生了变化 | onchange |

如前所述,这些是最常见的事件处理程序列表。 微软 ie 浏览器有一个单独的规格列表,可以在http://msdn.microsoft.com/en-us/library/ie/ms533051(v=vs.85).aspx上找到。

完整的事件兼容性列表可以在以下地方看到:

http://www.quirksmode.org/dom/events/index.html

响应事件的触发功能

JavaScript 事件需要触发才能得到响应。 事件处理程序负责响应这些事件,但有四种常用的方法以适当的方式触发事件:

  • JavaScript 伪协议
  • 内联事件处理程序
  • 作为对象属性的处理程序
  • 事件监听器

JavaScript 中的事件类型

在 JavaScript 中有许多不同类型的事件,如下所示:

  • 接口事件
  • 鼠标事件
  • 形成事件
  • W3C 事件
  • 微软的事件
  • Mozilla 的事件

接口事件

界面事件是由于用户的操作而发生的。 当用户点击任何元素时,他/她总是会引起一个点击事件。 当单击元素有特定目的时,会导致额外的接口事件。

鼠标事件

当用户将鼠标移动到链接区域时,将触发鼠标悬停事件。 当他/她点击它时,点击事件触发。

形成事件

表单识别提交和重置事件,当用户提交或重置表单时,可以预见地触发这些事件。 提交事件是任何形式的验证脚本的关键。

W3C 事件

当文档的 DOM 结构发生改变时,会触发 W3C 事件。 最常见的是 HTML 元素下面的 DOM 树被触发时触发的DOMSubtreeModified事件。

DOM 2 事件规范可以在http://www.w3.org/TR/2000/REC-DOM-Level-2-Events-20001113/events.html#Events-eventgroupings-mutationevents中看到。

微软事件

微软已经创建了一些自己的事件处理程序规范,(当然)只能在自己的平台上运行。 这可以在http://msdn.microsoft.com/en-us/library/ie/ms533051(v=vs.85).aspx上看到。

Mozilla 事件

Mozilla 有自己的规范,可以在https://developer.mozilla.org/en/docs/Web/API/Event上看到。

出版商/订阅者

事件是异步回调完成执行时通信的另一个解决方案。 对象可以成为发射器并发布其他对象可以监听的事件。 这是观察者模式的最好例子之一。

这种方法的性质类似于“事件监听器”,但比后者要好得多,因为我们可以查看“消息中心”,以便找出有多少信号存在,以及每个信号的订阅者数量,从而运行监视程序。

对观察者模式的简要描述

观察者提供了物体之间非常松散的耦合。 这提供了向收听者广播更改的能力。 这个广播可以是单个观察者的,也可以是一组等待侦听的观察者的。 主题维护一个观察员列表,它必须向这些观察员广播更新。 主题还为对象提供了注册自己的接口。 如果它们不在列表中,被试者就不会关心谁或什么在听。 这是主体与观察者解耦的方式,允许轻松地用一个观察者替换另一个观察者,甚至是一个主体,只要它维护相同的事件系列。

观察者的正式定义

以下是观察者的定义:

|   | 定义对象之间一对多的依赖关系,当一个对象改变状态时,它的所有依赖关系都被通知并自动更新。 |   | |   | ——四人帮 |

这个定义的来源是第 20 页设计模式:可重用的面向对象软件元素Addison-Wesley Professional

推拉模式

当创建主题/观察者关系时,您将希望向主题发送信息; 有时,这些信息可以是简短的,有时,它可以是额外的信息。 这种情况也可能发生在观察者发送一小块信息,作为回报,你的对象会查询更多的信息作为回应。

当发送大量信息时,它被称为push模型,当观察者查询更多信息时,它被称为pull模型。

|   | pull 模型强调被试对观察者的无知,而 push 模型假设被试对观察者的需求有所了解。 推模型可能会降低观察者的可重用性,因为 Subject 类对观察者类的假设可能并不总是正确的。 另一方面,拉模型可能效率不高,因为 Observer 类必须在没有 Subject 帮助的情况下确定什么更改了。 |   | |   | ——四人帮 |

Design Patterns: Elements of Reusable Object-Oriented SoftwareAddison-Wesley Professional

The advent of observer/ pusepub

这个观察者/推发布模式提供了一种思考如何维护应用不同部分之间关系的方法。 这也让我们知道应用的哪个部分应该被观察者和主题替换,以获得最大的性能和可维护性。 在 JavaScript 和其他语言中使用此模式时,请记住以下几点:

  • 使用此模式,可以将应用分解为更小、耦合更松的块,以改进代码管理和重用潜力
  • 当需要维护相关对象之间的一致性,而不需要使类紧密耦合时,观察者模式是最好的
  • 由于观察者和主题之间存在动态关系,它提供了很大的灵活性,当应用的不同部分紧密耦合时,这可能不容易实现

The 弊端 of observer/push-pub

因为每个图案都有自己的价格,所以这个图案也一样。 最常见的一种是由于其松散耦合的特性,有时很难维护对象的状态和跟踪信息流的路径,导致那些没有订阅该信息的人获得与主题无关的信息。

比较常见的缺点如下:

  • 通过将发布者与订阅者分离,有时很难保证应用的特定部分按预期运行
  • 这种模式的另一个缺点是订阅者不知道彼此的存在,也不知道在发布者之间切换的成本
  • 由于订阅者和发布者之间的动态关系,更新依赖关系很难跟踪

承诺的对象

promise 对象是异步编程模型实现的最后一个主要概念。 我们将把承诺看作一种设计模式。

Promise 在 JavaScript 中是一个相对较新的概念,但它已经存在很长时间了,并且已经在其他语言中实现了。

Promise 是一个抽象概念,它包含两个主要属性,使它们更容易使用:

  • 你可以用一个承诺附加多个回调
  • 值和状态(错误)被传递
  • 由于这些属性,promise 使得使用回调的通用异步模式变得容易

承诺可以定义为:

承诺是一个物体给另一个物体的可观察的标记。 promise 封装了一个操作,并在操作成功或失败时通知它们的观察者。

这个定义的来源是设计模式:可重用的面向对象软件元素Addison-Wesley Professional

由于这本书的范围围绕着承诺和它是如何实现的,我们将在第三章承诺范例中更详细地讨论它。

总结-异步编程模式

到目前为止,我们已经看到了异步模型是如何在 JavaScript 中实现的。 这是理解 JavaScript 有自己的异步编程模型实现的一个核心方面,它在异步编程模型中使用了很多核心概念。

  • 异步模式非常重要。 在浏览器中,一个非常耗时的操作应该异步执行,避免浏览器无响应时间; 最好的例子是 Ajax 操作。
  • 在服务器端,异步执行模式,因为环境是单线程的。 因此,如果允许同步执行所有 http 请求,服务器性能将急剧下降,很快就会失去响应能力。
  • 这就是为什么 JavaScript 实现在现代应用中被广泛接受的原因。 MongoDB、Node.js(作为服务器端 JavaScript)、Angular.js 和 Express.js(作为前端)这样的数据库,以及逻辑构建工具,都是 JavaScript 在整个行业实现程度很高的例子。 他们的栈通常被称为 MEAN 栈(MongoDB, Angular.js, Express.js 和 Node.js)

小结

在本章中,我们学习了什么是编程模型,以及它们如何在不同的语言中实现,从简单的编程模型到同步模型再到异步模型。

我们还看到了任务是如何在内存中组织的,以及如何根据它们的轮流和优先级来服务它们,以及编程模型如何决定服务什么任务。

我们还了解了异步编程模型如何在 JavaScript 中工作,以及为什么有必要学习异步模型的动态,以便编写更好的、可维护的和健壮的代码。

本章还解释了 JavaScript 的主要概念是如何实现的,以及它们在应用开发中的不同角度的作用。

我们还了解了如何在 JavaScript 中应用回调、事件和观察者,以及这些核心概念如何驱动当今的应用开发场景。

在下一章,第 3 章承诺范例中,我们将学习大量关于承诺的内容,以及它如何帮助应用变得更加健壮和可扩展。