JavaScript 异步编程

news/2024/11/23 3:54:42/

JavaScript 异步编程

Javascript语言的执行环境是"单线程"(single thread)。

所谓"单线程",就是指一次只能完成一件任务。如果有多个任务,就必须排队,前面一个任务完成,再执行后面一个任务,以此类推。

这种模式的好处是实现起来比较简单,执行环境相对单纯;坏处是只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。常见的浏览器无响应(假死),往往就是因为某一段Javascript代码长时间运行(比如死循环),导致整个页面卡在这个地方,其他任务无法执行。
在这里插入图片描述

为了解决这个问题,Javascript语言将任务的执行模式分成两种:同步(Synchronous)和异步(Asynchronous)。

"同步模式"就是上一段的模式,后一个任务等待前一个任务结束,然后再执行,程序的执行顺序与任务的排列顺序是一致的、同步的;"异步模式"则完全不同,每一个任务有一个或多个回调函数(callback),前一个任务结束后,不是执行后一个任务,而是执行回调函数,后一个任务则是不等前一个任务结束就执行,所以程序的执行顺序与任务的排列顺序是不一致的、异步的

在这里插入图片描述
异步模式"非常重要。在浏览器端,耗时很长的操作都应该异步执行,避免浏览器失去响应,最好的例子就是Ajax操作。在服务器端,"异步模式"甚至是唯一的模式,因为执行环境是单线程的,如果允许同步执行所有http请求,服务器性能会急剧下降,很快就会失去响应。

本文总结了"异步模式"编程的4种方法,理解它们可以让你写出结构更合理、性能更出色、维护更方便的Javascript程序。

并发模型(Concurrency model)

目前,我们已经知道,JavaScript执行异步任务时,不需要等待响应返回,可以继续执行其他任务,而在响应返回时,会得到通知,执行回调或事件处理程序。那么这一切具体是如何完成的,又以什么规则或顺序运作呢?接下来我们需要解答这个问题。

注:回调和事件处理程序本质上并无区别,只是在不同情况下,不同的叫法。

前文已经提到,JavaScript异步编程使得多个任务可以并发执行,而实现这一功能的基础是JavScript拥有一个基于事件循环的并发模型。

堆栈与队列

介绍JavaScript并发模型之前,先简单介绍堆栈和队列的区别:

堆(heap):内存中某一未被阻止的区域,通常存储对象(引用类型);
栈(stack):后进先出的顺序存储数据结构,通常存储函数参数和基本类型值变量(按值访问);
队列(queue):先进先出顺序存储数据结构。

事件循环(Event Loop)

JavaScript引擎负责解析,执行JavaScript代码,但它并不能单独运行,通常都得有一个宿主环境,一般如浏览器或Node服务器,前文说到的单线程是指在这些宿主环境创建单一线程,提供一种机制,调用JavaScript引擎完成多个JavaScript代码块的调度,执行(是的,JavaScript代码都是按块执行的),这种机制就称为事件循环(Event Loop)。

JavaScript执行环境中存在的两个结构需要了解:

  • 消息队列(message queue),也叫任务队列(task queue):存储待处理消息及对应的回调函数或事件处理程序;
  • 执行栈(execution context stack),也可以叫执行上下文栈:JavaScript执行栈,顾名思义,是由执行上下文组成,当函数调用时,创建并插入一个执行上下文,通常称为执行栈帧(frame),存储着函数参数和局部变量,当该函数执行结束时,弹出该执行栈帧;

注:关于全局代码,由于所有的代码都是在全局上下文执行,所以执行栈顶总是全局上下文就很容易理解,直到所有代码执行完毕,全局上下文退出执行栈,栈清空了;也即是全局上下文是第一个入栈,最后一个出栈。

任务

分析事件循环流程前,先阐述两个概念,有助于理解事件循环:同步任务和异步任务。

任务很好理解,JavaScript代码执行就是在完成任务,所谓任务就是一个函数或一个代码块,通常以功能或目的划分,比如完成一次加法计算,完成一次ajax请求;很自然的就分为同步任务和异步任务。同步任务是连续的,阻塞的;而异步任务则是不连续,非阻塞的,包含异步事件及其回调,当我们谈及执行异步任务时,通常指执行其回调函数。

