谈谈JavaScript的异步实现

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/xuanzhu007/article/details/80613709

1 异步和同步

  • 同步:发出一个“调用”,没有得到结果,该“调用”就不会返回;

  • 异步:“调用”发出之后,这个调用就直接返回了,没有返回结果。

2 JavaScript的单线程

2.1 简单的settimeout

setTimeout(function () { while (true) { } }, 1000);
setTimeout(function () { alert('end 2'); }, 2000);
setTimeout(function () { alert('end 1'); }, 100);
alert('end');

执行的结果是弹出’end’、’end 1’,然后浏览器假死,就是不弹出‘end 2’。也就是说第一个setTimeout里执行的时候是一个死循环,这个直接导致了理论上比它晚一秒执行的第二个setTimeout里的函数被阻塞,这个和我们平时所理解的异步函数多线程互不干扰是不符的。

2.2 ajax请求回调

接着我们来测试一下通过xmlhttprequest实现ajax异步请求调用,主要代码如下:

var xmlReq = createXMLHTTP();//创建一个xmlhttprequest对象
function testAsynRequest() {
    var url = "/AsyncHandler.ashx?action=ajax";
    xmlReq.open("post", url, true);
    xmlReq.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
    xmlReq.onreadystatechange = function () {
        if (xmlReq.readyState == 4) {
            if (xmlReq.status == 200) {
                var jsonData = eval('(' + xmlReq.responseText + ')');
                alert(jsonData.message);
            }
            else if (xmlReq.status == 404) {
                alert("Requested URL is not found.");
            } else if (xmlReq.status == 403) {
                alert("Access denied.");
            } else {
                alert("status is " + xmlReq.status);
            }
        }
    };
    xmlReq.send(null);
}
testAsynRequest();//1秒后调用回调函数
 
while (true) {
}

理论上,如果ajax异步请求,它的异步回调函数是在单独一个线程中,那么回调函数必然不被其他线程“阻挠”而顺利执行,也就是1秒后,它回调执行弹出‘ajax’,可是实际情况并非如此,回调函数无法执行,因为浏览器再次因为死循环假死。

据上面两个例子,总结如下:

(1)所有的回调函数在同一个线程中执行;

(2)回调函数和主函数在同一个线程中执行;

(3)JavaScript引擎是单线程运行的,浏览器无论在什么时候都有且只有一个线程在运行JavaScript程序。

3 为什么JavaScript是单线程

JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。那么,为什么JavaScript不能有多个线程呢?这样能提高效率啊。

JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?

所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。

为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。

4 任务队列

单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。

如果排队是因为计算量大,CPU忙不过来,倒也算了,但是很多时候CPU是闲着的,因为IO设备(输入输出设备)很慢(比如Ajax操作从网络读取数据),不得不等着结果出来,再往下执行。

JavaScript语言的设计者意识到,这时主线程完全可以不管IO设备,挂起处于等待中的任务,先运行排在后面的任务。等到IO设备返回了结果,再回过头,把挂起的任务继续执行下去。

于是,所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。

具体来说,异步执行的运行机制如下。(同步执行也是如此,因为它可以被视为没有异步任务的异步执行。)

