JS/NodeJS中的异步任务与事件环

3,722 阅读8分钟
  • pre-notify
  • 术语
  • 为什么JS要设计成单线程的?
  • JS并非只有一个线程,而只是主线程是单线程的
  • 异步任务以及事件循环
    • Node.js中のEventLoop
    • 微任务和宏任务
      • 其它的微任务
  • Q
      1. setTimeout,setImmediate谁先谁后?
      1. nextTick和promise.then谁快?
      1. nextTick和其它的定时器嵌套
      1. 定时器指定的回调函数一定会在指定的时间内执行吗?
  • 关于浏览器模型

pre-notify

本文主要根据网上资源总结而来,如有不对,请斧正。

参考

术语

synchronous:同步任务

asynchronous:异步任务

task queue/callback queue:任务队列

execution context stack:执行栈

heap:堆

stack:栈

macro-task:宏任务

micro-task:微任务

为什么JS要设计成单线程的?

在进入正式的主题之前我们来探究探究这个历史问题,嗯,还是有点价值的哈,因为JS若不是单线程的也就不会衍生出后来的异步任务以及事件环了嘛。


js最开始时是跑在浏览器端的,主要的作用是与用户互动(接收用户输入并给出自定义的响应)以及操作DOM(各种特效,控制输入输出)。

So网上有一种说法,举了个栗子说,两个线程,有一个线程在添加一个dom元素 a,还有一个线程在删除一个dom元素a,那么浏览器就需要决策该听谁的,这样的话就增加了语言设计的复杂性。

嗯。。。如果这例子以及说法还不能令你信服,我觉得你可以再想想javascript当初开发时的情景

其实我觉的这也是很重要的一个原因之一,嗯,这是我比较能代入的,那就是

我大javascript当初10天就给弄出来了。。。so,你还想咋地?

JS并非只有一个线程,而只是主线程是单线程的

h5有一个新api,它叫webworker,利用它,能帮助我们创建子线程

//index.html
let worker = new Worker('./worker.js');
//把内容发给 工作线程,工作线程可以把结果丢回来

worker.postMessage('向你雇佣的工人发送指令');

worker.onmessage = function(e){
  console.log(e.data); //数据在data属性下
}

//worker.js
window.onmessage = function(e){
	console.log(e);
  this.postMessage('你的工人告诉你他收到命令开始干活了!');
};


注意: 他和js主线程不是平级的,主线程可以控制webworker,webworker不能操作dom,不能获取document以及window。

异步任务以及事件循环

单线程虽然简单,不容易出错,但是有一个问题,这货一直是一个人在干活,假若涉及到读取写入这种I/O行为,那么不仅CPU资源是妥妥的浪费(webworker的出现其实可能是为了尝试解决这部分问题),我们后面的代码还要等待它读写完毕才能执行,这就是所谓的站着那撒不拉那撒了- -!

So,为了解决这个问题,Javascript将任务的执行方式分为两种:同步/synchronous异步/asynchronous,遇到像上面那种需要长时间等待的I/O操作,我们就将它作为一个异步任务分发出去,等待它执行完毕后再通知我们。

我们说遇到异步任务会分发出去,那么分发分发究竟分发给了谁呢?

在浏览器内核中,除了JS线程用户界面后端/UI Backend线程,还有一些其它的线程,比如说浏览器事件触发线程定时器触发线程异步HTTP请求线程,而异步任务就是分发给这些线程来处理的。(如果有不大了解这方面的同学可以查看本文的最后部分)

当这些异步任务在对应的线程中处理完成得到结果后,这些任务的回调就会被加入到callback queue队列当中。另一方面,JS主线程的执行栈中一旦所有同步代码执行完毕后就会开始不停的检测callback queue,只要队列中存在任务,就会被提取到执行栈中执行。

下图是网上流传很广的一张示意图

其中JS主线程从callback queue中不断读取事件到执行栈中执行的这种循环的过程又被称之为EventLoop,即事件循环。

Node.js中のEventLoop

node中的事件环和浏览器中的是不一样的,node的事件环分为六个阶段,每个阶段都一有一个callbcak queue,只有当一个阶段的queue清理干净后才会进入到下一个阶段。

  • timer(计时器),执行setTimeout以及setInterval的回调
  • I/O callbcacks,处理网络、流、tcp的callbcak以及错误
  • idle,prepare node内部使用
  • poll(轮询),会等待I/O执行直到得到cb
  • check,处理setImmediate回调
  • close callbcaks,处理关闭的回调例如socke.on('close')