事件循环流程

关于事件循环流程分解如下:

  • 宿主环境为JavaScript创建线程时,会创建堆(heap)和栈(stack),堆内存储JavaScript对象,栈内存储执行上下文;
  • 栈内执行上下文的同步任务按序执行,执行完即退栈,而当异步任务执行时,该异步任务进入等待状态(不入栈),同时通知线程:当触发该事件时(或该异步操作响应返回时),需向消息队列插入一个事件消息;
  • 当事件触发或响应返回时,线程向消息队列插入该事件消息(包含事件及回调);
  • 当栈内同步任务执行完毕后,线程从消息队列取出一个事件消息,其对应异步任务(函数)入栈,执行回调函数,如果未绑定回调,这个消息会被丢弃,执行完任务后退栈;
  • 当线程空闲(即执行栈清空)时继续拉取消息队列下一轮消息(next tick,事件循环流转一次称为一次tick)。

使用代码可以描述如下:

var eventLoop = [];var event;var i = eventLoop.length - 1; // 后进先出while(eventLoop[i]) {event = eventLoop[i--]; if (event) { // 事件回调存在event();}// 否则事件消息被丢弃}

在这里插入图片描述

执行异步的方法

回调函数

回调函数就是一个函数,它是在我们启动一个异步任务的时候就告诉它:等你完成了这个任务之后要干什么。这样一来主线程几乎不用关心异步任务的状态了,他自己会善始善终。

function print() {document.getElementById("demo").innerHTML="RUNOOB!";
}
setTimeout(print, 3000);

这段程序中的 setTimeout 就是一个消耗时间较长(3 秒)的过程,它的第一个参数是个回调函数,第二个参数是毫秒数,这个函数执行之后会产生一个子线程,子线程会等待 3 秒,然后执行回调函数 “print”,在命令行输出 “RUNOOB!”。

当然,JavaScript 语法十分友好,我们不必单独定义一个函数 print ,我们常常将上面的程序写成:

setTimeout(function () {document.getElementById("demo").innerHTML="RUNOOB!";
}, 3000);

注意:既然 setTimeout 会在子线程中等待 3 秒,在 setTimeout 函数执行之后主线程并没有停止,所以:

setTimeout(function () {document.getElementById("demo1").innerHTML="RUNOOB-1!";
}, 3000);
document.getElementById("demo2").innerHTML="RUNOOB-2!";

这段程序的执行结果是:

RUNOOB-1!
RUNOOB-2!
异步 AJAX

除了 setTimeout 函数以外,异步回调广泛应用于 AJAX 编程。有关于 AJAX 详细请参见:https://www.runoob.com/ajax/ajax-tutorial.html

XMLHttpRequest 常常用于请求来自远程服务器上的 XML 或 JSON 数据。一个标准的 XMLHttpRequest 对象往往包含多个回调:

var xhr = new XMLHttpRequest();xhr.onload = function () {// 输出接收到的文字数据document.getElementById("demo").innerHTML=xhr.responseText;
}xhr.onerror = function () {document.getElementById("demo").innerHTML="请求出错";
}// 发送异步 GET 请求
xhr.open("GET", "https://www.runoob.com/try/ajax/ajax_info.txt", true);
xhr.send();
事件监听

另一种思路是采用事件驱动模式。任务的执行不取决于代码的顺序,而取决于某个事件是否发生。

还是以f1和f2为例。首先,为f1绑定一个事件(这里采用的jQuery的写法)。

f1.on('done', f2);

