什么是浏览器的Event Loop?,线程,循环特质

Event Loop是什么

什么是浏览器的Event Loop?,线程,循环特质

因为js设计之初,多线程的执行模式还不流行,所以一直以为,js都是单线程执行的。但是js拥有异步执行的能力,这依赖于事件循环(Event Loop)的执行模式。我们将通过js在浏览器中的执行来研究一下该模式。

其中涉及到一些概念,我们先简单研究一下,以便后续更好地了解。

进程和线程

参考阮一峰的解释,将整个CPU比喻为一座工厂,进程就是其中的车间,车间中的需要完成的工序就是线程。一个工厂可以有多个车间,每个车间有一个或者多个工序,但是必须 按照顺序执行,这就是单线程的概念。也是浏览器事件执行的基础。

浏览器渲染过程

浏览器是一个多进程应用,每一个窗口就是一个进程,其中包含以下线程:

  • GUI渲染线程

负责渲染页面,布局和绘制

页面需要重绘和回流时,该线程就会执行

与js引擎线程互斥,防止渲染结果不可预期

  • JS引擎线程

负责处理解析和执行javascript脚本程序

只有一个JS引擎线程(单线程)

与GUI渲染线程互斥,防止渲染结果不可预期

  • 事件触发线程

用来控制事件循环(鼠标点击、setTimeout、ajax等)

当事件满足触发条件时,将事件放入到JS引擎所在的执行队列中

  • 定时触发器线程

setInterval与setTimeout所在的线程

定时任务并不是由JS引擎计时的,是由定时触发线程来计时的

计时完毕后,通知事件触发线程

  • 异步http请求线程

浏览器有一个单独的线程用于处理AJAX请求

当请求完成时,若有回调函数,通知事件触发线程

各个进程之间的关系

  • 同步任务都在js引擎线程上完成,当前的任务都存储在执行栈中;
  • js引擎线程执行到setTimeout/setInterval的时候,通知定时触发器线程,间隔一定时间,触发回调函数;
  • 定时触发器线程在接收到这个消息后,会在等待的时间后,将回调事件放入到由事件触发线程所管理的事件队列(事件队列分为宏任务队列微任务队列)中;
  • js引擎线程执行到XHR/fetch时,通知 异步http请求线程,发送一个网络请求;
  • 异步http请求线程在请求成功后,将回调事件放入到由事件触发线程事件队列中;
  • 如果JS引擎线程中的执行栈没有任务了,JS引擎线程会询问事件触发线程,在 事件队列中是否有待执行的回调函数,如果有就会加入到执行栈中交给JS引擎线程执行;
  • JS引擎线程空闲之后,GUI渲染线程开始工作
什么是浏览器的Event Loop?,线程,循环特质

总结:

  • JS 是可以操作 DOM 的, 因此浏览器设定 GUI渲染线程和 JS引擎线程为互斥关系;
  • setTimeout/setInterval和 XHR/fetch代码执行时, 本身是同步任务,而其中的回调函数才是异步任务
  • JS引擎线程只执行执行栈中的事件
  • 执行栈中的代码执行完毕,就会读取事件队列中的事件
  • 事件队列中的回调事件,是由各自线程插入到事件队列中的
  • 如此循环

js如何异步执行

了解了浏览器多线程之间的关联之后,我们开始探究,js是如何依赖Event Loop,进行异步操作的。

执行栈和事件队列

在分析多线程之间的关系时,我们提到了两个概念,执行栈执行队列

执行栈

栈,是一种数据结构,具有先进后出的原则。JS 中的执行栈就具有这样的结构,当引擎第一次遇到 JS 代码时,会产生一个全局执行上下文并压入执行栈,每遇到一个函数调用,就会往栈中压入一个新的上下文。引擎执行栈顶的函数,执行完毕,弹出当前执行上下文

事件队列

事件队列是一个存储着 异步任务 的队列,按照先进先出的原则执行。事件队列每次仅执行一个任务。当执行栈为空时,JS 引擎便检查事件队列,如果事件队列不为空的话,事件队列便将第一个任务压入执行栈中运行。

宏任务和微任务

异步任务又分为宏任务跟微任务、他们之间的区别主要是执行顺序的不同。

宏任务(macrotask)

也叫tasks,一些异步任务的回调会依次进入macro task queue,等待后续被调用,这些异步任务包括:

  • 包括整体代码script
  • setTimeout
  • setInterval
  • requestAnimationFrame

