浏览器中的 Event loop
JavaScript 是单线程的
首先,语言产生的时代多进程多线程的架构并不普及,基于当时硬件支持也不好,而且 多线程比较复杂,多线程操作需要加锁,使得编码方面就会变得很复杂;而且当时 JavaScript 只是处理页面的用户交互,以及操作 DOM 和 CSS,如果是多线程,两个线程 同时操作 同一个 DOM, 就会产生 预期的结果,所以 JavaScript 选择单线程。
基本概念
JavaScript 分为 同步任务 和 异步任务;
所有同步任务都在 main thread 主线程 上执行,形成一个 执行栈(execution context stack),所有的任务都会被放到执行栈等待主线程执行;
执行栈 采用的是 后进先出 的规则,当函数执行的时候,会被添加到栈的顶部,当执行栈执行完成后,就会从栈顶移出,直到栈内被清空;
主线程之外,存在一个任务队列(task queue),在走主流程的时候,如果碰到异步任务,那么就在 任务队列 中放置这个异步任务;
一旦 执行栈 中所有 同步任务执行完毕,系统就会读取 任务队列,看看里面存在哪些事件。那些对应的异步任务 在异步任务有了结果后,将注册的回调函数放入 任务队列 中等待主线程空闲的时候(调用栈被清空),被读取到栈内等待主线程的执行。
任务队列 Task Queue,即队列,是一种先进先出的一种数据结构。
宏任务和微任务
在 JavaScript 中,任务被分为两种,一种 宏任务(MacroTask),一种叫 微任务(MicroTask)。
MacroTask(宏任务)有:setTimeout、setInterval、setImmediate、I/O、UI Rendering
MicroTask(微任务)有:Process.nextTick(Node独有)、Promise、Object.observe(废弃)、MutationObserver
微任务和宏任务是绑定的,每个宏任务在执行时,会创建自己的微任务队列。
宏任务结束后,会执行渲染,然后执行下一个宏任务, 而微任务可以理解成在当前宏任务执行后立即执行的任务。
宏任务 --> 微任务 --> 渲染 --> 宏任务 --> ......
微任务就是一个需要异步执行的函数,执行时机是在主函数执行结束之后、当前宏任务结束之前。
当JavaScript执⾏⼀段脚本的时候,V8会为其创建⼀个全局执行上下文,在创建全局执行上下文的 同时,V8引擎也会在内部创建⼀个 微任务队列 来存放微任务;
在当前宏任务执行的过程中,有时候会产生多个微任务;如果在执行微任务的过程中,产⽣了新的微任务,同样会将该微任务添加到微任务队列中,V8引擎⼀直循环执行微任务队列中的任务,直到队列为空才算执行结束,并不会推迟到下个宏任务中执行。
总结 :微任务和宏任务是绑定的,每个宏任务在执行时,会创建自己的微任务队列。
浏览器事件循环的进程模型
执行栈在执行完 同步任务 后,查看 执行栈 是否为空,如果执行栈为空,就会去检查 微任务 (microTask)队列是否为空,如果为空的话,就执行 Task(宏任务),否则就一次性执行完所有微任务。
每次单个宏任务 执行完毕后,检查 微任务 (microTask) 队列是否为空,如果不为空的话,会按照 先入先出 的规则全部执行完微任务(microTask)后,设置 微任务(microTask)队列为null,然后再执行 宏任务,如此循环。
注意 :async/await 在底层转换成了 promise 和 then 回调函数,也就是说,这是 promise 的语法糖。
sync函数在await之前的代码都是同步执行的,可以理解为await之前的代码属于new Promise时传入的代码,await之后的所有代码都是在Promise.then中的回调
总结
每当一个 js 脚本运行的时候,都会先执行script中的整体代码;
当执行栈中的同步任务执行完毕,就会执行 微任务 中的第一个任务并推入执行栈执行,当执行栈为空,则再次读取执行微任务,循环重复直到微任务列表为空;
等到微任务列表为空,才会读取宏任务中的第一个任务并推入执行栈执行,当执行栈为空则再读取执行微任务,微任务为空才再读取执行宏任务,如此循环。
Nodejs 中的 Event Loop
Node 中的 Event Loop 和浏览器中的是完全不相同的东西
nodejs 的 event loop 分为 6 个阶段,每当进入某一个阶段的时候,都会从对应的回调队列中取出函数去执行。当队列为空或者执行的回调函数数量到达系统设定的阈值,就会进入下一阶段。
timers 阶段:这个阶段执行timer(setTimeout、setInterval)的回调
I/O callbacks 阶段:执行一些系统调用错误,比如网络通信的错误回调
idle, prepare 阶段:仅node内部使用
poll 阶段:获取新的I/O事件, 适当的条件下node将阻塞在这里
check 阶段:执行 setImmediate() 的回调
close callbacks 阶段:执行 socket 的 close 事件回调
执行顺序
一开始执行栈的同步任务(宏任务)执行完毕后(将异步任务放入对应的队列),再会先去执行微任务(这点跟浏览器端的一样, 这个过程中 先执行 process.nextTick, 在执行其他 微任务,process.nextTick 优先级较高),接着进入 event loop 的 六个阶段,循环执行。
详细的可以参看文档 事件循环
process.nextTick(微任务)
这个函数其实是独立于 Event Loop 之外的,它有一个自己的队列,当每个阶段完成后,如果存在 nextTick 队列,就会清空队列中的所有回调函数,并且优先于其他 microtask 执行。
浏览器 Event loop 和 nodejs Event loop 差异
浏览器环境下,microtask的任务队列是每个macrotask执行完之后执行。
在Node.js中,microtask 会在事件循环的各个阶段之间执行,也就是一个阶段执行完毕,就会去执行microtask队列的任务。