Javascript的执行机制(Event Loop)
作为前端程序员,无论是菜鸟还是老鸟,javascript的运行机制是绕不过的一个问题,理解这些,会让我们对js有更深的认知,减少在编码时候出现一些异步问题的bug。那么我们先来看一道题目,代码如下:
示例1
console.log(1);
setTimeout(function() {
console.log(2);
}, 0);
new Promise(function(resolve) {
console.log(3);
resolve(Date.now());
}).then(function() {
console.log(4);
});
console.log(5);
setTimeout(function() {
new Promise(function(resolve) {
console.log(6);
resolve(Date.now());
}).then(function() {
console.log(7);
});
}, 0);
如果你的答案不是1 3 5 4 2 6 7那么就需要认证往下看了,如果你答对了,那确实很厉害,也可以接着往下看分析,比较一下和自己的思路是否一致。
Javascript中的异步是如何实现的?
任务队列:
1. 所有同步任务都在主线程上执行, 形成一个执行栈(stack)。
2. 主线程之外, 还存在一个任务队列Event Loop, 异步任务在event table中注册函数, 当满足触发条件(即DOM,AJAX,setTimeout,setImmediate有返回结果了) 后, 被推入任务队列(Event Loop)。
3. 一旦执行栈(stack) 中所有同步任务都执行完了, 系统就会读取任务队列(Event Loop), 看看里面有哪些事件.那些对应的异步任务, 于是结束等待状态, 进入执行栈, 开始执行 。
4. 主线程不断重复上面的第三步。
下面我们写一段代码分析
示例2
console.log(1);
setTimeout(function () {
console.log(2);
}, 0);
console.log(3);
上面代码的执行结果是1 2 3,我们按照步骤分析:
1. console.log(1)是同步任务,房入主线程;
2. setTimeout是异步任务,被放入eventtable,0秒后被推入主任务队列(Event Loop)里;
3. console.log(3)是同步任务,被放到主线程里。
4. 当1,3在控制台被打印后,主线程去Event Loop(事件队列)里查看是否有可执行的函数,发现有setTimeout然后执行setTimeout里的函数,这就是Event Loop。
什么是Event Loop?
主线程从任务队列(Event Loop) 中读取事件, 这个过程是循环不断的, 所以整个的这种运行机制又称为Event Loop(事件循环)。
上图中, 主线程运行的时候, 产生堆(heap) 和栈(stack), 栈中的代码调用各种外部API, 它们在” 任务队列(Event Loop)” 中加入各种事件( click, load, done)。 只要栈中的代码执行完毕, 主线程就会去读取” 任务队列(Event Loop)”, 依次执行那些事件所对应的回调函数。
示例3
setTimeout(function() {
console.log('定时器开始啦')
});
new Promise(function(resolve) {
console.log('马上执行for循环啦');
for(var i =0; i <10000; i++) {
i ==99 &&resolve();
}
}).then(function() {
console.log('执行then函数啦')
});
console.log('代码执行结束');
尝试按照,上文我们刚学到的js执行机制去分析:
1.setTimeout 是异步任务,被放到event table
2.new Promise是同步任务,被放到主线程里,直接执行打印console.log('马上执行for循环啦');
3.then里的函数是异步任务,被放到event table
4.console.log('代码执行结束');是同步代码,被放到主线程里,直接执行
所以根据分析的结果是:马上执行for循环啦---代码执行结束---定时器开始啦---执行then函数啦
自己运行了下代码后,结果居然不是这样的,而是: 马上执行for循环啦---代码执行结束---执行then函数啦---定时器开始啦
事实上,按照异步和同步的方式来划分,并不准确,而准确的划分方式是:
macro-task(宏任务):script(整体代码), setTimeout, setInterval, setImmediate(node.js), IO操作(网络请求、文件读), UI rendering(渲染任务)。
micro-task(微任务):process.nextTick(node.js), Promise.resolve, MutationObserver(html5新特性)
按照这种分类方式,js的执行机制是:
1.执行一个宏任务,过程中如果遇到微任务,就将其放到微任务的"事件队列"里
2.当前宏任务执行完成后,会查看微任务的"事件队列",并将里面全部的微任务依次执行完
3.重复以上2步骤,结合图1和图2就是更为准确的js执行机制了
那么,去分析例3:
1.首先执行script下的宏任务,遇到setTimeout,将其放到宏任务的“队列”里
2.遇到 new Promise直接执行,打印"马上执行for循环啦"
3.遇到then方法,是微任务,将其放到微任务的“队列”里。
4.遇到console.log('代码执行结束');是同步任务,直接打印"代码执行结束"
5.本轮宏任务执行完毕,查看本轮的微任务,发现有一个then方法里的函数,打印"执行then函数啦"
6.到此,本轮的event loop 全部完成。
7.下一轮的循环里,先执行一个宏任务,发现宏任务的“队列”里有一个setTimeout里的函数,执行打印"定时器开始啦" 所以最后的执行顺序是: 马上执行for循环啦---代码执行结束---执行then函数啦---定时器开始啦
最后,我们再分析示例1
执行步骤如下:
执行 log(1),输出 1;
遇到 setTimeout,将回调的代码 log(2)添加到宏任务中等待执行;
执行 console.log(3),将 then 中的 log(4)添加到微任务中;
执行 log(5),输出 5;
遇到 setTimeout,将回调的代码 log(6, 7)添加到宏任务中;
宏任务的一个任务执行完毕,查看微任务队列中是否存在任务,存在一个微任务 log(4)(在步骤 3 中添加的),执行输出 4;
取出下一个宏任务 log(2)执行,输出 2;
宏任务的一个任务执行完毕,查看微任务队列中是否存在任务,不存在;
取出下一个宏任务执行,执行 log(6),将 then 中的 log(7)添加到微任务中;
宏任务执行完毕,存在一个微任务 log(7)(在步骤 9 中添加的),执行输出 7;
因此,最终的输出顺序为:1, 3, 5, 4, 2, 6, 7;