转载

node基础面试事件环?微任务、宏任务?一篇带你飞

我们这里来举个例子,我们node和java相比,在同样的请求下谁更占优一点。看图

node基础面试事件环?微任务、宏任务?一篇带你飞
  • 当用户请求量增高时,node相对于java有更好的处理 并发 性能,它可以快速通过主线程绑定事件。java每次都要创建一个线程,虽然java现在有个 线程池 的概念,可以控制线程的复用和数量。
  • 异步i/o操作,node可以更快的操作数据库。java访问数据库会遇到一个并行的问题,需要添加一个锁的概念。我们这里可以打个比方,下课去饮水机接水喝,java是一下子有喝多人去接水喝,需要等待,node是每次都只去一个人接水喝。
  • 密集型CPU运算指的是逻辑处理运算、压缩、解压、加密、解密,node遇到CPU密集型运算时会阻塞主线程 (单线程) ,导致其下面的时间无法快速绑定,所以 node不适用于大型密集型CPU运算案例 ,而java却很适合。

node在web端场景?

web端场景主要是 用户的请求 或者 读取静态资源 什么的,很适合node开发。应用场景主要有 聊天服务器电子商务网站 等等这些高并发的应用。

二、node是什么?

Node.js是一个基于 Chrome V8 引擎的JavaScript 运行环境(runtime) ,Node不是一门语言,是让js运行在后端的 运行时 ,并且不包括javascript全集,因为在服务端中不包含 DOMBOM ,Node也提供了一些新的模块例如 http,fs 模块等。Node.js 使用了 事件驱动、非阻塞式 I/O 的模型,使其轻量又高效并且Node.js 的包管理器 npm ,是全球最大的开源库生态系统。

总而言之,言而总之,它只是一个运行时,一个运行环境。

node特性

(回调函数)
非阻塞式i/o
事件驱动

node的进程与线程

进程 是操作系统分配资源和调度任务的基本单位, 线程 是建立在进程上的一次程序运行单位,一个进程上可以有多个线程。

在此之前我们先来看看浏览器的进程机制

node基础面试事件环?微任务、宏任务?一篇带你飞

自上而下,分别是:

  • 用户界面 --包括地址栏、书签菜单等
  • 浏览器引擎 --用户界面和渲染引擎之间的传送指令(浏览器的主进程)
  • 渲染引擎 --浏览器的内核,如(webkit,Gecko)
  • 其他 --网络请求,js线程和ui线程

从我们的角度来看,我们更关心的是浏览器的 渲染引擎 ,让我们往下看。

渲染引擎

  • 渲染引擎是 多线程 的,包含ui线程和js线程。ui线程和js线程会 互斥 ,因为js线程的运行结果会影响ui线程,ui更新会被保存在队列,直到js线程空闲,则被取出来更新。
  • js单线程是单线程的,为什么呢?假如js是多线程的,那么操作DOM就是多线程操作,那样的话就会很 混乱 ,DOM不知道该听谁的,而这里的单线程指得是主线程是单线程的,他同样可以有异步线程,通过队列存放这些线程,而主线程依旧是单线程,这个我们后面再讲。所以在node中js也是单线程的。
  • 单线程的好处就是节约内存,不需要再切换的时候执行上下文,也不用管锁的概念,因为我们每次都通过一个。

三、浏览器中的Event Loop

这里我先要说一下浏览器的事件环,可能有人会说,你这篇文章明明是讲node的怎么会扯到浏览器。首先他们都是以js为底层语言的不同运行时,有其相似之处,再者多学一点也不怕面试官多问。好了我废话不多说,开始。

首先我们需要知道堆,栈和队列的关系和意义。

队列是先进先出的
node基础面试事件环?微任务、宏任务?一篇带你飞
  • 栈(stack):栈本身是存储基础的变量,比如1,2,3,还有引用的变量,这里可能有人会问你上面的堆不是存放引用类型的对象吗,怎么变栈里去了。这里我要解释一下,因为栈里面的存放的 引用变量 是指向堆里的引用对象的 地址只是一串地址 。这里栈代表的是执行栈,我们js的主线程。 栈是先进后出的 ,先进后出就是相当于喝水的水杯,我们倒水进去,理论上喝到的水是最后进水杯的。我们可以看代码, follow me
function a(){
  console.log('a')
  function b(){
    console.log('b')    
    function c(){
      console.log('c')
    }
    c()
  }
  b()
}
a()

//这段代码是输出a,b,c,执行栈中的顺序的c,b,a,如果是遵循先进先出,就是输出c,b,a。所以栈先进后出这个特性大家要牢记。

OK,现在大家已经知道堆,栈和队列的关系,现在我们来看一张图。

node基础面试事件环?微任务、宏任务?一篇带你飞

我分析一下这张图

setTimeout、onClick
event loop
event loop
Event Loop

微任务、宏任务?

macro-task(宏任务): setTimeout,setImmediate,MessageChannel micro-task(微任务): 原生Promise(有些实现的promise将then方法放到了宏任务中),Object.observe(已废弃), MutationObserver

微任务和宏任务皆为异步任务,它们都属于一个队列,主要区别在于他们的执行顺序,Event Loop的走向和取值。那么他们之间到底有什么区别呢

node基础面试事件环?微任务、宏任务?一篇带你飞

每次执行栈的同步任务执行完毕,就会去任务队列中取出完成的异步任务,队列中又分为 microtasks queues和宏任务队列 等到把 microtasks queues所有的microtasks 都执行完毕,注意是 所有的 ,他才会从 宏任务队列 中取事件。等到把队列中的事件取出 一个 ,放入执行栈执行完成,就算一次循环结束,之后 event loop 还会继续循环,他会再去 microtasks queues 执行所有的任务,然后再从 宏任务队列 里面取 一个 ,如此反复循环。

  • 同步任务执行完
  • 去执行 microtasks ,把所有 microtasks queues 清空
  • 取出一个 macrotasks queues 的完成事件,在执行栈执行
  • 再去执行 microtasks
  • ...
  • ...
  • ...

我这么说可能大家会有点懵,不慌,我们来看一道题

setTimeout(()=>{
  console.log('setTimeout1')
},0)
let p = new Promise((resolve,reject)=>{
  console.log('Promise1')
  resolve()
})
p.then(()=>{
  console.log('Promise2')    
})

最后输出结果是Promise1,Promise2,setTimeout1

  • Promise参数中的Promise1是同步执行的,Promise还不是很了解的可以看看我另外一篇文章 Promise之你看得懂的Promise ,
  • 其次是因为Promise是 microtasks ,会在同步任务执行完后会去 清空 microtasks queues
  • 最后清空完微任务再去 宏任务队列取值
Promise.resolve().then(()=>{
  console.log('Promise1')  
  setTimeout(()=>{
    console.log('setTimeout2')
  },0)
})

setTimeout(()=>{
  console.log('setTimeout1')
  Promise.resolve().then(()=>{
    console.log('Promise2')    
  })
},0)

这回是嵌套,大家可以看看,最后输出结果是Promise1,setTimeout1,Promise2,setTimeout2

  • 一开始执行栈的同步任务执行完毕,会去 microtasks queues
  • 清空 microtasks queues ,输出 Promise1 ,同时会生成一个异步任务setTimeout1
  • 宏任务队列 查看此时队列是setTimeout1在setTimeout2之前,因为setTimeout1执行栈一开始的时候就开始异步执行,所以输出 setTimeout1 ,在执行setTimeout1时会生成Promise2的一个microtasks,放入 microtasks queues
  • 接着又是一个循环,去清空 microtasks queues ,输出 Promise2
  • 清空完 microtasks queues ,就又会去宏任务队列取一个,这回取的是 setTimeout2
node基础面试事件环?微任务、宏任务?一篇带你飞

四、node中的事件环

node的事件环相比浏览器就不一样了,我们先来看一张图,他的工作流程

node基础面试事件环?微任务、宏任务?一篇带你飞
  • 首先我们能看到我们的js代码 (APPLICATION) 会先进入v8引擎,v8引擎中主要是一些 setTimeout 之类的方法。
  • 其次如果我们的代码中执行了nodeApi,比如 require('fs').read() ,node就会交给 libuv 库处理,这个 libuv 库是别人写的,他就是node的事件环。
  • libuv 库是通过单线程异步的方式来处理事件,我们可以看到 work threads 是个多线程的队列,通过外面 event loop 阻塞的方式来进行异步调用。
  • 等到 work threads 队列中有执行完成的事件,就会通过 EXECUTE CALLBACK 回调给 EVENT QUEUE 队列,把它放入队列中。
  • 最后通过事件驱动的方式,取出 EVENT QUEUE 队列的事件,交给我们的应用

node中的event loop

node中的event loop是在libuv里面的,libuv里面有个事件环机制,他会在启动node时,初始化事件环

node基础面试事件环?微任务、宏任务?一篇带你飞
  • 这里的每一个阶段都对应着一个 事件队列
  • 每当 event loop 执行到某个阶段时,都会执行对应的 事件队列 中的事件,依次执行
  • 当该队列执行完毕或者执行数量超过上限, event loop 就会执行下一个阶段
  • 每当 event loop 切换一个执行队列时,就会去清空 microtasks queues ,然后再切换到下个队列去执行,如此反复

这里我们要注意 setImmediate 是属于check队列的,还有poll队列主要是异步的I/O操作,比如node中的fs.readFile()

我们来具体看一下他的用法吧

setImmediate(()=>{
  console.log('setImmediate1')
  setTimeout(()=>{
    console.log('setTimeout1')    
  },0)
})
setTimeout(()=>{
  console.log('setTimeout2') 
  process.nextTick(()=>{console.log('nextTick1')})
  setImmediate(()=>{
    console.log('setImmediate2')
  })   
},0)
  • 首先我们可以看到上面的代码先执行的是 setImmediate1 ,此时 event loopcheck队列
  • 然后 setImmediate1 从队列取出之后,输出 setImmediate1 ,然后会将 setTimeout1 执行
  • 此时 event loop 执行完 check队列 之后,开始往下移动,接下来执行的是 timers队列
  • 这里会有问题,我们都知道 setTimeout1 设置延迟为0的话,其实还是有 4ms 的延迟,那么这里就会有两种情况。先说第一种,此时 setTimeout1 已经执行完毕
    • 根据node事件环的规则,我们会执行完所有的事件,即取出 timers队列 中的 setTimeout2,setTimeout1
    • 此时根据队列先进先出规则,输出顺序为 setTimeout2,setTimeout1 ,在取出 setTimeout2 时,会将一个 process.nextTick 执行(执行完了就会被放入 微任务队列 ),再将一个 setImmediate 执行(执行完了就会被放入 check队列
    • 到这一步, event loop 会再去寻找下个事件队列,此时 event loop 会发现 微任务队列 有事件 process.nextTick ,就会去清空它,输出 nextTick1
    • 最后 event loop 找到下个有事件的队列 check队列 ,执行 setImmediate ,输出 setImmediate2
  • 假如这里 setTimeout1 还未执行完毕(4ms耽误了它的终身大事?)
    • 此时 event loop 找到 timers队列 ,取出*timers队列**中的 setTimeout2 ,输出 setTimeout2 ,把 process.nextTick 执行,再把 setImmediate 执行
    • 然后 event loop 需要去找下一个事件队列, 这里大家要注意一下 ,这里会发生2步操作, 1、 setTimeout1 执行完了,放入timers队列。2、找到微任务队列清空。 ,所以此时会先输出 nextTick1
    • 接下来 event loop 会找到 check队列 ,取出里面已经执行完的 setImmediate2
    • 最后 event loop 找到 timers队列 ,取出执行完的 setTimeout1 这种情况下 event loop 比上面要多切换一次

所以有两种答案

setImmediate1,setTimeout2,setTimeout1,nextTick1,setImmediate2
setImmediate1,setTimeout2,nextTick1,setImmediate2,setTimeout1
node基础面试事件环?微任务、宏任务?一篇带你飞

这里的图只参考了第一种情况,另一种情况也类似

五、node的同步、异步,阻塞、非阻塞

  • 同步:即为调用者等待被调用者这个过程,如果被调用者一直不反回结果,调用者就会一直等待,这就是同步, 同步有返回值
  • 异步:即为调用者不等待被调用者是否返回,被调用者执行完了就会通过状态、通知或者回调函数给调用者, 异步没有返回值
  • 阻塞:指代当前线程在结果返回之前会被挂起,不会继续执行下去
  • 非阻塞: 即当前线程不管你返回什么,都会继续往下执行

有些人可能会搞乱他们之间的关系, 同步、异步 是被调用者的状态, 阻塞、非阻塞 是调用者的状态、消息

接下来我们来看看他们的组合会是怎么样的

组合 意义
同步阻塞 这就相当于我去饭店吃饭,我需要在厨房等待菜烧好了,才能吃。我是调用者我需要等待上菜于是被阻塞,菜是被调用者做好直接给我是同步
异步阻塞 我去饭店吃饭,我需要等待菜烧好了才能吃,但是厨师有事,希望之后处理完事能做好之后通知我去拿,我作为调用者等待就是阻塞的,而菜作为被调用者是做完之后通知我的,所以是异步的,这种方式一般没用。
同步非阻塞 我去饭店吃饭,先叫了碗热菜,在厨房等厨师做菜,但我很饿,就开始吃厨房冷菜,我是调用者我没等热菜好就开始吃冷菜,是非阻塞的,菜作为被调用者做好直接给我是同步的,这种方式一般也没人用
异步非阻塞 我去饭店吃饭。叫了碗热菜,厨师在做菜,但我很饿,先吃冷菜,厨师做好了通知我去拿,我是调用者我不会等热菜烧好了再吃冷菜,是非阻塞的,菜作为被调用者通知我拿是异步的
而是这样。好了,最后希望大家世界杯都能够,自己喜欢的球队也能够

原文  https://juejin.im/post/5b35cdfa51882574c020d685
正文到此结束
Loading...