75

解读 JavaScript 之事件循环和异步编程

 6 years ago
source link: https://www.oschina.net/translate/how-does-javascript-actually-work-part-4?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

解读 JavaScript 之事件循环和异步编程

欢迎阅读专门探索 JavaScript 及其构建组件的系列文章的第四章。 在识别和描述核心元素的过程中,我们还分享了关于构建 SessionStack 时需要遵循的一些经验法则,一个 JavaScript 应用必须是强大且高性能的,才能保持竞争力。

你有没有错过前三章? 你可以在这里找到它们:

这一次,我们将通过回顾如何克服在单线程环境中编程的缺点以及构建令人惊叹的 JavaScript UI 来扩展我们的第一篇文章。按惯例,在文章的最后我们将会分享 5 个关于如何用 async / await 编写更简洁代码的技巧。

翻译于 2017/12/14 15:46

为什么说单线程是一种限制?

在我们开始的第一篇文章中,我们思考了在调用堆栈(Call Stack)中进行函数调用时需要处理耗费大量时间的程序时会发生什么情况。

想象一下,例如,一个在浏览器中运行的复杂图像转换算法。

虽然调用堆栈具有执行的功能,但此时浏览器不能做任何事情  —— 它被停止下来。这意味着浏览器无法渲染,它不能运行任何代码,它卡住了。那么问题来了 - 你的应用用户界面不再高效和令人满意。

你的应用程序卡住了。

在某些情况下,这可能不是很关键的问题。但是,这是一个更严重的问题。一旦你的浏览器开始处理调用堆栈中的太多任务,它可能会停止响应很长一段时间。在这一点上,许多浏览器会通过抛出错误来处理上述问题,显示并询问是否应该终止页面:

这是很难看的,它完全毁了你的用户体验:

113844_a1ua_2896879.png
翻译于 2017/12/14 15:54

构建JavaScript程序模块

您可能正在将您的JavaScript应用程序写入一个单独.js文件,但是肯定的是您的程序由几个模块组成,其中只有一个将会立即执行,其余的将在稍后执行。 最常见的模块单位是函数。

大多数JavaScript新手开发者似乎都有这样的理解,即以后不一定要求立即发生。 换句话说,根据定义,现在无法完成的任务将以异步的形式完成,这意味着当您想到使用异步来处理时,将不会遇到上述浏览器停止的行为。

我们来看看下面的例子:

// ajax(..) is some arbitrary Ajax function given by a library
var response = ajax('https://example.com/api');

console.log(response);
// `response` won't have the response

您可能知道标准的Ajax请求并不是同步完成的,这意味着在执行代码的时候,ajax(..)函数还没有任何返回值来分配给用于返回的变量。

翻译于 2017/12/14 16:04

一种简单的“等待”异步函数返回结果的方式是使用callback的函数:

ajax('https://example.com/api', function(response) {
    console.log(response); // `response` is now available
});

需要说明一下:实际上,您可以创建同步的Ajax请求。 但永远不要这样做。 如果您发出同步的Ajax请求,则JavaScript应用的UI界面将被阻止渲染 - 用户将无法点击,输入数据,导航或滚动。 这将阻止任何用户与浏览器交互。 这是一个可怕的做法。

// This is assuming that you're using jQuery
jQuery.ajax({
    url: 'https://api.example.com/endpoint',
    success: function(response) {
        // This is your callback.
    },
    async: false // And this is a terrible idea
});

这是它的样子,但请不要这样做 - 不要毁掉你的网站:我们以一个Ajax请求为例。 你可以编写任何代码模块并异步执行。

这可以通过使用setTimeout(回调(callback),毫秒(milliseconds))函数来完成。 setTimeout函数的作用是设置一个在稍后发生的事件(一个超时)。 让我们来看看:

function first() {
    console.log('first');
}
function second() {
    console.log('second');
}
function third() {
    console.log('third');
}
first();
setTimeout(second, 1000); // Invoke `second` after 1000ms
third();

控制台中的输出如下所示:

first
third
second
翻译于 2017/12/14 16:15

分析事件循环

我们从一个奇怪的说法开始——尽管允许执行异步JavaScript代码(如我们刚才讨论的setTimeout函数),但直到ES6出现,实际上JavaScript本身从来没有任何明确的异步概念。 JavaScript引擎从来都只是执行单个程序模块而不做更多别的事情。

