培育能力的事必须继续不断地去做,又必须随时改善学习方法,提高学习效率,才会成功。 —— 叶圣陶
一、我们为什么要使用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
- 根据node事件环的规则,我们会执行完所有的事件,即取出timers队列中的
- 假如这里
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的同步、异步,阻塞、非阻塞
- 同步:即为调用者等待被调用者这个过程,如果被调用者一直不反回结果,调用者就会一直等待,这就是同步,同步有返回值
- 异步:即为调用者不等待被调用者是否返回,被调用者执行完了就会通过状态、通知或者回调函数给调用者,异步没有返回值
- 阻塞:指代当前线程在结果返回之前会被挂起,不会继续执行下去
- 非阻塞: 即当前线程不管你返回什么,都会继续往下执行
有些人可能会搞乱他们之间的关系,同步、异步
是被调用者的状态,阻塞、非阻塞
是调用者的状态、消息
接下来我们来看看他们的组合会是怎么样的
组合 | 意义 |
---|---|
同步阻塞 | 这就相当于我去饭店吃饭,我需要在厨房等待菜烧好了,才能吃。我是调用者我需要等待上菜于是被阻塞,菜是被调用者做好直接给我是同步 |
异步阻塞 | 我去饭店吃饭,我需要等待菜烧好了才能吃,但是厨师有事,希望之后处理完事能做好之后通知我去拿,我作为调用者等待就是阻塞的,而菜作为被调用者是做完之后通知我的,所以是异步的,这种方式一般没用。 |
同步非阻塞 | 我去饭店吃饭,先叫了碗热菜,在厨房等厨师做菜,但我很饿,就开始吃厨房冷菜,我是调用者我没等热菜好就开始吃冷菜,是非阻塞的,菜作为被调用者做好直接给我是同步的,这种方式一般也没人用 |
异步非阻塞 | 我去饭店吃饭。叫了碗热菜,厨师在做菜,但我很饿,先吃冷菜,厨师做好了通知我去拿,我是调用者我不会等热菜烧好了再吃冷菜,是非阻塞的,菜作为被调用者通知我拿是异步的 |
结尾
希望大家看了本篇文章都有收获,这样出去面试的时候就不会这样
而是这样。好了,最后希望大家世界杯都能够逢赌必赢,自己喜欢的球队也能够杀进决赛。