我们这里来举个例子,我们node和java相比,在同样的请求下谁更占优一点。看图
并发
性能,它可以快速通过主线程绑定事件。java每次都要创建一个线程,虽然java现在有个 线程池
的概念,可以控制线程的复用和数量。 (单线程)
,导致其下面的时间无法快速绑定,所以 node不适用于大型密集型CPU运算案例
,而java却很适合。 web端场景主要是 用户的请求
或者 读取静态资源
什么的,很适合node开发。应用场景主要有 聊天服务器
, 电子商务网站
等等这些高并发的应用。
运行环境(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
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') })
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)
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
这里的图只参考了第一种情况,另一种情况也类似
同步、异步
是被调用者的状态, 阻塞、非阻塞
是调用者的状态、消息 接下来我们来看看他们的组合会是怎么样的
组合 | 意义 |
---|---|
同步阻塞 | 这就相当于我去饭店吃饭,我需要在厨房等待菜烧好了,才能吃。我是调用者我需要等待上菜于是被阻塞,菜是被调用者做好直接给我是同步 |
异步阻塞 | 我去饭店吃饭,我需要等待菜烧好了才能吃,但是厨师有事,希望之后处理完事能做好之后通知我去拿,我作为调用者等待就是阻塞的,而菜作为被调用者是做完之后通知我的,所以是异步的,这种方式一般没用。 |
同步非阻塞 | 我去饭店吃饭,先叫了碗热菜,在厨房等厨师做菜,但我很饿,就开始吃厨房冷菜,我是调用者我没等热菜好就开始吃冷菜,是非阻塞的,菜作为被调用者做好直接给我是同步的,这种方式一般也没人用 |
异步非阻塞 | 我去饭店吃饭。叫了碗热菜,厨师在做菜,但我很饿,先吃冷菜,厨师做好了通知我去拿,我是调用者我不会等热菜烧好了再吃冷菜,是非阻塞的,菜作为被调用者通知我拿是异步的 |
。