上面这行代码的意思是,当f1发生done事件,就执行f2。然后,对f1进行改写:

  function f1(){setTimeout(function () {// f1的任务代码f1.trigger('done');}, 1000);}

f1.trigger(‘done’)表示,执行完成后,立即触发done事件,从而开始执行f2。

这种方法的优点是比较容易理解,可以绑定多个事件,每个事件可以指定多个回调函数,而且可以"去耦合"(Decoupling),有利于实现模块化。缺点是整个程序都要变成事件驱动型,运行流程会变得很不清晰。

发布/订阅

我们假定,存在一个"信号中心",某个任务执行完成,就向信号中心"发布"(publish)一个信号,其他任务可以向信号中心"订阅"(subscribe)这个信号,从而知道什么时候自己可以开始执行。这就叫做"发布/订阅模式"(publish-subscribe pattern),又称"观察者模式"(observer pattern)。

这个模式有多种实现,下面采用的是Ben Alman的Tiny Pub/Sub,这是jQuery的一个插件。

首先,f2向"信号中心"jQuery订阅"done"信号。

jQuery.subscribe("done", f2);

然后,f1进行如下改写:

function f1(){setTimeout(function () {// f1的任务代码jQuery.publish("done");}, 1000);}

jQuery.publish(“done”)的意思是,f1执行完成后,向"信号中心"jQuery发布"done"信号,从而引发f2的执行。

此外,f2完成执行后,也可以取消订阅(unsubscribe)。

jQuery.unsubscribe("done", f2);

这种方法的性质与"事件监听"类似,但是明显优于后者。因为我们可以通过查看"消息中心",了解存在多少信号、每个信号有多少订阅者,从而监控程序的运行


http://www.ppmy.cn/news/22942.html

相关文章

谷粒商城:认证服务准备+60s短信验证

gatewa服务路由配置 - id: gulimall_auth_routeuri: lb://gulimall-auth-serverpredicates:- Hostauth.gulimall.com nginx改变 将静态资源全部转移 gulimall-auth-server启动类 SpringBootApplication EnableFeignClients EnableDiscoveryClient public class GulimallAut…

冒泡排序——“C”

各位CSDN的uu们你们好呀,今天小雅兰考试了呀,考的是计算机基础,希望不大......不过这些都不是问题,这不能阻止我对C语言的热情,那现在,就让我们进入冒泡排序的世界吧,我在写数组这篇博客的时候就…

c# 面试题 2023-2-6

1.POP、OOP、AOP区别。AOP解决什么问题 2.AOP的实现 3.装箱和拆箱 4.抽象类和接口的区别和使用场景 5.锁:乐观锁,悲观锁 举例说明应用场景 6.什么是死锁?如何保证你实现的锁结果不发生死锁 7.数组和链表的区别 8.WebAPI 和 webservice的区别 9.什么是线…

小程序抽象节点(插槽获取组件数据)

在开发过程中,封装组件经常会遇到这一种场景,例如这里我要封装一个swiper组件,但是不同的使用位置,布局结构样式这些需要不同的展现方式,这里我们就会使用插槽,在vue中我们可以使用slot-scope来获取组件的数…

软件工程——软件测试方法

软件测试是在软件投入生产性运行之前,对软件需求分析、设计规格说明和编码的最终复审,是软件质量控制的关键步骤。 软件开发过程是一个自顶向下、逐步细化的过程,而测试过程则是依相反的顺序安排的自底向上、逐步集成的过程。 一、白盒测试…

【Unet系列】(三)Unet++网络

一、UNet整体网络结构 Unet主要是探索encoder和decoder需要多大的问题,以此为基础,提出了融合不同尺寸Unet结构的网络。创新点就是把不同尺寸的Uent结构融入到了一个网络里。 二、结构的好处 (1)不管哪个深度的特征有效&#…

【王道数据结构】第五章(下) | 树 | 二叉树

目录 一、树的存储结构 1、双亲表示法(顺序存储): 2、孩子表示法(顺序链式) 3、孩子兄弟表示法(链式存储) 二、树、森林的遍历 1、树的先根遍历 2、树的后根遍历 3、层序遍历(队列实现) 4、森林的遍历 三、二叉排序树 …

Learning C++ No.5【类和对象No.4】

引言: 北京时间:2023/2/6/10:45,今天起床时间10:00,睡迟了,可能是因为昨天睡的有点晚,但是庆幸的是昨天没人叫我玩鹅鸭杀,我们把博客给更新了,哈哈哈!所以今…