有关JavaScript引擎如何工作的详细信息(特别是Google的V8),请查看我们之前关于该主题的文章。

那么,谁来告诉JS引擎去执行你编写的一大段程序?实际上,JS引擎并不是孤立运行,它运行在一个宿主环境中,对于大多数开发人员来说,宿主环境就是一个典型的Web浏览器或Node.js。实际上,如今,JavaScript被嵌入到从机器人到灯泡的各种设备中。每个设备都代表一个包含JS引擎的不同类型的宿主环境。

所有环境中的共同点是一个称为事件循环的内置机制,它随着时间的推移处理程序中多个模块的执行顺序,并每次调用JS引擎。

这意味着JS引擎只是任何JS代码的一个按需执行环境。并调度事件的周围环境(JS代码执行)。

所以,例如,当你的JavaScript程序发出一个Ajax请求来从服务器获取一些数据时,你在一个函数(“回调函数”)中写好了“响应”代码,JS引擎将会告诉宿主环境:

“嘿,我现在暂停执行,但是每当你完成这个网络请求,并且你有一些数据,请调用这个函数并返回给我。

然后浏览器开始监听来自网络的响应,当响应返回给你的时候,宿主环境会将回调函数插入到事件循环中来安排回调函数的执行顺序。

翻译于 2017/12/14 16:32

我们来看下面的图表:

144844_QS3g_2896879.png

您可以在我们以前的文章中阅读更多关于内存堆和调用栈的信息。

这些Web API是什么? 从本质上讲,它们是你无法访问的线程,你仅仅只可以调用它们。 它们是浏览器并行启动的一部分。如果你是一个Node.js开发者,那么这些就相当于是C ++ API。

那么事件循环究竟是什么?

114046_FNxg_2896879.png

Event Loop有一个简单的工作机制——就是去监视Call Stack和Callback Queue。 如果调用栈为空,它将从队列中取出第一个事件,并将其推送到调用栈,从而更有效率的运行。

这种迭代在事件循环中被称为一“刻度(tick)”。 每个事件只是一个函数回调。

console.log('Hi');
setTimeout(function cb1() { 
    console.log('cb1');
}, 5000);
console.log('Bye');
翻译于 2017/12/14 16:43

现在执行一下这段代码,看发生了什么:

1、状态是清晰的。浏览器控制台没有输出,调用堆栈是空的。

140458_XFrF_2896879.png

2、console.log('Hi') 被添加到调用堆栈。

140420_G9Cj_2896879.png

3、执行 console.log('Hi').

140430_PQuC_2896879.png

4、console.log('Hi') 从调用堆栈中删除。

140412_gRIF_2896879.png
翻译于 2017/12/14 16:12

5、函数 setTimeout(function cb1(){...}) 添加到调用堆栈

114213_PVJI_2896879.png

6、执行函数 setTimeout(function cb1(){...}) 。浏览器用 Web API 创建一个定时器,定时器开始倒计时。

140350_18U7_2896879.png

7、函数 setTimeout(function cb1(){...}) 执行完成并从调用堆栈中删除。

140342_zgYU_2896879.png

8、console.log('Bye') 添加到调用堆栈。

114320_pwdl_2896879.png
翻译于 2017/12/14 16:19

9、函数 console.log('Bye') 被执行。

140327_QfdS_2896879.png

10、console.log('Bye') 从调用堆栈中删除。

140319_zDCc_2896879.png

11、在至少1500毫秒之后,定时器结束并且定时器将回调函数 cb1 放入回调函数队列里面。

140307_I4I7_2896879.png

12、事件循环从回调队列里面取出 cb1 并将其放入调用堆栈。

140253_wNbR_2896879.png
翻译于 2017/12/14 16:24

13、cb1 被执行并且 console.log('cb1') 被放入调用堆栈。

140240_8407_2896879.png

14、函数 console.log('cb1') 被执行。

140221_hrAb_2896879.png

15、console.log('cb1') 被从调用堆栈中删除。

140205_U6Tg_2896879.png

16、cb1 被从调用堆栈中删除。

140149_BcTy_2896879.png
翻译于 2017/12/14 16:28

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK