eightHundreds / web-clipper-store

网页采集工具存储区域
5 stars 0 forks source link

漫话JavaScript与异步·第一话——异步:何处惹尘埃 - 大唐西域都护 - 博客园 #42

Open eightHundreds opened 3 years ago

eightHundreds commented 3 years ago

自 JavaScript 诞生之日起,频繁与异步打交道便是这门语言的使命,并为此衍生出了许多设计和理念。因此,深入理解异步的概念对于前端工程师来说极为重要。

什么是异步?

程序是分 “块” 执行的。最常见的 “块” 是函数。在一个块内,语句大体上从上到下依次执行(除了 “声明提升” 等个别例外,但那是发生在编译阶段),这些语句便是同步代码;而有一些语句,即所谓的回调函数,并不会按照 “正常的” 顺序执行,而是会在将来的某一时刻被调用执行,这部分语句便是异步代码。

也就是说,同步和异步的本质区别在于:一个现在执行,一个将来执行。此处的 “现在” 和“将来”与时间长短无关,而是指“是否在同一个块内执行”。

举个例子:

// **同步** var data = 0; // getData 是某种阻塞式的 IO 操作 data = getData(); // IO 操作耗时 10 秒 console.log(data); // 语句 A

// **异步** var data = 0; // ajax 是某个库函数 ajax('www.someurl.com', function(res) { data \= res; console.log(data); // 语句 B }); // ajax 操作耗时 1 秒 console.log(data); // 语句 C

其中语句 A、C 是同步代码,而 B 位于异步块内,也就是说,“同步”部分的三条语句在同一个块内执行,而 “异步” 部分的代码被分成了两个执行块。如果把这两段代码放在两个浏览器里,同时开始执行,则这三条语句从时间线上看完成的顺序是:C(几乎立即)->B(1 秒后)->A(10 秒后),可见同步、异步的概念与时间上的先后没有必然关系。

想象如下场景,一群代码语句正依次排队进入一家名叫 “CPU” 的电影院看电影,突然,其中一条语句被检票员 “JS 引擎” 拦下了,检票员对该语句说:“你是异步代码,不属于这个场次,请到休息区等候。请放心,我们已经记住你了,到了你的场次我们会通知的。”

“那我该看哪场电影呢?”

“这个不一定,有可能下一场就轮到你,也有可能等好几场。”

“那我要等多久呢?”

“这也不一定,等候时间从无限趋近于 0 到正无穷大都有可能。”

“……”

语句 B 所在的回调函数就是那个被拦下的苦命的异步代码。

JS 中的异步

本文开头说,JS 是为异步而生的。为什么这么说呢?

首先,JS 是为了给网页提供用户交互功能而发明的脚本语言,而用户行为是无法预知的,页面只能在用户动作时被动响应,因此响应代码必然是异步执行的。

另外,自从发明了 ajax,JS 还承担了网络通信的任务,这是一种时间无法预知的 IO 操作,而为了不阻塞用户界面,IO 也必须做成异步的。

上面说了两个异步场景,加上第三个,便构成了 JS 的三大异步来源:用户交互IO定时器

定时器很有意思,因为它是唯一一个创建 “自我控制” 的异步代码的方式,可以大致控制异步代码被调用的时机(但无法精确控制)。看如下代码:

// **定时器** var data = 0; // 将时延设为 0 setTimeout(function() { data \= 1; console.log(data); // 语句 A }, 0); console.log(data); // 语句 B

试问,A 和 B 谁先执行?

答案是 B,因为 A 是异步代码嘛,必须等到 “下一场” 才能执行。

那么下一个问题来了,JS 是如何实现异步调度的?

事件队列

答案就是事件队列。接触过前端的人应该或多或少知道这个概念。

JS 引擎(如 Chrome 的 V8)本身只负责编译和执行宿主环境 “喂” 给它的语句,事件队列由宿主环境提供。当引擎发现回调代码时,它便通知宿主环境在 “适当” 的时候来调用该回调。但是既然是异步代码,那就存在一个问题:可能同一个时间有多个回调被触发,也有可能一个回调正在执行时另一个回调被触发了。JS 又是单线程,不能并行处理多个回调。这种情况下怎么办呢?学过操作系统的我们毫不犹豫作答:弄一个队列,FIFO 啊!于是就有了事件队列。队列里的其实并不是“事件”,而是一个个异步代码块(回调函数)。每当一个“事件”(不只是 DOM 事件)被触发,相应的回调函数便被加入队列,等待被依次调用。调用一个回调函数的过程被称为一个 tick。

回顾上一部分的代码,setTimeout 实质上是在指定时延之后把回调函数加入事件队列的队尾,因此即使时延为 0,也一定比当前 tick 更晚执行,而且开始执行的时间往往会稍晚于设定的时延。

有一个地方值得注意:没有任何办法 “插队”,也无法打断当前 tick 的运行。因为在这种机制下,对回调函数的调度,如何时被调用、调用几次等,都掌握在宿主环境的手里,它不提供 API 的话,JS 本身没有办法影响。

更进一步思考,发现 JS 的三大异步来源(用户交互: DOM;IO:AJAX;定时器: 无标准,浏览器厂商自己搞的??)都不是 ECMAScript 标准的势力范围!这意味着什么呢?细思恐极…… 直到有一天,随着 ES6 的发布,Promise 闪亮登场,标志着 ECMAScript 开始设法争夺对异步的控制权,前端开发进入了一个崭新的时代……

推荐阅读:《你不知道的 JavaScript· 中卷》第二部分:异步和性能 https://www.cnblogs.com/leegent/p/6016403.html