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

培育能力的事必须继续不断地去作,又必须随时改善学习方法,提升学习效率,才会成功。 —— 叶圣陶javascript

1、咱们为何要使用node,它的好处是什么?

Node的首要目标是提供一种简单的,用于建立高性能服务器的开发工具。还要解决web服务器高并发的用户请求。java

解决高并发?

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

  • 当用户请求量增高时,node相对于java有更好的处理并发性能,它能够快速经过主线程绑定事件。java每次都要建立一个线程,虽然java如今有个线程池的概念,能够控制线程的复用和数量。
  • 异步i/o操做,node能够更快的操做数据库。java访问数据库会遇到一个并行的问题,须要添加一个锁的概念。咱们这里能够打个比方,下课去饮水机接水喝,java是一会儿有喝多人去接水喝,须要等待,node是每次都只去一我的接水喝。
  • 密集型CPU运算指的是逻辑处理运算、压缩、解压、加密、解密,node遇到CPU密集型运算时会阻塞主线程(单线程),致使其下面的时间没法快速绑定,因此node不适用于大型密集型CPU运算案例,而java却很适合。

node在web端场景?

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

2、node是什么?

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

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

node特性

  • 主线程是单线程(异步),将后续的逻辑写成函数,传入到当前执行的函数中,当执行的函数获得告终果后,执行传入的函数(回调函数)
  • 五我的同时吃一碗饭(异步)。
  • 阻塞不能异步(如今假定数据库是厨师,服务员是node,顾客是请求,通常是厨师作菜让一个服务员递给多个用户,若是厨师邀请服务员聊天,就会致使阻塞,而且是针对内核说的)。
  • i/o操做,读写操做,异步读写(能用异步毫不用同步) 非阻塞式i/o,便可以异步读写。
  • event-driven事件驱动(发布订阅)。

node的进程与线程

进程是操做系统分配资源和调度任务的基本单位,线程是创建在进程上的一次程序运行单位,一个进程上能够有多个线程。npm

在此以前咱们先来看看浏览器的进程机制后端

自上而下,分别是:promise

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

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

渲染引擎

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

3、浏览器中的Event Loop

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

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

  • 堆(heap):堆是存放对象的一个空间(Object、function)
  • 队列(loop):是指存放全部异步请求操做的结果,直到有一个异步操做完成它的使命,就会在loop中添加一个事件,队列是先进先出的,好比下面的图,最早进队列的会先被打出去

隔山打牛!

  • 栈(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,如今你们已经知道堆,栈和队列的关系,如今咱们来看一张图。

我分析一下这张图

  • 咱们的同步任务在主线程上运行会造成一个执行栈
  • 若是碰到异步任务,好比setTimeout、onClick等等的一些操做,咱们会将他的执行结果放入队列,此期间主线程不阻塞
  • 等到主线程中的全部同步任务执行完毕,就会经过event loop在队列里面从头开始取,在执行栈中执行
  • event loop永远不会断
  • 以上的这一整个流程就是Event Loop(事件循环机制)

微任务、宏任务?

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

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

每次执行栈的同步任务执行完毕,就会去任务队列中取出完成的异步任务,队列中又分为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

4、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时,初始化事件环

  • 这里的每个阶段都对应着一个事件队列
  • 每当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步操做,一、setTimeout1执行完了,放入timers队列。二、找到微任务队列清空。,因此此时会先输出nextTick1
    • 接下来event loop会找到check队列,取出里面已经执行完的setImmediate2
    • 最后event loop找到timers队列,取出执行完的setTimeout1这种状况下event loop比上面要多切换一次

因此有两种答案

  1. setImmediate1,setTimeout2,setTimeout1,nextTick1,setImmediate2
  2. setImmediate1,setTimeout2,nextTick1,setImmediate2,setTimeout1

这里的图只参考了第一种状况,另外一种状况也相似

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

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

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

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

组合 意义
同步阻塞 这就至关于我去饭店吃饭,我须要在厨房等待菜烧好了,才能吃。我是调用者我须要等待上菜因而被阻塞,菜是被调用者作好直接给我是同步
异步阻塞 我去饭店吃饭,我须要等待菜烧好了才能吃,可是厨师有事,但愿以后处理完事能作好以后通知我去拿,我做为调用者等待就是阻塞的,而菜做为被调用者是作完以后通知个人,因此是异步的,这种方式通常没用。
同步非阻塞 我去饭店吃饭,先叫了碗热菜,在厨房等厨师作菜,但我很饿,就开始吃厨房冷菜,我是调用者我没等热菜好就开始吃冷菜,是非阻塞的,菜做为被调用者作好直接给我是同步的,这种方式通常也没人用
异步非阻塞 我去饭店吃饭。叫了碗热菜,厨师在作菜,但我很饿,先吃冷菜,厨师作好了通知我去拿,我是调用者我不会等热菜烧好了再吃冷菜,是非阻塞的,菜做为被调用者通知我拿是异步的

结尾

但愿你们看了本篇文章都有收获,这样出去面试的时候就不会这样

而是这样。好了,最后但愿你们世界杯都可以 逢赌必赢,本身喜欢的球队也可以 杀进决赛
相关文章
相关标签/搜索