JavaScript
是一种单线程语言,这意味着在任何给定的实例中,JavaScript 的引擎(在托管环境中运行,如标准 Web 浏览器)一次只能执行一条语句或一行代码。在浏览器中加载 JavaScript 文件时,JavaScript 引擎会从上到下处理文件中的每一行。
虽然单线程语言简化了代码编写,因为你不必担心并发问题,但这也意味着你无法在不阻塞主线程的情况下执行网络访问等耗时操作。
- 想象一下从 API 请求一些数据。根据情况,服务器可能需要一些时间来处理请求,同时阻塞主线程使网页无响应。而使用异步 JavaScript(例如回调、promises 和 async/await),你可以在不阻塞主线程的情况下执行耗时的网络请求。
- 当该线程在执行某些耗时操作时(比如网络请求、文件读取等),如果这个操作是同步的,那么主线程会被完全阻塞,无法响应任何其他的用户交互事件,导致整个页面失去响应。这就是所谓的“UI 阻塞”。
为了避免这种情况的发生,JavaScript 引入了异步编程的概念。异步编程的本质是在主线程发起一个耗时操作时,不必等待其完成,而是立即返回给主线程并继续执行其他代码。当操作完成后,又将相应的结果通知到主线程。
JavaScript 的引擎在浏览器上运行,但它并不是孤立运行的。Web 浏览器结合了 JavaScript 引擎和其他附加功能(Web API
),在 JavaScript 中对其进行了标记。这些 Web API(setTimeout、setInterval 等)
允许 JavaScript 异步运行,允许正常的同步函数在异步任务完成时继续运行。
具体来说,异步编程使用回调函数
、Promise
、Generator
、async/await
等方式来实现,在执行异步操作时,主线程将异步任务委托给浏览器的底层引擎,通过 Event Loop
机制实现对异步操作的检测和处理,使得主线程能够及时响应用户的交互事件,提高了 JavaScript 的性能和用户体验。
因此,JavaScript 之所以需要异步编程,是为了提供更好的用户体验及改善性能。接下来我们就去回顾一下异步 JavaScript 的演进历程。
一、什么是异步?
1、调用堆栈(Call Stack)
调用堆栈是JavaScript中的一个结构,用来暂时保存函数调用的列表。一旦你的JavaScript应用程序开始执行,就会在调用栈的底部创建并添加一个全局执行环境。因为JavaScript是单线程的,这个页面上的所有东西都是由一个单线程执行的。这个执行线程开始一行一行地遍历你的代码。
因为JavaScript一次只能执行一件事,所以它使用调用栈来跟踪已经被调用的函数、已经被调用但还没有完成的函数等的执行环境。当一个函数被调用时,一个新的执行上下文被创建,对该函数的引用被添加到调用栈的顶部,在那里开始执行。当函数的执行完成后,就会从堆栈中移除,JavaScript的引擎就会开始执行顶部的下一个函数。
就像这样:
它被称为调用堆栈,因为它使用了数据结构中堆栈的概念,遵循后进先出(LIFO
)原则。它总是会先处理堆栈顶部的调用。例如,如果你在桌子上有一堆盘子,想添加更多的盘子,你就把它们添加到顶部,如果你要从这堆盘子中拿一个或多个盘子,你就从顶部拿,而不是从底部。最后添加到这堆盘子里的盘子将是第一个被拿走的。
这与调用栈的情况相同。栈顶的函数总是首先被执行。在执行函数并将其添加到调用栈时,如果JavaScript遇到一个使用Web API方法的异步函数,它不会将其添加到调用栈中,而是将其发送到Web API容器中。
2、事件队列(Event Queue)
如果JavaScript遇到一个使用Web API方法的异步函数,它会将其发送到Web API容器中。随后,每当所需的事件发生时,该方法就会从 Web API容器
转移到事件队列中。例如,setTimeout中的回调在指定时间过后被添加到队列中。
与调用堆栈不同,事件队列遵循 FIFO
(先进先出)原则,这意味着调用是按照它们被添加到队列的顺序处理的。
3、事件循环(Event Loop)
事件循环是一个无限期运行的循环,作为调用栈和事件队列之间的连接。事件循环反复检查调用栈,并将子任务从事件队列转移到调用栈。在事件循环开始从队列中转移子任务之前,调用堆栈必须是空的,这表明所有程序的常规(同步)功能已经被执行。
如果调用堆栈是空的,添加到事件队列中的第一个子任务(最老的那个)将从队列中移除,其相关的函数将被添加到调用堆栈中,并以该子任务为参数执行。
就像这样: