阅读 3244

浏览器说:虽然都叫event loop,但是我和node不一样

讨论event loop要做到以下两点

  • 首先要确定好上下文,nodejs和浏览器的event loop是两个有明确区分的事物,不能混为一谈。
  • 其次,讨论一些js异步代码的执行顺序时候,要基于node的源码而不是自己的臆想。

简单来讲:

  • nodejs的event是基于libuv,而浏览器的event loop则在html5的规范中明确定义。
  • libuv已经对event loop作出了实现,而html5规范中只是定义了浏览器中event loop的模型,具体实现留给了浏览器厂商。

浏览器中的event loop

浏览器事件环中js分为两部分,一个叫heap(堆),一个叫stack(栈)。

对象放在heap(堆)里,常见的基础类型和函数放在stack(栈)里,函数执行的时候在栈里执行。栈里函数执行的时候可能会调一些Dom操作,ajax操作和setTimeout定时器,这时候要等stack(栈)里面的所有程序先走(注意:栈里的代码是先进后出),走完后再走WebAPIs,WebAPIs执行后的结果放在callback queue(回调的队列里,注意:队列里的代码先放进去的先执行),也就是当栈里面的程序走完之后,再从任务队列中读取事件,将队列中的事件放到执行栈中依次执行,这个过程是循环不断的。

简单来讲:

  • 1.所有同步任务都在主线程上执行,形成一个执行栈
  • 2.主线程之外,还存在一个任务队列。只要异步任务有了运行结果,就在任务队列之中放置一个事件。
  • 3.一旦执行栈中的所有同步任务执行完毕,系统就会读取任务队列,将队列中的事件放到执行栈中依次执行
  • 4.主线程从任务队列中读取事件,这个过程是循环不断的

整个的这种运行机制又称为Event Loop(事件循环)

概念中首先要明白是:stack(栈)和queue(队列)的区别,它们是怎么去执行的?

栈方法LIFO(Last In First Out):先进后出(先进的后出),典型的就是函数调用。

//执行上下文栈  作用域
var a = "aa";
function one(){
    let a = 1;
    two();
    function two(){
        let b = 2;
        three();
        function three(){
            console.log(b)
        }
    }
}
console.log(a);
one();

复制代码
aa
2
复制代码

图解执行原理:

执行栈里面最先放的是全局作用域(代码执行有一个全局文本的环境),然后再放one, one执行再把two放进来,two执行再把three放进来,一层叠一层。

那么怎么出呢,怎么销毁的呢?

最先走的肯定是three,因为two要是先销毁了,那three的代码b就拿不到了,所以是先进后出(先进的后出),所以,three最先出,然后是two出,再是one出。

队列方法FIFO(First In First Out)

(队头)[1,2,3,4](队尾) 进的时候从队尾依次进1,2,3,4 出的时候从对头依次出1,2,3,4

浏览器事件环中代码执行都是按栈的结果去执行的,但是我们调用完多线程的方法(WebAPIs),这些多线程的方法是放在队列里的,也就是先放到队列里的方法先执行。

那什么时候WebAPIs里的方法会再执行呢?

比如:stack(栈)里面都走完之后,就会依次读取任务队列,将队列中的事件放到执行栈中依次执行,这个时候栈中又出现了事件,这个事件又去调用了WebAPIs里的异步方法,那这些异步方法会在再被调用的时候放在队列里,然后这个主线程(也就是stack)执行完后又将从任务队列中依次读取事件,这个过程是循环不断的。

下面通过列子来说明:

例子1

console.log(1);
console.log(2);
setTimeout(function(){
    console.log(3);
})
setTimeout(function(){
    console.log(4);
})
console.log(5);
复制代码
// 结果
1
2
5
3
4
复制代码

1、首先执行栈里面的同步代码

1

2

5

2、栈里面的setTimeout事件会依次放到任务队列中,当栈里面都执行完之后,再依次从从任务队列中读取事件往栈里面去执行。

3

4

例子2

console.log(1);
console.log(2);

setTimeout(function(){
    console.log(3);
    setTimeout(function(){
        console.log(6);
    })
})
setTimeout(function(){
    console.log(4);
    setTimeout(function(){
        console.log(7);
    })
})
console.log(5)
复制代码
// 结果
1
2
5
3
4
6
7
复制代码

1、首先执行栈里面的同步代码

1

2

5

2、栈里面的setTimeout事件会依次放到任务队列中,当栈里面都执行完之后,再依次从从任务队列中读取事件往栈里面去执行。

3

4

3、当执行栈开始依次执行setTimeout时,会将setTimeout里面的嵌套setTimeout依次放入队列中,然后当执行栈中的setTimeout执行完毕后,再依次从从任务队列中读取事件往栈里面去执行。

