摸清同步、异步和事件循环(Event Loop),面试不用慌

129 阅读6分钟

CPU、进程、线程

  • CPU:计算机的核心,承担了所有的计算任务;
  • 进程:进程就好比工厂的车间,它代表CPU所能处理的单个任务,CPU使用时间片轮转进度算法来实现同时运行多个进程;
  • 线程:线程就好比车间里的工人,一个进程可以包括多个线程,多个线程共享进程资源;

浏览器是多进程的,其中包含了主进程、第三方插件进程、GPU进程、渲染进程,其中有渲染进程,也就是我们说的浏览器内核;

浏览器内核又包含多条线程

GUI渲染线程:

  • 负责渲染浏览器界面,解析HTML,CSS,构建DOM树和RenderObject树,布局和绘制等
  • 页面需要重绘和回流时,该线程就会执行
  • 与JS引擎线程互斥,防止渲染结果不可预期

JS引擎线程:

  • 负责处理解析和执行Javascript脚本程序
  • 只有一个JS引擎线程(单线程)
  • 与GUI渲染线程互斥,防止渲染结果不可预期

事件触发线程:

  • 用来控制事件循环(鼠标点击、setTimeout、ajax等)
  • 当事件满足触发条件时,将事件放入到JS引擎所在的执行队列中

定时触发器线程:

  • setInterval与setTimeout所在的线程
  • 定时任务并不是由JS引擎计时的,是由定时触发线程来计时的
  • 计时完毕后,通知事件触发线程

异步http请求线程:

  • 浏览器有一个单独的线程用于处理AJAX请求
  • 当请求完成时,若有回调函数,通知事件触发线程

所以我们常说的 “Javascript是单线程的” 指的是 JS引擎中解析和执行Javascript的是线程只有一个,而 浏览器并不是单线程的

同步和异步

通过上面的讲解我们可以把JS引擎中解析和执行Javascript的线程叫做 主线程

同步还是异步指的是运行环境提供的API是以同步或异步模式的方式去工作,而所有的任务又可以分为同步任务和异步任务;

同步模式: 代码当中的任务在 主线程 上依次执行,执行顺序与代码的编写顺序完全一致,在单线程的情况下,大多数任务都会以同步模式的方式去执行。

console.log(1)

function b() {
    console.log(3)
}

function a() {
    console.log(2)
    b()
}

a()

console.log(4)

// 1 2 3 4

异步模式: 主线程依次执行同步任务,遇到异步任务不会去等待这个任务的结束才开始下一个任务,开启过后就立即往后执行下一个任务,异步任务在别的线程执行完成后将回调放入一个消息队列,然后主线程的任务执行完成再依次拿出消息队列中的回调执行。

console.log(1)

setTimeout(function time1() {
    console.log(4)
}, 1800)

setTimeout(function time2() {
    console.log(3)
    setTimeout(function inner () {
        console.log(5)
    }, 1000)
}, 1000)

console.log(2)

// 1 2 3 4 5

image.png

事件循环(Event Loop)

  • 同步任务:立即执行的任务,在主线程上排队执行,前一个任务执行完毕,才能执行后一个任务;
  • 异步任务:异步执行的任务,不在主线程执行,而是在其辅助线程上执行,执行完成将产生的回调函数放入任务队列中等待主线程空闲的时候读取执行。

事件循环是一种运行机制。JS引擎执行JavaScript代码时,任务会分为同步任务和异步任务,同步任务按顺序被立即放入执行栈等待主线程执行,异步任务会在任务执行结束后,将产生的回调函数放入任务队列中,当主线程空闲(即执行栈清空)时,任务队列中的回调函数依次被放入执行栈等待主线程执行。

image.png

image.png

  • 同步和异步任务分别进入不同的执行环境,同步任务进入主线程依次执行,异步任务在对应辅助线程执行
  • 异步任务执行完成时,将产生的回调函数依次放入任务队列
  • 当主线程上同步任务执行完后,监听任务队列,依次读取其中的回调函数进行执行
  • 重复上述过程,就是常说的 事件循环(Event Loop)

除了广义的同步任务和异步任务,又可以分为宏任务和微任务,它们属于异步任务。

  • 宏任务:macrotask,又称为task, 可以理解为每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行)。
  • 微任务:microtask, 又称为job, 可以理解是在当前 task 执行结束后立即执行的任务。
  • 常见宏任务:script( 整体代码)setTimeoutsetIntervalsetImmediate,I/O(Node.js)
  • 常见微任务:Promise.thenAsync/Awaitprocess.nextTick(Node.js)

两者的执行流程看下面的两张图:

image.png

image.png

解读:首先当前的宏任务执行完成后,查看当前宏任务是否产生微任务(即查看微任务队列是否为空),如果不为空,就按照产生的顺序依次执行所有的微任务(先入先执行),当微任务队列清空后,然后再执行下一个宏任务,然后循环往复。

生活举例说明: 我们去银行办理业务,每个人预约一个办卡业务需求排队等待办理,每个人预约的一个办卡业务需求看作宏任务。整个排队看作宏任务队列。 当我办理业务,也就是执行宏任务,当我预约的办卡业务办理结束时,我突然想查转个账,取点钱,这是另业务需求了,每一个新加的业务需求看作微任务,所有新的业务需求看作微任务队列。 但我新加的几个业务需求完成后,才轮到下一位办理业务,也就是当前宏任务后所有的微任务执行完,才执行下一个宏任务

练习题

console.log('script start');

setTimeout(() => {
  console.log('timeout1');
},0);

new Promise(resolve => {
    console.log('promise1');
    resolve();
    setTimeout(() => {
      console.log('timeout2')
    },0);
}).then(function() {
    console.log('then1')
})

console.log('script end');

// script start  promise1  script end  then1  timeout1  timeout2

解读:首先遇到了console.log,同步任务直接输出 script start;接着往下走,遇到 setTimeout 宏任务,后放入宏任务队列记为 timeout1 ;接着遇到 promise,new promise 中的代码立即执行,输出 promise1 ,然后执行 resolve ,遇到 setTimeout 宏任务,放入宏任务队列记为 timemout2 ,将其 then 放入微任务队列记为 then1 ;接着遇到 console.log 代码,直接输出 script end 接着检查微任务队列,发现有个 then1 微任务,执行,输出then1;再检查微任务队列,发现已经清空,则开始检查宏任务队列,执行 timeout1 ,输出 timeout1 ; 接着执行 timeout2 ,输出 timeout2 至此,所有的都队列都已清空,执行完毕。

console.log( '1' );
async function async1() {
    console.log( '2' );
    await async2();
    console.log( '3' );
}
async function async2() {
    console.log( '4' );
}
setTimeout( function () {
    console.log( '5' );
    new Promise( function ( resolve ) {
        console.log( '6' );
        resolve();
    } ).then( function () {
        console.log( '7' )
    } )
} )
async1();
new Promise( function ( resolve ) {
    console.log( '8' );
    resolve();
} ).then( function () {
    console.log( '9' );
} );
console.log( '10' );   

// 1  2  4  8  10  3  9  5  6  7

参考资料

  1. juejin.cn/post/684490…
  2. juejin.cn/post/684490…