前言:本人表述能力不太好,如果有不清晰的地方请一定回复,我会改。

先确定几个概念:

1.单线程:作为浏览器脚本语言,用于解释用户操作,为了避免复杂性,js设计之初就是单线程的,这个是毋庸置疑的,也不会改变。
2.[同步,异步,阻塞,非阻塞](https://www.zhihu.com/question/19732473): 
  同步和异步关注的是消息通信机制,区别在于是否等待结果返回。
  阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态。
3.执行栈,任务队列:当调用js代码的时候,会生成一个执行上下文。当开始调用一系列的方法,这些方法就存储在执行栈中,等待依次执行。执行栈是执行同步任务。任务队列存放执行当前上下文的异步任务
复制代码

什么是事件循环

单线程意味着事件是一个一个执行的,前一个执行的时候,后一个需要等待着。但前一个若是网络请求,可能长时间没有回复,就容易导致CPU浪费。执行完同步之后再执行异步,解决了一部分等待的问题。同步的任务在执行栈上执行,遇到异步就放进任务队列里,执行栈中的执行完后执行异步队列。 所谓循环,就是上述事情在重复执行。它经常按照类似如下的方式来被实现:

while (queue.waitForMessage()) {
    queue.processNextMessage();
}
复制代码

事件循环是一种运行机制,js引擎需要执行的时候,将需要执行的消息放进执行栈中,开辟执行上下文。事件循环就是一直重复循环检查执行栈中是否有待执行的消息,当消息完整的执行完后,检查是否有未执行的消息,有就继续执行,没有保持监听。

怎么执行事件

简单事件执行

以下这种简单代码怎么执行呢?

function test () {
    console.log(1);
    console.log(2);
}
复制代码

简单调用执行栈
这个很好理解,代码一行一行执行,入栈,执行完了之后出栈。console.log是立即执行的,所以在执行console.log后立即出栈了。最后只剩下test{}执行后进入下一次循环。

函数调用

那如果有函数调用呢?执行栈如何处理?

function foo(b) {
  var a = 10;
  return a + b + 11;
}

function bar(x) {
  var y = 3;
  return foo(x * y);
}

console.log(bar(7)); // 返回 42
复制代码

函数调用执行栈

宏任务

函数调用的时候,执行栈中增加了被调用的函数的上下文,执行完被调用的函数后再继续执行当前函数。 项目中有很多异步代码。比如定时器,这些代码会安排在任务队列里,等待当前主函数执行后依次执行。

  console.log('这是开始');

  setTimeout(function cb() {
    console.log('这是来自第一个回调的消息');
  });

  console.log('这是一条消息');

  setTimeout(function cb1() {
    console.log('这是来自第二个回调的消息');
  });

  console.log('这是结束');
复制代码

异步调用执行栈
同步代码顺序执行,异步代码另起任务队列,只有在执行栈为空的情况下,任务队列才会开始工作。所以代码中执行到setTimeout时,往任务队列中添加事件,但并没有执行。而当console.log('这是结束');执行出结果后执行任务队列里的任务。队列是先进先出,所以首先执行cb,最后执行cb1;

微任务

ECMAScript 2015 引入了 Promises(也在 ES6 / ES2015 中引入) ,使用了作业队列(Job Queue)概念,这是一种尽快执行异步函数的方法。异步队列分为两种,消息队列(也有叫宏任务)和作业队列(也有叫微任务)。执行顺序为: 当前执行上下文 -> 微任务 -> 宏任务根据事件循环理论分析代码

var t = new Promise((resolve, reject) => {
    console.log('宏事件');
    resolve()
}).then(() => {
    console.log('微事件');
})
setTimeout(() => {
    console.log(t.then(opt => {
        console.log('内层微事件')
    }));
    console.log('内层宏事件')
})
console.log('外层事件');
复制代码

宏任务微任务执行栈
promise的方法立即执行,而其回调则是在微任务队列中。setTimeout在宏任务队列中。主任务执行结束后,执行微任务,再执行宏任务。

事件循环和浏览器的关系

事件循环是js代码执行顺序的解释,但是这里面没有说到什么时候会对用户界面产生影响。 我在W3C的文章上看到的是这样的:

-运行JS代码
-运行微任务队列
-执行布局和IO工作
-运行宏任务队列。
复制代码

有结论,很美好,验证下。

<!DOCTYPE html>
<html lang="en">
    <body>
        <div class="outer" style="width:200px;height:200px;background-color: #ccc"></div>
    </body>
    <script>
        var outer = document.querySelector('.outer');
    
        function onClick() {
            alert('start')
            outer.innerHTML = 'main func';
            alert('main func do')
            setTimeout(function () {
                alert('setTimeout start')
                outer.innerHTML = 'setTimeout';
                alert('setTimeout end')
            }, 10);
    
            Promise.resolve().then(function () {
                alert('Promise start')
                outer.innerHTML = 'Promise';
                alert('Promise end')
            });
        }
        outer.addEventListener('click', onClick);
    </script>
</html>
复制代码

我们在各类任务里增加了DOM渲染,在DOM渲染的前后增加了弹窗(debug模式下不一样的机制,不讨论),在Chrome(其他浏览器不一样)上执行,发现Promisehtml end打印出来后,inner里才有了Promise字样,任务结束后,字样改成了setTimeout。上面的结论在chrome是ok的。

参考文章:

并发模型与事件循环

8.1.4 Event loops Definitions

Tasks, microtasks, queues and schedules