进程和线程
JavaScript的单线程指的是,代码执行的时候一个进程里只有一个线程
进程和线程的区别:
- 进程是CPU资源分配的最小单位;线程是CPU调度的最小单位
- 一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线
- 一个进程的内存空间可以给其中的线程共享
进程类似于工厂,有很多的资源需要加工;而线程就类似于其中的工人,每个人利用资源加工完成一个个的产品
多进程就好比你在电脑前既能敲着代码也能同时听着歌,这就是多进程
多线程一个程序中可以同时运行多个不同的线程来执行不同的任务
JavaScript单线程的原因是,JS可以用来操作DOM,如果是多线程的话,加入一个线程要修改DOM-A,一个线程又要删除DOM-A,那么浏览器就不知道该已谁为准了。
执行栈和任务队列
JavaScript将所有任务分为同步任务和异步任务
执行栈相当于一个存储函数调用的栈结构,遵循先进后出的原则
- 所有同步任务都在主线程上执行,形成一个执行栈
- 主线程之外,还存在任务队列。异步任务有了运行结果,就在任务队列之中放置一个
- 一旦执行栈中的所有同步任务执行完毕,系统就会读取任务队列
- 主线程不断重复执行3
其中异步任务分为:
- 宏任务: script(整体代码), setTimeout, setInterval, setImmediate(Nodejs), I/O, UI rendering
- 微任务: process.nextTick(Nodejs), Promises, Object.observe, MutationObserver
注:setTimeout(fn,t)真正的含义是指,t秒之后,将fn放入任务队列,当主线程执行完执行栈上的任务后,从任务队列中取出fn执行。所以setTimeout第二个参数的意义是不低于t秒后执行fn
事件循环
主线程从任务队列中读取事件,这个过程是循环不断的,这种运行机制就叫EventLoop(事件循环)
浏览器下JavaScript运行机制:
- JavaScript代码运行,此时执行栈空,微任务队列空,宏任务队列里有且只有一个script脚本(整体代码)
- 全局上下文(script标签)被推入执行栈,同步代码执行,同时会产生新的微任务与宏任务
- 当全局上下文(script标签)中同步任务执行完毕后,会去清空此宏任务产生的微任务队列
- 执行渲染操作,更新界面
- 检查是否存在Web worker任务,如果有,则对其进行处理
- 然后再去宏任务队列处理下一个宏任务,不断重复3-6(Event Loop)
Node中Event Loop
Node.js采用V8作为js的解析引擎,而I/O处理方面使用了自己设计的libuv,libuv是一个基于事件驱动的跨平台抽象层,封装了不同操作系统一些底层特性,对外提供统一的API,事件循环机制也是它里面的实现
Node运行机制
- V8引擎解析JavaScript脚本,解析后的代码,调用Node API(libuv库)
- libuv库将不同的任务分配给不同的线程,形成一个Event Loop(事件循环),以异步的方式将任务的执行结果返回给V8引擎
6个阶段
外部输入数据-->轮询阶段(poll)-->检查阶段(check)-->关闭事件回调阶段(close callback)-->定时器检测阶段(timer)-->I/O事件回调阶段(I/O callbacks)-->闲置阶段(idle, prepare)-->轮询阶段...
- poll阶段获取新的I/O事件, 适当的条件下node将阻塞在这里
- check阶段执行setImmediate()的回调
- closecallbacks 阶段执行socket的close事件回调
- timers阶段这个阶段执行timer(setTimeout、setInterval)的回调
- I/O callbacks阶段处理一些上一轮循环中的少数未执行的 I/O 回调
- idle, prepare阶段仅node内部使用
注:
process.nextTick独立于 Event Loop 之外的,它有一个自己的队列,当每个阶段完成后,如果存在 nextTick 队列,就会清空队列中的所有回调函数
Node端事件循环中的异步队列也是这两种:宏任务队列和微任务队列
- 常见的 macro-task 比如:setTimeout、setInterval、 setImmediate、script(整体代码)、 I/O 操作等
- 常见的 micro-task 比如: process.nextTick、new Promise().then(回调)等
Node与浏览器的 Event Loop 差异
- 浏览器环境下,microtask的任务队列是每个macrotask执行完之后执行
- 在Node中,microtask会在事件循环的各个阶段之间执行,也就是一个阶段执行完毕,就会去执行microtask队列的任务
setTimeout(()=>{
console.log('timer1')
Promise.resolve().then(function() {
console.log('promise1')
})
}, 0)
setTimeout(()=>{
console.log('timer2')
Promise.resolve().then(function() {
console.log('promise2')
})
}, 0)