[JS] You Don't Know JavaScript [Async & Performance] - Now & Later

前言

由於JavaScript是一个单线程的程序语言,这意味着JavaScript一次只能做一件事,虽然只有一个执行绪可以简化程序不用担心并发与冲突的问题,但是这代表JS引擎不能执行长时间的操作,如果一个函数依赖於另一个函数的结果,那麽就必须等待上一个函数结束後才能进行,因此他会将整个主执行绪阻塞导致网页没有反应从而降低使用者体验。

const btn = document.querySelector('button');
btn.addEventListener('click', () => {
  let myDate;
  for(let i = 0; i < 10000000; i++) {
    let date = new Date();
    myDate = date
  }

  console.log(myDate);

  let pElem = document.createElement('p');
  pElem.textContent = 'This is a newly-added paragraph.';
  document.body.appendChild(pElem);
});

https://i.imgur.com/UE0obLx.gif

在电脑能够拥有多个处理器的时代,可以将其他任务放到另一个处理器上处理并让使用者知道何时会完成这样比坐在那等却什麽都没有还来的有意义并可以同时完成其他工作,而这就是Asynchronous的用途了,使用Asynchronous(callback function、promise、async/await...)可以达到执行长时间的网路请求的同时又不会阻塞主执行绪。


How Does Synchronous JavaScript Work?

在了解非同步JavaScript之前,我们要先了解在JavaScript中是如何运行同步的程序,举个例子:

const second = () => {
  console.log('Hello there!');
}
const first = () => {
  console.log('Hi there!');
  second();
  console.log('The End');
}
first(); // Hi there! -> Hello there! -> The End

要了解JavaScript引擎是如何执行上面的程序,我们需要了解Execution Contextcall stack的概念。

Execution Context (EC)

在JavaScript中EC是一个抽象的概念,包含着有关在其中执行当前程序码的环境信息,这句话有点深奥,换句话说当JavaScript运行你的程序码的时候,他会建立一个全域的EC,它包含着这个程序中全域的值、变量、物件、函数,而这些可以被访问的东西就称为环境,而每次运行都只会有一个EC的存在,在JS的Execution Context中主要分为三种类型:

  1. Global execution context (GEC): 默认的EC会在首次加载到浏览器时会加入,所有的全域程序都会在这里执行。
  2. Functional execution context (FEC): 会在JS引擎发现函数时创建的EC,每个函数都有自己的EC用来包含这个函数中所使用到的变量、物件等等,而FEC可以访问到GEC的程序,而反之亦然。
  3. Eval: eval函数执行的EC。

Call Stack

在资料结构中有一个重要的概念LIFO(Last in, First out),而JS中的call stack也是这种资料结构的模式,让我们举个例子:

const second = () => {
  console.log('Hello there!');
}
const first = () => {
  console.log('Hi there!');
  second();
  console.log('The End');
}
first();

https://ithelp.ithome.com.tw/upload/images/20210112/20124767voyVOLdwYP.png
(图片来源:Understanding Asynchronous JavaScript)

  • 当执行了这个程序时JS引擎会先创造一个GEC (由上图main()表示)并送入call stack中,而当JS引擎发现了first这个函式时,他会为这个函数建立一个FEC并将他放在call stack中。

  • 接下来当执行完console.log('Hi there!')之後将他pop出call stack,之後调用second(),而JS引擎也会为了second建立一个FEC并放在call stack中。

  • 一样的当second中的console.log('Hello there!!')完成後pop出call stack,JS引擎知道second已经执行完毕便将他也pop出stack。

  • 最後将console.log('The end')放入call stack中执行,完成後一样pop出stack,这时first也执行完毕被pop出stack。

  • 当这一系列的操作完成後JS引擎确定没有其他需要执行的程序,便将GEC也pop出call stack代表整个程序完成运行。


Event Loop

在我们讨论到什麽事Event loop之前我们需要了解到为什麽我们需要他,就像我们在前言中提到的,JS是单线程的程序语言,所以当JS做了一件长时间处理的事情时,会造成其他动作被卡住就为了等他完成,这个现象就是所谓的Blocking,所以为了解决这个问题所以JS才提供了asynchronous这个方法。

const network = () => {
  setTimeout(() => {
    console.log('Async Code');
  }, 2000);
  console.log('Hello World');
};

network();

当我们使用setTimeout这个函数,他会将我们的call back function在2秒後再呼叫,所以会先输出Hello World後在输出Async Code,对於新手来说可能会觉得疑惑,应该是要过两秒後输出Async Code後在输出Hello World才对啊!接下来我们就要深入介绍到底发生什麽事。

Event Loop

我们先来看一个经典的图:

