天天看点

node基础面试事件环?微任务、宏任务?一篇带你飞一、我们为什么要使用node,它的好处是什么?二、node是什么?三、浏览器中的Event Loop四、node中的事件环五、node的同步、异步,阻塞、非阻塞结尾

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

一、我们为什么要使用node,它的好处是什么?

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

解决高并发?

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

  • 当用户请求量增高时,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全集,因为在服务端中不包含

DOM

BOM

,Node也提供了一些新的模块例如

http,fs

模块等。Node.js 使用了

事件驱动、非阻塞式 I/O

的模型,使其轻量又高效并且Node.js 的包管理器

npm

,是全球最大的开源库生态系统。

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

node特性

  • 主线程是单线程(异步),将后续的逻辑写成函数,传入到当前执行的函数中,当执行的函数得到了结果后,执行传入的函数

    (回调函数)

  • 五个人同时吃一碗饭(异步)。
  • 阻塞不能异步(现在假定数据库是厨师,服务员是node,顾客是请求,一般是厨师做菜让一个服务员递给多个用户,如果厨师邀请服务员聊天,就会导致阻塞,并且是针对内核说的)。
  • i/o操作,读写操作,异步读写(能用异步绝不用同步)

    非阻塞式i/o

    ,即可以异步读写。
  • event-driven

    事件驱动

    (发布订阅)。

node的进程与线程

进程

是操作系统分配资源和调度任务的基本单位,

线程

是建立在进程上的一次程序运行单位,一个进程上可以有多个线程。

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

自上而下,分别是:

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

渲染引擎

,让我们往下看。

渲染引擎

  • 渲染引擎是

    多线程

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

三、浏览器中的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

四、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 loop

    在check队列
  • 然后

    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

      比上面要多切换一次

所以有两种答案

  1. setImmediate1,setTimeout2,setTimeout1,nextTick1,setImmediate2

  2. setImmediate1,setTimeout2,nextTick1,setImmediate2,setTimeout1

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

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

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

有些人可能会搞乱他们之间的关系,

同步、异步

是被调用者的状态,

阻塞、非阻塞

是调用者的状态、消息

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

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

结尾

希望大家看了本篇文章都有收获,这样出去面试的时候就不会这样

而是这样。好了,最后希望大家世界杯都能够逢赌必赢,自己喜欢的球队也能够杀进决赛。

原文发布时间:2018-6-20

原文作者: 上帝的眼

本文来源

掘金

如需转载请紧急联系作者