当前位置:主页 > 网页前端 > JavaScript代码 >

实例详解JS中的事件循环机制

时间:2023-02-18 10:17:09 | 栏目:JavaScript代码 | 点击:

一、前言

之前我们把react相关钩子函数大致介绍了一遍,这一系列完结之后我莫名感到空虚,不知道接下来应该更新有关哪方面的文章。最近想了想,打算先回归一遍JS基础,把一些比较重要的基础知识点回顾一下,然后继续撸框架(可能是源码、也可能补全下全家桶)。不积跬步无以至千里,万丈高楼咱们先从JS的事件循环机制开始吧,废话不多说,开搞开搞!

在JS中,我们所有的任务可以分为同步任务和异步任务。那么什么是同步任务?什么又是异步任务呢?

同步任务:是在主线程执行栈上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;比如:console.log、赋值语句等。

异步任务:不进入主线程,是进入任务队列的任务,只有等主线程任务执行完毕,"任务队列"开始通知主线程,请求执行任务,该任务才会进入主线程执行。比如:ajax网络请求,setTimeout 定时函数等都属于异步任务,异步任务会通过任务队列的机制(先进先出的机制)来进行协调。

我们执行一段代码时,在我们主线程的执行栈执行过程中,如果遇到同步任务会立即执行,如果遇到异步任务会暂时挂起,将此异步任务推入任务队列中(队列的执行机制遵循先进先出)。当主线程执行栈里的同步任务执行完毕后,js执行引擎会去任务队列中读取挂起的异步任务并将其推入到执行栈中执行。这个不断重复的过程(执行栈执行--->判断同异步--->同步执行/异步挂起推入事件对列--->栈空后取事件队列里任务并推入执行栈执行--->继续判断同异步--->.......)就是本文所要介绍的事件循环。

二、宏、微任务

我们每进行一次事件循环的操作被称之为tick,在介绍一次 tick 的执行步骤之前,我们需要补充两个概念:宏任务、微任务。

宏任务和微任务严格来说是ES6之后才有的概念(原因在于ES6提出了Promise这个概念);在Es6之后我们把JS的任务更细分成了宏任务和微任务。

其中,宏任务主要包括:script(整体代码)、setTimeout、setInterval、I/O、UI交互事件、postMessage、requestAnimationFrame(帧动画)、MessageChannel、setImmediate(Node.js环境);

微任务主要包括:Promise.then、MutaionObserver、process.nextTick(Node.js环境);

好了,了解了宏微任务的概念之后我们就来掰扯掰扯每次tick的执行顺序吧。首先看下图:

三、Tick 执行顺序

1、首先执行一个宏任务(栈中没有就从事件队列中获取);

2、执行过程中如果遇到微任务,就将它添加到微任务的任务队列中、如果有宏任务的话推到相应的事件队列中去;

3、宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行);

4、当前宏任务执行完毕,开始进行渲染;5、开始下一个宏任务(从事件队列中获取)开启下一次的tick;

需要注意的是:宏任务执行过程中如果宏任务中又添加了一个新的宏任务到任务队列中。 这个新的宏任务会等到下一次事件循环再执行;而微任务则不同,微任务执行过程中如果又添加了新的微任务,则新的微任务也会在本次微任务执行过程中被执行,直到微任务队列为空。每次宏任务执行完在开启下一次宏任务时会把微任务队列中所有的微任务执行完毕!

四、案例详解

概念性的东西说完了,下面就来找些demo练练手吧!

1.掺杂setTimeout

console.log('开始');

setTimeout(()=>{
    console.log('同级的定时器');
      setTimeout(() => {
          console.log('内层的定时器');
      }, 0);
},0)

console.log('结束');

输出结果为

开始 -> 结束 -> 同级的定时器 ->内层的定时器

解释上述代码:

2.掺杂微任务,此处主要是Promise.then

console.log('script start');

setTimeout(function() {
  new Promise(resolve=>{
        console.log('000');
      resolve()
    }).then(res=>{
        console.log('这是微任务');
    })
  console.log('timeout1');
}, 10);

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

console.log('script end');

输出结果为:

script start -> promise1 -> script end -> then1 -> 000 -> timeout1 -> 这是微任务 -> timeout2

解释上述代码:

好了,相信经过这两个例子,小伙伴们对事件循环有了初步的认识。接下来我们再顽皮一下:对上面这个demo做一丢丢微调

微调一 : 其他地方不变,then里塞定时器

setTimeout(function() {
  new Promise(resolve=>{
        console.log('000');
      resolve()
    }).then(res=>{
         setTimeout(()=>{
         console.log('这次的执行顺序呢?') -----> 如果这里再塞个定时器呢?执行顺序是什么?
        },10)
        console.log('这是微任务');
    })
  console.log('timeout1');
}, 10);

微调二:其他地方不变,对Promise进行链式调用

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

此Promise进行链式调用,其他地方不动,此时的执行顺序是什么?

提示:在一次tick结束时,此tick内微任务队列中的微任务一定会执行完并清空,如果在执行过程中又产生了微任务,那么同样会在此tick过程中执行完毕;而宏任务的执行则可以看成是下一次tick的开始。

3.掺杂async/await

在进行demo解析之前,我们需要补充一下async/await的相关知识点。

async

async相当于隐式返回Promise:当我们在函数前使用async的时候,使得该函数返回的是一个Promise对象,async的函数会在这里帮我们隐式使用Promise.resolve();

下面看个小demo来理解下async函数是怎么隐式转换的:

async function test() {
    console.log('这是async函数')
    return '测试隐式转换' 
}

上面这个async就相当于如下代码:

function test(){
    return new Promise(function(resolve) {
      console.log('这是async函数')
       resolve('测试隐式转换')
   })
}

await

await表示等待,是右侧表达式的结果,这个表达式的计算结果可以是 Promise 对象的值或者一个函数的值(换句话说,就是没有特殊限定)。并且await只能在带有async的内部使用;使用await时,会从右往左执行,当遇到await时,会阻塞函数内部处于它后面的代码,去执行该函数外部的 代码 当外部代码执行完毕,再回到该函数内部执行await后面剩余的代码

好了,补充完前置知识我们来做个demo助助兴:

掺杂async/await的事件循环

async function async2() {				
    console.log('async2');     
}

async function async1() {
    console.log('async1 start');
    await async2();
    console.log('async1 end');
}

console.log('script start');

setTimeout(function () {
    console.log('setTimeout');
}, 0);

async1();

new Promise((resolve) => {
    resolve()
    console.log('promise1');
}).then(function () {
    console.log('promise2');
});

console.log('script end');

输出顺序为

script start --> async1 start --> async2 --> promise1 --> script end --> async1 end --> promise2 --> setTimeout  

首先为方便理解我们先将async函数转为return Promise的那种形式:

①:
async function async2() {				
    console.log('async2');     
}
转换后如下:
function async2() {			
    return  new Promise(resolve=>{
       console.log('async2');     
    })
}


②:
async function async1() {
    console.log('async1 start');
    await async2();
    console.log('async1 end');
}
转换后如下:
function async1() {
  return new Promise(resolve=>{
    console.log('async1 start');
    #执行async2,并且会阻塞其后面的代码
    console.log('async1 end');
  })
}

所以,最后我们包含async函数的代码块就相当于如下代码:

function async2() {			
    return  new Promise(resolve=>{
       console.log('async2');     
    })
}

function async1() {
  return new Promise(resolve=>{
    console.log('async1 start');
    #执行async2,并且会阻塞其后面的代码,在此处是阻塞了console.log('async1 end')的执行
    console.log('async1 end');
  })
}
=============上面为声明部分===========
console.log('script start');

setTimeout(function () {
    console.log('setTimeout');
}, 0);

new Promise(resolve=>{
    console.log('async1 start');
    #执行async2,并且会阻塞其后面的代码,在此处是阻塞了console.log(async1end)的执行;这里相当于awaitasync2()
   
    console.log('async1 end');
  })
}

new Promise((resolve) => {
    resolve()
    console.log('promise1');
}).then(function () {
    console.log('promise2');
});

console.log('script end');

经过一系列骚操作之后,我们终于可以来分析这个代码块的执行顺序了,废话不多说,开冲。

解释上述代码:

您可能感兴趣的文章:

相关文章