微任务和宏任务

异步任务主要分为两种,

一种称之为macro task,即宏任务,像setTimeout、I/O读写、AJAX这类耗时灰常长的。

另外一种则称之为micro task,即微任务,例如nextTickpromise。(即使定时器的delayt时间设置为0,也是宏任务,会在本轮的微任务执行完毕后再执行)

宏任务和微任务的区别在于,微任务是会被加入本轮循环的,而宏任务都是在次轮循环中被执行。

本轮循环是指什么呢?JS主线程会从任务队列中提取任务到执行栈中执行,每一次执行都可能会再产生一个新的任务,对于这些任务来说这次执行到下一次从任务队列中提取新的任务到执行栈之前就是这些新生任务的本轮

[danger] 注意: 在node中微任务的触发时机是在进入事件环之前以及当状态转换的时才会触发,这意味着如果一个状态中的callbcak queue中的cb还没有全部清空完毕,那么微任务并不会像浏览器中一样加入本轮后在一个回调执行完毕后就会立即执行,而会等待queue清空后才执行。

其它的微任务

  • MutationObserve(不兼容的),
  • MessageChannel

Q

1. setTimeout,setImmediate谁先谁后?

setTimeout(function(){
console.log('Timeout');
})
setImmediate(function(){
console.log('Immediate');
})

网上有有一种说法,因为setTimeout虽然在node事件循环中的第一个阶段,但setTimeout即使将delay设置为0也会有1ms+(node做不到那么精确),那么当第一次事件循环时,setTimeout可能还没有准备好,就将会让setImmediate先执行。

但,真会发生这种情况吗?

嗯。。。我没事点了几十下,全是setTimeout。

另外还有如下一种情况,一定是setImmediate会先走的

fs.readFile('./1.txt',function(){
    console.log('fs');
    setTimeout(function(){
    	console.log('timeout');
    });
    setImmediate(function(){
    	console.log('setImmediate');
    });
})

因为当fs的I/O回调执行执行时是处于事件循环中的poll阶段,而下一个阶段为check是存放setImmediate的阶段。

2. nextTick和promise.then谁快?

nextTick快,就是这么设计的

3. nextTick和其它的定时器嵌套

setImmediate(function(){
  console.log(1);
  process.nextTick(function(){
    console.log(4);
  })
})
process.nextTick(function(){
  console.log(2);
  setImmediate(function(){
    console.log(3);
  })
})

<<<
2134

原因在于nextTick在node中的执行实际和浏览器中不完全一样,虽然它们在第一次进入事件环时都会先执行,但如果后续还有nextTick加入,node中只会在阶段转换时才会去执行,而浏览器中则是一有nextTick加入就会立即执行。

造成这样区别的原因在于,node中的事件环是有6种状态的,每种状态都是一个callbcak queue,只有当一个状态的callback queue中存放的回调都清空后才会执行nextTick。

4. 定时器指定的回调函数一定会在指定的时间内执行吗?

不一定,先不说node中事件环六中状态之间转化时的猫腻,光是浏览器中的事件环也可能因为本轮循环的执行时间过长,长得比定时器指定的事件还长从而导致定时器的回调触发被延误。

关于浏览器模型

嗯,先上图。。也是很火的一张

浏览器是多进程的,每个进程管理着浏览器不同的部分,主要分为以下几种

  • 用户界面:包括地址栏、前进/后退按钮、书签菜单等
  • 浏览器引擎:在用户界面和呈现引擎之间传送指令
  • 呈现引擎,又称渲染引擎,在线程方面又称为UI线程,这是最为核心的部分,So也被称之为浏览器内核
  • GPU:用于提高网页浏览的体验
  • 插件:一个插件对应一个进程(第三方插件进程)

其中渲染引擎内部有三个线程是我们注重需要关注的

  • Networking:用于网络调用,比如HTTP请求
  • Javascript解释器:用于解析和执行Javascript代码
  • UI Backend

其中js线程和ui线程是互斥的,

当js执行的时候可能ui还在渲染,那么这时ui线程会把更改放到队列中 当js线程空闲下来 ui线程再继续渲染

除此之外还有一些其它的线程,这也是我们分发异步任务时用到的线程

  • 浏览器事件触发线程
  • 定时触发器线程
  • 异步HTTP请求线程

--- End ---