Event Loop即事件循环,是指浏览器或Node的一种解决javaScript单线程
运行时不会阻塞的一种机制,也就是我们经常使用异步的原理。
因为JavaScript就是单线程,也就是说,同一个时间只能做一件事。单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。
所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。
在讲EventLoop之前,先说一下什么是 同步任务 和 异步任务。
简单点说 :
同步任务:会立即执行的任务
异步任务:不会立即执行的任务(异步任务又分为宏任务与微任务)
在异步任务中,任务被分为两种,一种宏任务(MacroTask)
也叫Task,一种叫微任务(MicroTask)
。
宏任务:由宿主对象发起的任务(setTimeout)
宏任务包括 script
, setTimeout
,setInterval
,setImmediate(Node.js)
,I/O
,postMessage
, MessageChannel
,UI rendering
微任务:由js引擎发起的任务(promise)
微任务包括 process.nextTick(Node.js)
,promise.then()
,promise.catch()
,MutationObserver
。
宏任务与微任务的概念不必强记,理解不了可先记住宏任务和微任务包含哪些情况。
由上述可知,在代码执行的过程中,同步任务会立即执行,异步任务会通过一些手段和过程才会拿到结果。所以在异步任务等待结果的同时,可先执行其后的同步任务。当异步任务有结果的时候,在回过头来执行异步任务。
所以 EventLoop 的执行机制如下:
(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
(2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
(3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
(4)主线程不断重复上面的第三步。
话不多说,直接上题
console.log('script start')
async function async1() {
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2 end')
}
async1()
setTimeout(function() {
console.log('setTimeout')
}, 0)
new Promise(resolve => {
console.log('Promise')
resolve()
})
.then(function() {
console.log('promise1')
})
.then(function() {
console.log('promise2')
})
console.log('script end')
这道题有些繁琐,先放置一下,后面将会对这道题进行讲解。
EvnentLoop执行的机制就是先执行同步代码,接着是微任务,然后是宏任务。
遇到 Promise
在解答上题之前,先来两道简单点的,小试牛刀一下:
例题 1:
setTimeout(() => {
console.log(‘A’);
}, 0);
var obj = {
func: function() {
setTimeout(function() {
console.log(‘B’);
}, 0);
return new Promise(function(resolve) {
console.log(‘C’);
resolve();
});
},
};
obj.func().then(function() {
console.log(‘D’);
});
console.log(‘E’);
根据事件循环机制的执行顺序,上述代码执行步骤如下:
执行 setTimeout ,由于是宏任务,将其放置进宏任务队列,此时宏任务队列为 [ 'A ’ ]
接着执行 obj.func() , 先执行 setTimeout,由于是宏任务,将其放置进宏任务队列,此时宏任务队列为 [ 'A ','B ’ ]
然后函数返回值是一个 Promise,因为这是一个同步操作,所以先打印出 'C ’
紧接着是 promise.then() , 由于是一个微任务,将其放置进微任务队列,此时微任务队列为 [ 'D ’ ]
接着就会执行同步任务,打印出 'E ’
因为微任务执行比宏任务早,所以打印 'D ’
然后会执行宏任务,依次打印出来 'A ’ 和 'B ’
所以,这道题的打印顺序就是
C -> E -> D -> A -> B
要注意的是 obj.func().then() 这里,obj.func() 是普通函数/同步代码,后面的 .then() 才是微任务。
例题2:
function go() {
console.log(5)
}
let p = new Promise(resolve => {
resolve(1);
Promise.resolve(go()).then(() => console.log(2));
console.log(4);
}).then(t => console.log(t));
console.log(3);
上述代码执行步骤如下:
so,上述代码的打印结果为
5 -> 4 -> 3 -> 2 -> 1
要注意的是 Promise.resolve(go()) 这里也是普通函数/同步代码,当执行到这一行的时候,会立即执行 go(),后面的 .then() 才是微任务。
以上两道小题,是关于 Promise 的EventLoop分析。
遇到 async/await
根据定义,我们知道,async/await 仅仅是生成器的语法糖,所以不要怕,只要把它转换成 Promise 的形式即可。
async function foo() {
console.log(1)
await bar();
console.log(2)
}
async function bar() {
console.log(3)
}
foo();
可以转换成 Promise的形式,如下
function foo(){
console.log(1)
Promise.resolve(bar()).then(()=>{
console.log(2)
})
}
function bar(){
console.log(3)
}
foo()
执行步骤如下:
所以执行的顺序是:
1 -> 3 -> 2
所以,开篇的那段代码,可改写成如下:
function async1() {
console.log('async1 start'); // 2
Promise.resolve(async2()).then(() => {
console.log('async1 end'); // 6
});
}
function async2() {
console.log('async2'); // 3
}
console.log('script start'); // 1
setTimeout(function() {
console.log('settimeout'); // 8
}, 0);
async1();
new Promise(function(resolve) {
console.log('promise1'); // 4
resolve();
}).then(function() {
console.log('promise2'); // 7
});
console.log('script end'); // 5
其实,上面的代码都弄清楚之后,对于EventLoop的执行机制就掌握的差不多了。
接下来是一道强化的题,有兴趣的可以看一下
console.log('1')
setTimeout(()=>{
console.log('2');
new Promise(resolve=>{
console.log('3')
resolve();
}).then(()=>{
console.log('4')
})
},0)
new Promise(resolve=>{
console.log('5')
resolve();
}).then(()=>{
console.log('6')
})
setTimeout(()=>{
console.log('7');
},0)
setTimeout(()=>{
console.log('8');
new Promise(resolve=>{
console.log('9')
resolve();
}).then(()=>{
console.log('10')
})
},0)
new Promise(resolve=>{
console.log('11')
resolve();
}).then(()=>{
console.log('12')
})
console.log('13');
这里只把运行的答案写一下
1 -> 5 -> 11 -> 13 -> 6 -> 12 -> 2 -> 3 -> 4 -> 7 -> 8 -> 9 -> 10