6

7

例子3

console.log(1);
console.log(2);

setTimeout(function(){
    console.log(3);
    setTimeout(function(){
        console.log(6);
    })
},400)
setTimeout(function(){
    console.log(4);
    setTimeout(function(){
        console.log(7);
    })
},100)
console.log(5)
复制代码
// 结果
1
2
5
4
7
3
6
复制代码

在例子2的基础上,如果设置了setTimeout的时间,那就是按setTimeout的成功时间依次执行。

如上:这里的顺序是1,2,5,4,7,3,6。也就是只要两个set时间不一样的时候 ,就set时间短的先走完,包括set里面的回调函数,再走set时间慢的。(因为只有当时间到了的时候,才会把set放到队列里面去,这一点跟nodejs中的set设置了时间的机制差不多,可以看nodejs中的例子6,也是会先走完时间短,再走时间慢的。)

例子4

当触发回调函数时,会将回调函数放到队列中。永远都是栈里面执行完后再从任务队列中读取事件往栈里面去执行。

setTimeout(function(){
    console.log('setTimeout')
},4)
for(var i = 0;i<10;i++){
    console.log(i)
}
复制代码
// 结果
0
1
2
3
4
5
6
7
8
9
setTimeout
复制代码

在学习nodejs事件环之前,我们先了解一下宏任务和微任务在浏览器中的执行机制。也是面试中经常会被问到的。

宏任务和微任务

任务可分为宏任务和微任务,宏任务和微任务都是队列

  • macro-task(宏任务): setTimeout, setInterval, setImmediate, I/O
  • micro-task(微任务): process.nextTick, 原生Promise(有些实现的promise将then方法放到了宏任务中),Object.observe(已废弃), MutationObserver不兼容的,MessageChannel(消息通道,类似worker)

Promise.then(源码见到Promise就用setTimeout),then方法不应该放到宏任务中(源码中写setTimeout是迫不得已的),默认浏览器的实现这个then放到了微任务中。例如:

console.log(1)
let promise = new Promise(function(resolve,reject){
    console.log(3)
    resolve(100)
}).then(function(data){
    console.log(100)
})
console.log(2)
复制代码
1
3
2
100
复制代码

先走console.log(1),这里的new Promise()是立即执行的,所以是同步的,由于这个then在console.log(2)后面执行的,所以不是同步,是异步的。

那这跟宏任务和微任务有什么关系?

我们可以加一个setTimeout(宏任务)对比一下:

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(100)
})
console.log(2)
复制代码
1
3
2
100
setTimeout
复制代码

结论:在浏览器事件环机制中,同步代码先执行 执行是在栈中执行的,然后微任务会先执行,再执行宏任务

MutationObserver例子:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <div id="app"></div>
    <!-- 当dom加载完毕后,来一句渲染完成 -->
    <script>
        console.log(1)
        let observe = new MutationObserver(function(){
            console.log('渲染完成')
        });
        <!--监控app的节点列表是否渲染完成-->
        observe.observe(app,{
            childList:true
        })
        for(var i = 0;i<100;i++){
            let p = document.createElement('p');
            document.getElementById('app').appendChild(p);
        }
        for(var i = 0;i<100;i++){
            let p = document.createElement('p');
            document.getElementById('app').appendChild(p);
        }
        console.log(2)
    </script>
</body>
</html>
复制代码
// 结果
1
2
渲染完成
复制代码

MessageChannel例子

vue中nextTick的实现原理就是通过这个方法实现的

console.log(1);
let channel = new MessageChannel();
let port1 = channel.port1;
let port2 = channel.port2;
port1.onmessage = function(e){
    console.log(e.data);
}
console.log(2);
port2.postMessage(100);
console.log(3)
复制代码
// 浏览器中console结果 会等所有同步代码执行完再执行,所以是微任务晚于同步的
1
2
3
100
复制代码

nodejs中的event loop

node的特点:异步 非阻塞i/o node通过LIBUV这个库自己实现的异步,默认的情况下是没有异步的方法的。

nodejs中的event loop有6个阶段,这里我们重点关注poll阶段(fs的i/o操作,对文件的操作,i/o里面的回调函数都放在这个阶段)

event loop的每一次循环都需要依次经过上述的阶段。 每个阶段都有自己的callback队列,每当进入某个阶段,都会从所属的队列中取出callback来执行,当队列为空或者被执行callback的数量达到系统的最大数量时,进入下一阶段。这六个阶段都执行完毕称为一轮循环。

下面通过列子来说明:

例子1

