理解JS事件循环和异步处理
JS事件循环
JS运行环境被称为宿主环境,通常JS会运行在浏览器环境下,配合Web API来操作DOM;有了Node.js后JS可以脱离浏览器而运行了,配合Node API来做一些和操作系统打交道的事情。
JS是一门单线程语言,它在处理一些比较耗时的任务时采用了异步处理的机制,从而保证程序不会被一个任务给“拖住”。
为了更好地理解和使用异步处理机制,我们需要深入学习JS的事件循环,其中包括了进程和线程、执行栈、回调函数等知识。
进程和线程
当我们打开一个应用程序,我们就说“启动了一个进程”,所以一个进程就代表一个正在运行的应用程序。
在这个应用程序内可能有多个任务要处理,所以这个进程会启动多个线程来分别处理它们,所以一个进程可以包含多个线程。
在浏览器中,可以认为有以下这些线程:
- JS 引擎:执行执行JS代码
- GUI:渲染页面
- 事件监听
- 计时
- 网络相关
执行栈、事件队列、回调函数
JS引擎通过执行JS代码来使用浏览器的各项功能,如果把浏览器当作一个餐厅,那么JS引擎就是这里唯一的一个服务员。客人点菜后,JS引擎会让厨房做菜,然后它继续接待其他客人,等菜做好时再给客人端上。
具体来说,内存中有一块叫做执行栈(Call Stack)的区域,JS引擎所执行的代码都是这里的。在调用一个函数前,系统会创建这个函数的执行环境并加入执行栈,执行完毕后就从执行栈移除这个执行环境。我们知道栈的特点是先入后出,也就是说JS引擎总是执行位于最顶上的函数,直到执行栈完全被清空。
所以一个普通函数的执行过程是简单的入栈、出栈,这叫做同步操作。
然而有些函数则不一样,它的执行不是由JS引擎这个线程来做的,比如计时函数,它需要我们指定计时多久,以及计时完毕后要执行的函数(回调函数)。当我们调用计时函数时,计时线程就开始工作,无论计时多久(哪怕零秒),计时完成后就会将我们传入的回调函数放入内存中一个叫“事件队列”的区域,等待JS引擎去执行它。
那么JS引擎什么时候会去执行事件队列中的函数呢?答案是当执行栈完全被清空时。也就是说,当所有同步任务都执行完时,JS引擎才会开始执行事件队列中的函数,这就是以下代码中为什么计时0秒也最后执行的原因:
console.log(1);
setTimeout(()=>console.log(2), 0);
console.log(3);
console.log(4);
//1,3,4,2
总之,JS引擎自己能干的事情就会立即干,自己干不了的事情就交给其他线程干,它只在处理完自己的事情后去事件队列看看有没有可以干的事。
一个任务需要JS引擎之外的线程来处理,那么它的后续任务就需要用一个回调函数来交给JS引擎处理,如果这个后续任务又需要其他线程处理,那么又需要另外的回调函数了。一个异步操作依赖另一个异步操作,而每个异步操作都需要后续处理,这种情况就导致了回调函数的层层嵌套,称为回调地狱。
function TASK() {
asyncTask1(function () {
asyncTask2(function () {
asyncTask3(function () {
asyncTask4(function () {
//do something with
})
})
})
})
}
TASK();
ES6的异步处理方式
ES6将一个可能发生异步操作的事情分为两个阶段,每个阶段都有不同的状态:
- unsettled:尚未得到结果,状态:pending
- settled:已经得到结果,无论好坏,状态:
- resolved:事情以正常的流程走下去并得到了某个结果,后续还可以有进一步处理,表示为thenable
- rejected:事情在发展过程中出错了,后续只能进行错误处理,表示为catchable
事情总是从unsettled阶段发展到settled阶段,而且在unsettled阶段可以控制何时通向settled阶段,事情的发展是不可逆转的。在阶段转变的过程中还可以传递一些数据。
以上概念是学习Promise、Async/Await的关键。