对於JS来说虽然他本身只有单执行绪,但是由於JS是在浏览器中运行所以能够使用到浏览器的WebAPI,接下来我们来看看对於非同步运行到底发生了什麽事,


(图片来源:所以说event loop到底是什麽玩意儿?| Philip Roberts | JSConf EU)

上面的影片可以充分的解释JS的非同步事件到底发生了什麽事,当JS遇到了非同步事件时,他会将这个事件暂时放在webAPIs中处理,让JS的其他同步事件可以持续进行,而当在webAPIs中处理的事情完成後会将他放在task queue等待call stack中的同步事件完成後,才会透过event loop将这个非同步事件的结果放回call stack中。

由於queue是FIFO(First in, First out)的资料结构,所以如果有多个非同步事件处理完成,会在task queue中照着完成的进度排队,依序的透过event loop放回call stack中,这就是event loop的概念。


Parallel Threading

许多人会将parallelasync搞混,但是他们是完全不同的概念让我们来仔细将他们厘清。

Parallel

现在多处理器非常的流行,而多处理器代表机器在同一时间内拥有处理多个事情的能力。
https://ithelp.ithome.com.tw/upload/images/20210112/20124767lAS0ISYHHC.png
(图片来源:Asynchronous and Parallel Programming

Asynchronous

非同步处理消除了同步处理会造成因为单一线程处理而卡住UI的缺点,可以使用後台运行耗费大量时间的操作,但是对於主要架构来说他依然是只有一个线程,只是将耗时任务委托给後台(其他API)处理 (event loop)。
https://ithelp.ithome.com.tw/upload/images/20210112/20124767tTXp4wrkW5.png
图片来源:Asynchronous and Parallel Programming

了解了这两种编译方式的不同後,我们要来介绍对於非同步操作的一些要注意的部分。

var a = 20;

function foo() {
	a = a + 1;
}

function bar() {
	a = a * 2;
}

// ajax(..) is some arbitrary Ajax function given by a library
ajax( "http://some.url.1", foo );
ajax( "http://some.url.2", bar );

由於上面的程序对於函数的呼叫都是使用非同步的方式,所以这种情况可能会有两种不同的输出结果,因为对於非同步事件来说都会委托给webAPI进行处理,当处理完成後会将结果放到task queue中等待call stacl任务完成才会通过event loop传给call stack。

但是由於两个非同步的处理完成的时间不一样,先完成的会先被放在task queue中而後完成的则会排在它後面,所以
当foo先完成就会先将a+1所以会输出42,而若bar先完成则会先将a*2输出41,所以当我们使用非同步呼叫函数时,对於同一个变量的操作会因为先後完成顺序的不同而导致有不同的结果有可能会造成非预期的错误,所以在使用非同步呼叫时要额外的小心。


结论

由上面的介绍可以对JavaScript非同步的操作多一点概念,下面我们将整理一下本章的重点:

  • JavaScript是单线程语言,这意味着一次只能处理一件事。
  • 非同步是用来解决同步会发生的blocking问题。
  • Execution Context代表执行当前程序码的环境信息,通常有三种类型:
    1. Global execution context (GEC): 默认的EC会在首次加载到浏览器时会加入,所有的全域程序都会在这里执行。
    2. Functional execution context (FEC): 会在JS引擎发现函数时创建的EC,每个函数都有自己的EC用来包含这个函数中所使用到的变量、物件等等,而FEC可以访问到GEC的程序,而反之亦然。
    3. Eval: eval函数执行的EC。
  • call stack代表程序运行的流程
  • event loop是JS非同步操作背後的原理
  • parallel programming代表可以同时处理不同的事
  • asynchronous programming可以将耗时的事交给背景处理,不会造成UI卡住
  • 多个非同步呼叫同一个变数需要小心,不同顺序完成会有不同的结果

参考文献

You Don't Know JavaScript
ECMAScript® 2020 Language Specification
Execution context, Scope chain and JavaScript internals
Understanding Asynchronous JavaScript
所以说event loop到底是什麽玩意儿?| Philip Roberts | JSConf EU
Asynchronous and Parallel Programming


<<:  React中的优先级

>>:  认证因素(Authentication Factor)

AutoCAD ActiveX #5-1 Selection Filters (0)

Command:filter filtertype filterdata filtertype fi...

Day4 Hello World! &基本介绍

起初接触Java的时候,正式开始学习写程序的时候,第一支程序通常是『HelloWorld』,学习过J...

予焦啦!使用暂存器除错

本节是以 Golang 上游 ee91bb83198f61aa8f26c3100ca7558d30...

Day07 测试写起乃- let、let!、subject

昨天介绍了 before 之後今天就可以直接来看 let 搂! let、let! let =>...

冒险村06 - Auto-update dependencies

06 - Auto-update dependencies 除了 Release Drafter 及...