(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。

(2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。

(3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。

(4)主线程不断重复上面的第三步。

下图就是主线程和任务队列的示意图。

只要主线程空了,就会去读取"任务队列",这就是JavaScript的运行机制。这个过程会不断重复。

5 事件和回调函数

"任务队列"是一个事件的队列(也可以理解成消息的队列),IO设备完成一项任务,就在"任务队列"中添加一个事件,表示相关的异步任务可以进入"执行栈"了。主线程读取"任务队列",就是读取里面有哪些事件。

"任务队列"中的事件,除了IO设备的事件以外,还包括一些用户产生的事件(比如鼠标点击、页面滚动等等)。只要指定过回调函数,这些事件发生时就会进入"任务队列",等待主线程读取。

所谓"回调函数"(callback),就是那些会被主线程挂起来的代码。异步任务必须指定回调函数,当主线程开始执行异步任务,就是执行对应的回调函数。

"任务队列"是一个先进先出的数据结构,排在前面的事件,优先被主线程读取。主线程的读取过程基本上是自动的,只要执行栈一清空,"任务队列"上第一位的事件就自动进入主线程。但是,由于存在前面提到的"定时器"功能,主线程首先要检查一下执行时间,某些事件只有到了规定的时间,才能返回主线程。

6 JavaScript引擎

一个流行的JavaScript引擎的例子是Google的V8引擎。 例如,在Chrome和Node.js中使用的就是V8引擎。 下面是一个简化了的V8视图:

引擎由两个主要组件组成:

  • 内存堆 - 这是内存分配发生的地方

  • 调用堆栈 - 这是代码执行时的堆栈帧

6.1 运行时

浏览器中有几乎所有的JavaScript开发者都使用过API(例如“setTimeout”)。 但是,这些API不是由引擎提供的。

所以,除了引擎,实际上还有更多,例如那些被浏览器提供的称为Web API的东西,如DOM,AJAX,setTimeout等等。

此外,我们还有使用广泛的事件循环和回调队列

6.2 调用栈

JavaScript是一种单线程编程语言,这意味着它只有一个调用栈。 因此,它一次只可以做一件事。

调用栈是一个数据结构,它记录了程序执行到哪个地方。 如果进入一个函数,它就放在栈顶。 如果从一个函数返回,会从栈顶弹出,如同所有的堆栈所做的。

我们来看一个例子。 看看下面的代码:

function multiply(x, y) {
    return x * y;
}
function printSquare(x) {
    var s = multiply(x, x);
    console.log(s);
}
printSquare(5);

当引擎开始执行这个代码时,调用栈是空的。 之后,步骤如下:

调用堆栈中的每个条目称为堆栈帧。

这正说明了抛出异常时堆栈如何构建的 - 这基本上是异常发生时的调用堆栈的状态。 看看下面的代码:

function foo() {
    throw new Error('SessionStack will help you resolve crashes :)');
}
function bar() {
    foo();
}
function start() {
    bar();
}
start();

如果这段代码在Chrome中执行(假设这个代码在一个名为foo.js的文件中),那么会产生下面的调用栈:

7 JavaScript引擎线程和其它侦听线程

上图t1-t2...tn表示不同的时间点,tn下面对应的小方块代表该时间点的任务。

上图中,定时器和事件都按时触发了,这表明JavaScript引擎的线程和计时器触发线程、事件触发线程Http请求线程 是四个单独的线程,即使JavaScript引擎的线程被阻塞,其它三个触发线程都在运行。

浏览器内核实现允许多个线程异步执行,这些线程在内核制控下相互配合以保持同步。假如某一浏览器内核的实现至少有三个常驻线程: JavaScript引擎线程,事件触发线程,Http请求线程,单线程的JavaScript引擎与另外那些线程是怎样互动通信的呢?虽然每个浏览器内核实现细节不同,但这其中的调用原理都是大同小异。

​ 线程间通信:JavaScript引擎执行当前的代码块,其它诸如setTimeout给JS引擎添加一个任务,也可来自浏览器内核的其它线程,如界面元素鼠标点击事件,定时触发器时间到达通知,异步请求状态变更通知等.从代码角度看来任务实体就是各种回调函数,JavaScript引擎一直等待着任务队列中任务的到来.由于单线程关系,这些任务得进行排队,一个接着一个被引擎处理。(生产者-消费者

GUI渲染线程:

该线程负责渲染浏览器界面HTML元素,当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行。该线程与JavaScript引擎线程是互斥的,这容易理解,因为 JavaScript脚本是可操纵DOM元素,在修改这些元素属性同时渲染界面,那么渲染线程前后获得的元素数据就可能不一致了。

在JavaScript引擎运行脚本期间,浏览器渲染线程都是处于挂起状态的,也就是说被”冻结”了.

所以,在脚本中执行对界面进行更新操作,如添加结点,删除结点或改变结点的外观等更新并不会立即体现出来,这些操作将保存在一个队列中,待JavaScript引擎空闲时才有机会渲染出来.

8 NodeJS端的异步机制

NodeJS 的异步实现和浏览器端实现有所不同。在 NodeJS 中 Libuv 为 Node.js 提供了跨平台,线程池,事件池,异步 I/O 等能力,是 Node.js 如此强大的关键。Libuv 为上层的 Node.js 提供了统一的 API 调用,使其不用考虑平台差距,隐藏了底层实现。Libuv 本身就是异步和事件驱动的,所以,当我们将 I/O 操作的请求传达给 Libuv 之后,Libuv 开启线程来执行这次 I/O 调用,并在执行完成后,传回给 Javascript 进行后续处理。

总结来说,一个异步 I/O 的大致实现流程如下:

发起 I/O 调用

1 用户通过 Javascript 代码调用 NodeJS 核心模块,将参数和回调传入核心模块;

2 NodeJS 核心模块将传入参数和回调封装为一个请求对象;

3 将这个请求对象推入到I/O线程池中等待执行;

4 Javascript 发起的异步调用结束,Javascript 线程继续执行后续操作。

执行回调

1 异步任务完成之后,会将结果存放在请求对象的 result 属性上,并发出操作完成通知;

2 每次事件循环时会检查 I/O 线程池中是否存在已经完成的 I/O 操作,如果有就将请求事件加入到I/O观察者队列当中(事件队列),之后当作事件处理;

3 处理I/O观察者事件时,会将之前封装在请求对象中的回调函数取出,并将 result 参数传入执行,以完成 Javascript 回调的目的。

我们知道 NodeJS 非常适合开发 IO 密集型应用,但并不适合开发 CPU(计算) 密集型应用。为什么会这样呢?因为 NodeJS 异步的天性,在处理并发 IO 的时候不会阻塞主线程,实际上这种异步是借助于多线程实现的,IO 任务完成之后排队等待主线程执行。因为 NodeJS 主线程执行同步代码的速度非常之快,所以完全可以 hold 得住大规模的并发请求。但这也有存在例外,如果 NodeJS 主线程在执行同步任务的时候遇到一些计算量非常大,或者执行循环太久,等非常耗时的操作的时候,就会导致后续的代码以及事件队列里已完成的 IO 任务迟迟得不到执行,严重拖垮 NodeJS 的性能。

9 JS实现异步编程的几种方法

(1)回调函数;

(2)事件监听;

(3)发布/订阅(观察者模式);

(4)Promise;

 

猜你喜欢

转载自blog.csdn.net/xuanzhu007/article/details/80613709