node 的特点
- 主线程是单线程
- 异步非阻塞(非阻塞I/O)
- 事件驱动
单线程
- 进程和线程
进程是操作系统分配资源和调度任务的基本单位,线程是建立在进程上的一次程序运行单位,一个进程上可以有多个线程。
- 什么是单线程?
一个进程中只有一个线程,程序顺序执行,前面的执行完成后才会执行后面的程序。
有点儿像点菜一样, 顾客来了。服务员接单,服务员接完单后马上告诉厨房去做菜吧,此时服务员不会等待菜做好而是马上会被释放出来继续服务下一个客户。 一直都是一个服务员在工作,等菜做好了,服务员在回去取菜给客户吃。
单线程特点是节约内存,并且不需要再切换执行上下文,而且单线程不需要考虑锁的问题。
如下图
异步非阻塞
- 同步和异步
同步和异步关注的是消息通知机制,指代的是被调用方
同步就是发出调用后,没有得到结果前,该调用不返回,一旦调用返回,就得到返回值
当一个异步过程调用发出后,调用者不会立刻得到结果,而是调用发出后,被调用者通过状态、通知或者回调函数处理这个调用。
- 阻塞和非阻塞
阻塞和非阻塞关注的是程序在等待调用结果(消息、返回值)的状态,针对的是调用者
阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果后才会返回
非阻塞调用指在不能立刻得到结果前,该调用不会阻塞当前线程
a> 同步阻塞
调用者:我喜欢你
被调用者:我也喜欢你
b> 异步阻塞
调用者: 我喜欢你
被调用者:我和我妈商量下。回头回复你
调用者: 不挂断电话,我等你回复
c> 异步非阻塞
调用者: 我喜欢你
被调用者: 我和我妈商量下。回头回复你
调用者: 挂断电话,不等你了。联系了另外一个妹子
d> 同步非阻塞
调用者:给 A 打电话 我喜欢你
被调用者(A):正准备回复
调用者:不挂断电话 A 转身又给 B 打电话 我喜欢你
复制代码
-
I/O 操作
-
访问服务器的静态资源
-
读取数据,读取文件
-
node 在处理高并发, I/O密集场景有明显优势。高并发是指在同一时间并发访问服务器,I/O密集知道是文件操作、网络操作、数据库。相对的有 CPU 密集,CPU 密集值的是逻辑处理运算、压缩、解压、加密、解密等。 可是菜做好了,如何告诉服务员呢? ==回调==
事件驱动
谈谈浏览器中的Event Loop
- 渲染引擎
渲染引擎内部是多线程的,包括 UI 线程和 JS 线程。注意 UI 线程和 JS 线程是互斥的,因为 JS 运行结果会影响到 UI 线程的结果。 UI 更新会被保存在任务队列中等 JS 线程空闲时候立即被执行。
- JS 单线程(主线程)
JS 在最初为什么被设计成了单线程,而不是多线程呢?如果多个线程同时操作 DOM 那岂不是会很混乱?
-
其他线程
- 浏览器事件触发线程(用来控制事件循环、存放seTimeout、浏览器事件、ajax回调)
- 定时触发器线程(setTimeout)
- 异步 HTTP 请求线程(ajax请求线程)
浏览器中的 Event Loop
来个经典的图
- 所有同步任务都在主线程上执行,形成一个执行栈
- 主线程之外,还存在一个任务队列,只要异步任务有了运行结果,就在任务队列中放一个事件
- 一旦执行栈中的所有同步任务执行完毕,系统就会读取任务队列,将任务队列中的事件放到执行栈中依次执行
- 主线程从任务队列中读取事件,这个过程是循环不断的
栈内存 / 堆内存
javascript 中的变量分为基本类型和引用类型。基本类型是保存在栈内存中,引用类型则指的是保存在堆内存中的对象
任务队列
- 任务队列 先进先出
宏观任务(MacroTask):
setTimeout
setInterval
setImmediate(只兼容IE)
MessageChannel
requestAnimationFrame
I/O
UI rendering
复制代码
微观任务(MicroTask):
process.nextTick
Promise
Object.observe(已废弃)
MutationObserver
复制代码
- 栈 先进后出
比如: 函数的执行栈,作用域的释放顺序。
放进去的顺序: 全局作用域 <= one <= two <= three
函数 three 没有执行完,函数 one 是不会被释放的。函数销毁的顺序则是 three => two => one => 全局
function one () {
let a = 1;
two();
function two() {
console.log(a);
let b = 2;
function three () {
debugger;
console.log(b);
}
three();
}
}
one();
复制代码
断点调试下如下:进入的顺序和出去的顺序是相反的。
例子-1:
// 栈中的代码执行完毕后,会调用队列中的代码,此过程不挺的循环
// 当 1000 毫秒到达的时候 setTimeout 才会被放到任务队列里去
console.log(1);
setTimeout(() => {
console.log(2);
}, 1000)
setTimeout(() => {
console.log(3);
}, 500)
// 1 3 2
复制代码
例子-2:
console.log(1);
setTimeout(() => {
console.log(2);
}, 1000)
while(true) {}
setTimeout(() => {
console.log(3);
}, 500)
复制代码
此时不会输出 2 和 3。是因为 while 是个死循环。当时间到达时,要看栈中是否已经执行完了,如果没有执行完,就不会调用队列中的内容
例子-3:
console.log('global')
for (var i = 1;i <= 5;i ++) {
setTimeout(function() {
console.log('setTimeout1:', i)
},i*1000)
console.log(i)
}
new Promise(function (resolve) {
console.log('promise1')
resolve()
}).then(function () {
console.log('then1')
})
setTimeout(function () {
console.log('timeout2')
new Promise(function (resolve) {
console.log('timeout2_promise')
resolve()
}).then(function () {
console.log('timeout2_then')
})
}, 1000)
// 输出结果
// global
// 1
// 2
// 3
// 4
// 5
// promise1
// then1
// setTimeout1: 6
// timeout2
// timeout2_pormise
// timeout2_then
// setTimeout1: 6
// setTimeout1: 6
// setTimeout1: 6
// setTimeout1: 6
// setTimeout1: 6
复制代码
先执行主线程中的任务输出 global 和 for 循环中的 i。setTimeout 属于宏观任务,时间到了会放到宏任务队列中,setTimeout1 会根据 i * 1000 依次放入到宏任务队列中。Promise 构造函数中的执行器属于同步任务,会先输出 promise1, 调用 resolve 后改变了 Promise 状态,调用 then 方法会将任务放入微任务队列中。此时 setTimeout2 时间到会被放入宏任务队列中。timeout2_promise 也会根据promise1的执行过程进入到微任务队列。
每次 Event Loop 触发执行的过程是:
A> 执行主线程中的任务,调用栈为空
B> 取出==所有== micro-task 任务队列 => 执行
C> 取出==一个== macro-task 任务 => 执行
D> 取出==所有== micro-task 任务队列 => 执行
E> 重复 C 和 D
node 系统中的 Event Loop
- js 代码会交给 V8 引擎进行处理
- 代码中用到的 node api 会交给 libuv 库处理
- libuv 通过阻塞 i/o 和多线程实现异步 io
- 通过事件驱动方式,将结果放到事件队列中,最终交给我的应用