console.log(1);
console.log(2);
setTimeout(function(){
    console.log('setTimeout1')
    Promise.resolve().then(function(){
        console.log('Promise')
    })
})
setTimeout(function(){
    console.log('setTimeout2')
})
复制代码
-> node eventloop.js
1
2
setTimeout1
setTimeout2
Promise
复制代码

图解执行原理:

1、首先执行完栈里面的代码

console.log(1);

console.log(2);

2、从栈进入到event loop的timers阶段,由于nodejs的event loop是每个阶段的callback执行完毕后才会进入下一个阶段,所以会打印出timers阶段的两个setTimeout的回调

setTimeout1

setTimeout2

3、由于node event中微任务不在event loop的任何阶段执行,而是在各个阶段切换的中间执行,即从一个阶段切换到下个阶段前执行。所以当times阶段的callback执行完毕,准备切换到下一个阶段时,执行微任务(打印出Piromise),

Promise

如果例子1看懂了,以下例子2-例子6自己走一遍。需要注意的是例子6,当setTimeout设置了时间,优先按时间顺序执行(浏览器事件环中例子3差不多)。例子7,例子8是重点。

例子2

console.log(1);
console.log(2);
setTimeout(function(){
    console.log('setTimeout1')
    Promise.resolve().then(function(){
        console.log('Promise')
    })
},1000)
setTimeout(function(){
    console.log('setTimeout2')
},1000)
复制代码
-> node eventloop.js
1
2
setTimeout1
Promise
setTimeout2
复制代码

例子3

console.log(1);
console.log(2);
setTimeout(function(){
    console.log('setTimeout1')
    Promise.resolve().then(function(){
        console.log('Promise')
    })
},2000)
setTimeout(function(){
    console.log('setTimeout2')
},1000)
复制代码
-> node eventloop.js
1
2
setTimeout2
setTimeout1
Promise
复制代码

例子4

console.log(1);
console.log(2);
setTimeout(function(){
    console.log('setTimeout1')
    Promise.resolve().then(function(){
        console.log('Promise1')
    })
})
setTimeout(function(){
    console.log('setTimeout2')
    Promise.resolve().then(function(){
        console.log('Promise2')
    })
})
复制代码
-> node eventloop.js
1
2
setTimeout1
setTimeout2
Promise1
Promise2
复制代码

例子5

console.log(1);
console.log(2);
setTimeout(function(){
    console.log('setTimeout1')
    Promise.resolve().then(function(){
        console.log('Promise1')
    })
},1000)
setTimeout(function(){
    console.log('setTimeout2')
    Promise.resolve().then(function(){
        console.log('Promise2')
    })
},1000)
复制代码
-> node eventloop.js
1
2
setTimeout1
setTimeout2
Promise1
Promise2
复制代码

例子6

console.log(1);
console.log(2);
setTimeout(function(){
    console.log('setTimeout1')
    Promise.resolve().then(function(){
        console.log('Promise1')
    })
},2000)
setTimeout(function(){
    console.log('setTimeout2')
    Promise.resolve().then(function(){
        console.log('Promise2')
    })
},1000)
复制代码
-> node eventloop.js
1
2
setTimeout2
Promise2
setTimeout1
Promise1
复制代码

例子7:setImmediate() vs setTimeout()

  • setImmediate 设计在poll阶段完成时执行,即check阶段;
  • setTimeout 设计在poll阶段为空闲时,且设定时间到达后执行;但其在timer阶段执行

其二者的调用顺序取决于当前event loop的上下文,如果他们在异步i/o callback之外调用,其执行先后顺序是不确定的

setTimeout(function timeout () {
  console.log('timeout');
},0);

setImmediate(function immediate () {
  console.log('immediate');
});
复制代码
-> node eventloop.js
timeout
immediate

-> node eventloop.js
immediate
timeout
复制代码

但当二者在异步i/o callback内部调用时,总是先执行setImmediate,再执行setTimeout

这是因为fs.readFile callback执行完后,程序设定了timer 和 setImmediate,因此poll阶段不会被阻塞进而进入check阶段先执行setImmediate,后进入timer阶段执行setTimeout

var fs = require('fs')

fs.readFile(__filename, () => {
  setTimeout(() => {
    console.log('timeout')
  }, 0)
  setImmediate(() => {
    console.log('immediate')
  })
})
复制代码
$ node eventloop.js
immediate
timeout
复制代码

例子8:process.nextTick()

process.nextTick()不在event loop的任何阶段执行,而是在各个阶段切换的中间执行,即从一个阶段切换到下个阶段前执行。

function Fn(){
    this.arrs;
    process.nextTick(()=>{
        this.arrs();
    })
}
Fn.prototype.then = function(){
    this.arrs = function(){console.log(1)}
}
let fn = new Fn();
fn.then();
复制代码
-> node eventloop.js
   1