微任务(microtask)

也叫jobs,另一些异步任务的回调会依次进入micro task queue,等待后续被调用,这些异步任务包括:

  • Promise
  • MutationObserver
什么是浏览器的Event Loop?,线程,循环特质

理解Event Loop

示例

下面上一道很经典的题目:

 console.log('1');
setTimeout(()=>{
console.log('2');
},100);
setTimeout(()=>{
console.log('3');
},0);
console.log('4');

没有研究event loop之前,答案很可能以为是1 3 4 2,但是实际答案是1 4 3 2。其中的原理下面来分析一下。

异步任务执行的时候,有这样一个顺序:

  1. 执行全局Script代码,如果碰到异步任务,将该任务放入微任务队列中
  2. 全局Script执行完,执行栈清空
  3. 从微队列microtask queue中取出位于队首的回调任务,放入调用栈Stack中执行
  4. 一个微任务执行完毕之后,再从微任务队列中取出一个任务放入执行栈执行,若微任务中还有微任务,则放入当前微任务队列末尾
  5. 微任务队列为空,执行栈也为空,此时从宏任务队列取出一个任务执行,如果其中有微任务,放入微任务队列
  6. 重复执行3-5步骤...
  7. 重复执行3-5步骤...

*注:

  1. 宏任务队列一次只从队列中取一个任务执行,执行完后就去执行微任务队列中的任务;
  2. 微任务队列中所有的任务都会被依次取出来执行,直到队列为空;
  3. GUI渲染线程在微任务执行完,执行栈为空,下一个宏任务执行之前执行,。

以上就是浏览器事件循环——event loop。

理解了异步任务的执行顺序之后,再来回顾上面这道题:

 console.log('1');
setTimeout(()=>{
console.log('2');
},100);
setTimeout(()=>{
console.log('3');
},0);
console.log('4');
  1. 执行整个script,console.log('1')是同步任务,setTimeout是宏任务,js引擎线程通知事件触发线程
    ,在定时n秒后存入宏任务队列中,所以先存入console.log('3'),后存入console.log('2');;
  2. 执行下一个同步任务console.log('4');;
  3. 执行栈为空,查询微任务队列也为空,查询宏任务队列
  4. 根据先进先出原则,执行console.log('3');,后执行console.log('2');
  5. 输出1 4 3 2;

实践

再来2道题巩固一下:

一.

 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');
  1. 第一个setTimeout放到宏任务队列,此时宏任务队列为['A'];
  2. 接着执行obj的func方法,将setTimeout放到宏任务队列,此时宏任务队列为['A', 'B']
  3. 函数返回一个Promise,因为这是一个同步操作,所以先打印出'C';
  4. 接着将then放到微任务队列,此时微任务队列为 ['D'];
  5. 接着执行同步任务console.log('E');,打印出 'E';
  6. 因为微任务优先执行,所以先输出 'D';
  7. 最后依次输出 ['A', 'B'];
  8. 输出结果:C E D A B

二.

 async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2');
}
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
async1();
new Promise(function(resolve) {
console.log('promise1');
resolve();
}).then(function() {
console.log('promise2');
});
console.log('script end');

await前面的代码是同步的,调用此函数时会直接执行;而await a(); 这句可以被转换成 Promise.resolve(a()); await 后面的代码 则会被放到 Promise.then() 方法里。因此上面的代码可以被转换成如下形式:

 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
});
  1. 首先打印出script start
  2. 接着将settimeout添加到宏任务队列,此时宏任务队列为['settimeout']
  3. 然后执行函数async1,先打印出async1 start,又因为Promise.resolve(async2()) 是同步任务,所以打印出async2,接着将async1 end 添加到微任务队列,此时微任务队列为['async1 end']
  4. 接着打印出promise1,将promise2 添加到微任务队列,此时微任务队列为['async1 end', promise2]
  5. 打印出script end
  6. 因为微任务优先级高于宏任务,所以先依次打印出 async1 end和promise2
  7. 最后打印出宏任务settimeout

我自己是一名从事了多年的前端老程序员,小编为大家准备了新出的前端编程学习资料,免费分享给大家!

如果你也想学习前端,那么帮忙转发一下然后再关注小编后私信【1】可以得到我整理的这些前端资料了(私信方法:点击我头像进我主页有个上面有个私信按钮)


分享到:


相關文章: