Javascript 引擎是单线程机制, 首先我们要了解 Javascript 语言为什么是单线程
JavaScript 的主要用途主要是用户互动, 和操作 DOM 如果 JavaScript 同时有两个线程, 一个线程在某个 DOM 节点上添加内容, 另一个线程删除了这个节点, 这时这两个节点会有很大冲突, 为了避免这个冲突, 所以决定了它只能是单线程, 否则会带来很复杂的同步问题此外 html5 提出 web Worker 标准, 允许 JavaScript 脚本创建多个线程(UI 线程, 异步 HTTP 请求线程, 定时触发器线程...), 但是子线程完全受主线程控制, 这个新标准并没有改变 JavaScript 单线程的本质
在了解 event loop 之前, 我们先了解一下什么是栈和队列, 他们有什么特点? 请先看两张图
栈(stack) 是自动分配内存空间, 它由系统自动释放, 特点是先进后出 队列的特点是先进先出 再看一张图:
我们代码执行的时候, 都是在栈里执行的, 但是我调用多线程方法的时候是放到队列里的, 先放进去的先执行 那 WebAPIs 的方法什么时候放到栈里执行呢? 当栈里的代码执行完了, 会在队列里面读取出来, 放到栈执行比如: 写个事件, 事件里面再调用异步方法, 这些方法会在调用的时候, 放到队列里, 会不停的循环等到队列的代码干净了, 就停止循环了, 不然就会一直循环 看下面一串代码, 会输出什么?
- console.log(1);
- setTimeout(function(){
- console.log(2)
- },0)
- setTimeout(function(){
- console.log(3)
- },0)
- console.log('ok');
这段代码中, 会先把 setTimeout 的方法移到队列中, 当栈里的代码执行完之后, 会把队列里方法取出来放到栈中执行, 所以执行结果是:
1 ok 2 3
再对这串代码进行扩展
- console.log(1);
- //A
- setTimeout(function() {
- console.log(2);
- //C
- setTimeout(function() {
- console.log(4);
- //D
- setTimeout(function() {
- console.log(5);
- })
- })
- },
- 0)
- //B
- setTimeout(function() {
- console.log(3);
- //E
- setTimeout(function() {
- console.log(6);
- })
- },
- 0) console.log('ok');
这串代码中, 栈的代码执行的时候, 当触发回调函数时, 会将回调函数放到队列中, 所以, 先输出 1 和 ok 栈里的代码执行完之后, 会先读取第一个 setTimeout, 输出 2, 这时发现里面还有一个 setTimeout(既 C 行下的 setTimeout), 这个 setTimeout 又会放到队列中去然后执行 B 行下的 setTimeout, 输出 3, 这时 E 行下还有个 setTimeout, 这个 setTimeout 又会放到队列中当栈里代码执行完之后, 又会在队列中读取代码, 这时读取的是 C 行下的 setTimeout, 放到栈执行, 输出 4, 紧接着又发现 D 行下有 setTimeout, 这个 setTimeout 又放到队列中排队栈的代码执行完了, 又在队列中读取 E 行下的 setTimeout, 输出 6 执行完之后, 又在队列里读取 D 行下的 setTimeout, 输出 5 所以输出结果是:
1 ok 2 3 4 6 5
附图讲解:
- setTiemout(function(){
- console.log(1)
- },0)
- for(var i = 0;i<1000;i++){
- console.log(i)
- }
在当前队列里看到 setTimeout, 它会等着看事件什么时候成功所以它会先往下走, 走完以后, 再把 setTimeout 里的回调函数放到队列中即使 for 循环的代码走了 10s, 回调函数也会等到 10s 后再执行 所以, 浏览器的机制永远是: 先走完栈里代码, 才会到队列里去
宏任务和微任务
任务可分为宏任务和微任务 宏任务: setTimeout,setInterval,setImmediate,I/O 微任务: process.nextTick,Promise.then 队列可以看成是一个宏任务
微任务是怎么执行的? 同步代码先在栈中执行的, 执行完之后, 微任务会先执行, 再执行宏任务 先看一个例子:
- console.log(1)
- setTimeout(function(){
- console.log('setTimeout')
- },0)
- let promise = new Promise(function(resolve,reject){
- console.log(3);
- resolve(100);
- }).then(function(data){
- console.log(200)
- })
- console.log(2)
想一想会输出什么? 代码由上到下执行, 所以肯定先输出 1setTimeout 是宏任务, 会先放到队列中而 new Promise 是立即执行的, 它是同步的, 所以会先输出 3 因为 then 是异步的, 所以会先输出 2 因为 then 是微任务, 微任务走完, 才会走宏任务所以最终输出的结果是: 1 3 2 200 setTimeout ** 注意:** 浏览器的机制是把 then 方法放到微任务中 浏览器机制:
代码会先走我们的执行栈, 里面有变量, 函数等等栈的代码走完以后, 会先去微任务, 微任务里面可能有很多回调函数(比如: 栈里有 promise 的 then 方法, then 的回调函数会放到微任务里去), 栈里面可能还有 setTimeout, 它会把 setTimeout 的回调函数放到宏任务中什么时候放的呢? 就是当时间到达的时候, 会放到队列里当栈的代码都执行完了, 它会先取微任务的 then, 执行执行完之后, 再取宏任务的代码(自己都快说晕了~~)
猜猜看:
- console.log(1);
- setTimeout(function(){
- console.log(2);
- Promise.resolve(1).then(function(){
- console.log('ok')
- })
- })
- setTimeout(function(){
- console.log(3)
- })
你猜输出什么~ 分析: 先默认走栈, 输出 1 此时并没有微任务, 所以微任务不会执行先走第一个 setTimeout, 输出 2, 同时将微任务放到队列中, 执行微任务, 输出 ok, 微任务执行完, 再走宏任务, 输出 3
** 注意:** 浏览器和 node 环境输出是不一样的哦~
--------- 此处是分割线 ------------
node 的 event loop
接下来说说 node 的事件环 先画张图吧
由图可以看出微任务不在事件环里那代码怎么走? 同样上面的例题:
- console.log(1);
- setTimeout(function(){
- console.log(2);
- Promise.resolve(1).then(function(){
- console.log('ok')
- })
- })
- setTimeout(function(){
- console.log(3)
- })
先将 2 个定时器放到 A 中, 先输出 1; 这时候栈里走完了, 该走事件环了在走事件环之前, 会先将微任务清空, 第一次微任务没有东西, 就滤过了之后该走事件环了, 这时候先走 timers 这时候 setTimeout 不一定到达时间, 如果到达时间, 就直接执行了如果时间没到达, 这时候可能先略过, 接着往下走, 走到 poll 轮询阶段, 发现没有读文件之类的操作, 然后它会等着, 等到 setTimeout 的时间到达如果时间到达了, 它会把到达时间的定时器全部执行比如先走第一个 setTimeout, 并且把 then 方法放到微任务中它会把到达时间的 setTimeout 队列全部清掉(全部执行完), 再走微任务假如 poll 轮询有很多个 I/O 操作, 它会把 I/O 操作都走完, 再走 timers 它是一个队列一个队列的清空, 而不是取出一个, 执行一下, 取出一个, 执行一下所以它会把 2 个 setTimeout 都走完, 再走 then 所以在 node 的输出结果是:
1 2 3 ok
再来个进阶的栗子:
- process.nextTick(function(){
- console.log(1)
- })
- setImmediate(function(){
- console.log(2)
- })
它会先走栈的内容, 栈啥都没有当它要走事件环的时候, 会将微任务清空发现微任务有 nextTick, 它会把 nextTick 执行完, 再走事件环发现 timers 和 poll 都没有东西, 它就会走 theck 阶段 nextTick 和 then 都是在阶段转化时才调用所谓的阶段转化, 就是刚开始走当前栈, 在当前栈转到 timers 的时候, 清空微任务
事件循环的顺序, 决定 js 代码的执行顺序进入整体代码 (宏任务) 后, 开始第一次循环接着执行所有的微任务然后再次从宏任务开始, 找到其中一个任务队列执行完毕, 再执行所有的微任务
Node.js 的 Event Loop
V8 引擎解析 JavaScript 脚本
解析后的代码, 调用 Node API
libuv 库负责 Node API 的执行它将不同的任务分配给不同的线程, 形成一个 Event Loop(事件循环), 以异步的方式将任务的执行结果返回给 V8 引擎
V8 引擎再将结果返回给用户
先说到这里吧, 有欠缺的后续再补充
来源: https://juejin.im/post/5aba14526fb9a028c675bb15