理解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的关键。

posted @ 2020-05-16 12:59  Paykan  阅读(378)  评论(0编辑  收藏  举报