复制代码

不加process.nextTick,new Fn()的时候,this.arrs是undefind,this.arrs()执行会报错;

加了process.nextTick,new Fn()的时候,this.arrs()不会执行(因为process.nextTick是微任务,只有在各个阶段切换的中间执行,所以它会等到同步代码执行完之后才会执行)这个时候同步代码fn.then()执行=>this.arrs = function(){console.log(1)},this.arrs变成了一个函数,同步执行完后再去执行process.nextTick(()=>{this.arrs();})就不会报错。

需要注意的是:nextTick千万不要写递归,可以放一些比setTimeout优先执行的任务

// 死循环,会一直执行微任务,卡机
function nextTick(){
    process.nextTick(function(){
        nextTick();
    })
}
nextTick()
setTimeout(function(){

},499)
复制代码

最后再来段代码加深理解

var fs = require('fs');

fs.readFile(__filename, () => {
  setTimeout(() => {
    console.log('setTimeout');
  }, 0);
  setImmediate(() => {
    console.log('setImmediate');
    process.nextTick(()=>{
      console.log('nextTick3');
    })
  });
  process.nextTick(()=>{
    console.log('nextTick1');
  })
  process.nextTick(()=>{
    console.log('nextTick2');
  })
});
复制代码
-> node eventloop.js
nextTick1
nextTick2
setImmediate
nextTick3
setTimeout
复制代码

1、从poll —> check阶段,先执行process.nextTick,

nextTick1

nextTick2

2、然后进入check,setImmediate,

setImmediate

3、执行完setImmediate后,出check,进入close callback前,执行process.nextTick

nextTick3

4、最后进入timer执行setTimeout

setTimeout

结论:在nodejs事件环机制中,微任务是在各个阶段切换的中间去执行的。

最后

  • 在浏览器的事件环机制中,我们需要了解的是栈和队列是怎么去执行的。

    栈:先进后出;队列:先进先出。

    所有代码在栈中执行,栈中的DOM,ajax,setTimeout会依次进入到队列中,当栈中代码执行完毕后,有微任务先会将微任务依次从队列中取出放到执行栈中执行,最后再依次将队列中的事件放到执行栈中依次执行。

  • 在nodejs的事件环机制中,我们需要了解的是node的执行机制是阶段型的,微任务不属于任何阶段,而是在各个阶段切换的中间执行。nodejs把事件环分成了6阶段,这里需要注意的是,当执行栈里的同步代码执行完毕切换到node的event loop时也属于阶段切换,这时候也会先去清空微任务。

  • 微任务和宏任务

    macro-task(宏任务): setTimeout, setInterval, setImmediate, I/O

    micro-task(微任务): process.nextTick, 原生Promise(有些实现的promise将then方法放到了宏任务中),Object.observe(已废弃), MutationObserver不兼容的

问题

如果在执行宏任务的过程中又发现了回调中有微任务,会把这个微任务提前到所有宏任务之前,等到这个微任务完成后再继续执行宏任务吗?

console.log(1);
setTimeout(function(){
    console.log(2);
    Promise.resolve(1).then(function(){
        console.log('promise1')
    })
})
setTimeout(function(){
    console.log(3)
    Promise.resolve(1).then(function(){
        console.log('promise2')
    })
})
setTimeout(function(){
    console.log(4)
    Promise.resolve(1).then(function(){
        console.log('promise3')
    })
})
复制代码
// node中 每个阶段切换中间执行微任务
1
2
3
4
promise1
promise2
promise3
复制代码
// 浏览器中  先走微任务
1
VM59:3 2
VM59:5 promise1
VM59:9 3
VM59:11 promise2
VM59:15 4
VM59:17 promise3
复制代码

以下例子也可以看看

// 例子1
console.log(1);
setTimeout(function(){
    console.log(2);
    Promise.resolve(1).then(function(){
        console.log('promise1')

        setTimeout(function(){
            console.log(3)
            Promise.resolve(1).then(function(){
                console.log('promise2')
            })
        })

    })
})
复制代码
// node
1
2
promise1
3
promise2
复制代码
// 浏览器
1
VM70:3 2
VM70:5 promise1
VM70:8 3
VM70:10 promise2
复制代码
// 例子2
console.log(11);
setTimeout(function(){
    console.log(2);
    Promise.resolve(1).then(function(){
        console.log('promise1')
    })
    setTimeout(function(){
        console.log(3)
        Promise.resolve(1).then(function(){
            console.log('promise2')
        })
    })
})
复制代码
// node
11
2
promise1
3
promise2
复制代码
// 浏览器
11
VM73:4 2
VM73:6 promise1
VM73:9 3
VM73:11 promise2
复制代码
文章分类
前端