本文共 6791 字,大约阅读时间需要 22 分钟。
培育能力的事必须继续不断地去做,又必须随时改善学习方法,提高学习效率,才会成功。 —— 叶圣陶
Node的首要目标是提供一种简单的,用于创建高性能服务器的开发工具。还要解决web服务器高并发的用户请求。
我们这里来举个例子,我们node和java相比,在同样的请求下谁更占优一点。看图
并发
性能,它可以快速通过主线程绑定事件。java每次都要创建一个线程,虽然java现在有个 线程池
的概念,可以控制线程的复用和数量。 异步i/o操作,node可以更快的操作数据库。java访问数据库会遇到一个并行的问题,需要添加一个锁的概念。我们这里可以打个比方,下课去饮水机接水喝,java是一下子有喝多人去接水喝,需要等待,node是每次都只去一个人接水喝。 密集型CPU运算指的是逻辑处理运算、压缩、解压、加密、解密,node遇到CPU密集型运算时会阻塞主线程 (单线程)
,导致其下面的时间无法快速绑定,所以 node不适用于大型密集型CPU运算案例
,而java却很适合。 web端场景主要是用户的请求
或者读取静态资源
什么的,很适合node开发。应用场景主要有聊天服务器
,电子商务网站
等等这些高并发的应用。
Node.js是一个基于 Chrome V8 引擎的JavaScript运行环境(runtime)
,Node不是一门语言,是让js运行在后端的运行时
,并且不包括javascript全集,因为在服务端中不包含DOM
和BOM
,Node也提供了一些新的模块例如http,fs
模块等。Node.js 使用了事件驱动、非阻塞式 I/O
的模型,使其轻量又高效并且Node.js 的包管理器 npm
,是全球最大的开源库生态系统。
总而言之,言而总之,它只是一个运行时,一个运行环境。
(回调函数)
。非阻塞式i/o
,即可以异步读写。事件驱动
(发布订阅)。
进程
是操作系统分配资源和调度任务的基本单位,线程
是建立在进程上的一次程序运行单位,一个进程上可以有多个线程。
在此之前我们先来看看浏览器的进程机制
自上而下,分别是:
从我们的角度来看,我们更关心的是浏览器的
渲染引擎
,让我们往下看。
多线程
的,包含ui线程和js线程。ui线程和js线程会互斥,因为js线程的运行结果会影响ui线程,ui更新会被保存在队列,直到js线程空闲,则被取出来更新。这里我先要说一下浏览器的事件环,可能有人会说,你这篇文章明明是讲node的怎么会扯到浏览器。首先他们都是以js为底层语言的不同运行时,有其相似之处,再者多学一点也不怕面试官多问。好了我废话不多说,开始。
队列是先进先出的
,比如下面的图,最先进队列的会先被打出去引用变量
是指向堆里的引用对象的 地址, 只是一串地址。这里栈代表的是执行栈,我们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
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的事件环相比浏览器就不一样了,我们先来看一张图,他的工作流程
(APPLICATION)
会先进入v8引擎,v8引擎中主要是一些setTimeout
之类的方法。require('fs').read()
,node就会交给libuv
库处理,这个libuv
库是别人写的,他就是node的事件环。libuv
库是通过单线程异步的方式来处理事件,我们可以看到work threads
是个多线程的队列,通过外面event loop
阻塞的方式来进行异步调用。work threads
队列中有执行完成的事件,就会通过EXECUTE CALLBACK
回调给EVENT QUEUE
队列,把它放入队列中。EVENT QUEUE
队列的事件,交给我们的应用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
已经执行完毕 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
这里的图只参考了第一种情况,另一种情况也类似
有些人可能会搞乱他们之间的关系,同步、异步
是被调用者的状态,阻塞、非阻塞
是调用者的状态、消息
接下来我们来看看他们的组合会是怎么样的
组合 | 意义 |
---|---|
同步阻塞 | 这就相当于我去饭店吃饭,我需要在厨房等待菜烧好了,才能吃。我是调用者我需要等待上菜于是被阻塞,菜是被调用者做好直接给我是同步 |
异步阻塞 | 我去饭店吃饭,我需要等待菜烧好了才能吃,但是厨师有事,希望之后处理完事能做好之后通知我去拿,我作为调用者等待就是阻塞的,而菜作为被调用者是做完之后通知我的,所以是异步的,这种方式一般没用。 |
同步非阻塞 | 我去饭店吃饭,先叫了碗热菜,在厨房等厨师做菜,但我很饿,就开始吃厨房冷菜,我是调用者我没等热菜好就开始吃冷菜,是非阻塞的,菜作为被调用者做好直接给我是同步的,这种方式一般也没人用 |
异步非阻塞 | 我去饭店吃饭。叫了碗热菜,厨师在做菜,但我很饿,先吃冷菜,厨师做好了通知我去拿,我是调用者我不会等热菜烧好了再吃冷菜,是非阻塞的,菜作为被调用者通知我拿是异步的 |
希望大家看了本篇文章都有收获,这样出去面试的时候就不会这样