事件循环机制Event LOOP

事件循环是js执行的一个机制,理解了他就可以更好的理解js函数是如何执行的,对理解异步任务很有帮助

这类只是浏览器里面的event loop 在node.js中和浏览器中并一样。

 

先补充一些知识点

 

JavaScript执行上下文

简而言之,执行上下文是评估和执行 JavaScript 代码的环境的抽象概念。每当 Javascript 代码在运行的时候,它都是在执行上下文中运行。

JavaScript 中有三种执行上下文类型。

  • 全局执行上下文 — 这是默认或者说基础的上下文,任何不在函数内部的代码都在全局上下文中。它会执行两件事:创建一个全局的 window 对象(浏览器的情况下),并且设置 this 的值等于这个全局对象。一个程序中只会有一个全局执行上下文。
  • 函数执行上下文 — 每当一个函数被调用时, 都会为该函数创建一个新的上下文。每个函数都有它自己的执行上下文,不过是在函数被调用时创建的。函数上下文可以有任意多个。每当一个新的执行上下文被创建,它会按定义的顺序(将在后文讨论)执行一系列步骤。
  • Eval 函数执行上下文 — 执行在 eval 函数内部的代码也会有它属于自己的执行上下文,但由于 JavaScript 开发者并不经常使用 eval,所以在这里我不会讨论它
  1. 如何存储执行上下文:

执行上下文被存在一个数据栈中,这个栈保存着代码运行是创建的所有上下文,栈的特点是后进先出(LIFO),也就是说,每创建一个新的上下文,就会将它压入栈的顶部,当函数执行完毕时,上下文从栈中弹出,控制流程到达栈中的下一个上下文。当JavaScript引擎首次碰到脚本时,会创建一个全局的上下文,并将其压入栈中。

执行栈,也就是在其它编程语言中所说的“调用栈”,是一种拥有 LIFO(后进先出)数据结构的栈,被用来存储代码运行时创建的所有执行上下文。

1.每调用一个函数,会把这个函数的执行上下文添加到调用栈中执行

2.如果在调用栈中有执行函数 如果调用了其他函数(例:函数嵌套)那么新函数也会被添加到调用栈中并且立即执行

3.函数被执行完成后,会将其执行上下清除调用栈,然后继续执行调用栈中剩下的

 

示例:

let a = 'Hello World!';

function first() {
  console.log('Inside first function');
  second();
  console.log('Again inside first function');
}

function second() {
  console.log('Inside second function');
}

first();
console.log('Inside Global Execution Context');

 

 

 

当上述代码在浏览器加载时,JavaScript 引擎创建了一个全局执行上下文并把它压入当前执行栈。当遇到 first() 函数调用时,JavaScript 引擎为该函数创建一个新的执行上下文并把它压入当前执行栈的顶部。

当从 first() 函数内部调用 second() 函数时,JavaScript 引擎为 second() 函数创建了一个新的执行上下文并把它压入当前执行栈的顶部。当 second() 函数执行完毕,它的执行上下文会从当前栈弹出,并且控制流程到达下一个执行上下文,即 first() 函数的执行上下文。

first() 执行完毕,它的执行上下文从栈弹出,控制流程到达全局执行上下文。一旦所有代码执行完毕,JavaScript 引擎从当前栈中移除全局执行上下文。

 

那么在js中如何创建执行上下文

 

创建一个执行上下文分为两个阶段:创建阶段执行阶段

在创建阶段,会发生三件事:绑定this指向创建词法环境组件创建变量环境组件

 

异步任务和同步任务

在js中 所有的流程都是单线程运行的 运行的时候 是上往下执行的 从fun1 到 fun2 再到更多的fun3 fun4 这种按照顺序执行下去的一个时间内只能处理一个请求,也就是处理完了fun1 才可以处理fun2 不能fun

那么在一些情况下 比如 settimeout ajax这种有延时 js不可能要等待这个函数运行完再运行下一个 否则一个网页的交互会非常糟糕 卡的不行,没有人想停留在一个速度慢,交互反应迟钝的网站 但是又需要处理这些请求。

 

因此js把执行的任务分为了异步任务和同步任务

二者最大的区别就是同步任务发起调用之后,很快就可以得到响应结果

但是异步任务不同,例如请求一次网络请求,对方服务器需要时间响应同时还受到双方的网速的影响。

这样的差异造成了双方的执行机制有很大的不同:首先是同步任务就是从上到下按照代码的顺序和调用的顺序执行,任务执行完了之后就把其中的代码移出调用栈。

但是异步任务则不同,首先他依然会在调用栈中被调用,然后解释器会将其中响应的回调任务放着在一个任务队列中,直到同步任务全部结束之后,然后执行异步任务的队列,并且依次把已经完成异步任务的加入到调用栈中并执行。

 

就是异步任务不是直接进入任务队列的,等执行到异步函数(任务)的回调函数推入到任务队列中。

 

这里涉及到一个知识点

就是任务入队,什么任务进入任务队列?

在代码执行到浏览器中,浏览器会根据代码的实际内容调用不同的线程

对的,js确实是单线程的,但是浏览器不是。

这里举例子 这些也基本上都是异步

  • http异步网络请求线程:处理用户的get、post等请求,等返回结果后将回调函数推入到任务队列;
  • 定时触发器线程setIntervalsetTimeout等待时间结束后,会把执行函数推入任务队列中;
  • 浏览器事件处理线程:将click、mouse等UI交互事件发生后,将要执行的回调函数放入到事件队列中。

 

事件循环Event LOOP

我个人认为从这里开始就是和他有关的了这个机制的核心内容了。

个人习惯看图说话 

 

 

 

我们先简单的一下上图的一些名词

heap:堆

stack: 栈

callback queue :字面意义是回调队列 但我还是喜欢叫任务队列

上图就是一个函数是如何被执行的

其中我们的任务队列就是callback queue 里面的内容正是函数内部的各种异步任务他里面存放的是需要执行的任务 其符合的是“先进先出”的特点,也就是说要添加任务的话,添加到队列的尾部,要取出任务的话,从队列头部去取。

 

在这人任务队列中分为宏任务(macro-task微任务(micro-task

常见的宏任务:script(整体代码) ,setTimeout,setInterval,I/O UI交互事件,postMessage MessageChannel setImmediate(Node.js 环境)

常见的微任务:Promise.then ,Object.observe,MutaionObserver,process.nextTick(Node.js 环境)

 

二者就是事件循环机制的一部分了

事件循环的流程如下:

1.宏任务队列中,按照入队顺序,找到第一个执行的宏任务,放入调用栈,开始执行

2.执行完该宏任务下所有的同步任务之后,该宏任务被踢出之后,会清空所有的调用栈,然后按照微任务的入队顺序开始执行微任务,直到请空所有的微任务队列为止。

3.在清空微任务之后,一个事件循环结束,进入下一个事件循环,直到宏任务被清空为止

事件循环由宏任务和在执行宏任务期间产生的所有微任务组成。完成当下的宏任务后,会立刻执行所有在此期间入队的微任务。这种设计是为了给紧急任务一个插队的机会,否则新入队的任务永远被放在队尾。区分了微任务和宏任务后,本轮循环中的微任务实际上就是在插队,这样微任务中所做的状态修改,在下一轮事件循环中也能得到同步。

以下是一个流程图

下面通过一个更直观的例子来感受这个流程

setTimeout(() => {
        console.log('timeout callback1');
    }, 0);
    setTimeout(() => {
        console.log('timeout callback1');
    }, 0)
    Promise.resolve(1).then
    (
        value =>{
            console.log('Promise onResolve',value);
        }
    )
    Promise.resolve(2).then
    (
        value =>{
            console.log('Promise onResolve',value);
        }
    )

执行结果:

 

 

 

下方示例增加细节:

   setTimeout(() => {
        console.log('timeout callback1');
        Promise.resolve(1).then(
            value =>{
                console.log('成功了',value);
            }
        )
    }, 0);
    setTimeout(() => {
        console.log('timeout callback2');
    }, 0)
    Promise.resolve(1).then
    (
        value =>{
            console.log('Promise onResolve',value);
        }
    )
    Promise.resolve(2).then
    (
        value =>{
            console.log('Promise onResolve',value);
        }
    )


    function fn1()
    {
        console.log('fun1');
    }
    fn1()

document.getElementById('btn').onclick = function()
{
    console.log('执行了btn');
} 

运行结果:

 

 

由此我们可以知道运行的顺序

fn1(同步队列) =>promise1 promise2(主线程下的微任务) =>settimeout1 (第一个宏任务) =>成功的promise(第一个宏任务下的微任务)=>settimeout 2  

 

宏任务详情:

为了协调这些任务有条不紊地在主线程上执行,页面进程引入了消息队列和事件循环机制, 渲染进程内部会维护多个消息队列,比如(延迟执行队列和普通的消息队列)。然后主线程采用 一个 for 循环,不断地从这些任务队列中取出任务并执行任务。我们把这些消息队列中的任 务称为宏任务。


微任务详情:

任务就是一个需要异步执行的函数,执行时机是在主函数执行结束之后、当前宏任务结束 之前。

在创建 全局执行上下文的同时,V8 引擎也会在内部创建一个微任务队列。顾名思义,这个微任务 队列就是用来存放微任务的,因为在当前宏任务执行的过程中,有时候会产生多个微任务, 这时候就需要使用这个微任务队列来保存这些微任务了

 

注意点:

微任务和宏任务是绑定的,每个宏任务在执行时,会创建自己的微任务队列, 微任务的执行时长会影响到当前宏任务的时长。因此在一个宏任务中有10个微任务 每一个微任务都需要1000毫秒的时间,执行完后需要10000毫秒的,可以说把这个宏任务的时间延长了10000毫秒,所以在设置宏任务时候也需要控制时长。

async、await

ES7引入了async和await 这样使得promise的调用更加简单和方便 同时他们与这个事件循环机制也有很大的关系

 

我们知道隐式返回 Promise 作为结果的函数,那么可以简单理解为,await后面的函数执行完毕时,await会产生一个微任务

它是执行完await之后,直接跳出async函数,执行其他代码

其他代码执行完毕后,再回到async函数去执行剩下的代码,然后把await后面的代码注册到微任务队列当中

下面用一个具体的例子来说明

 

      console.log('script start')

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

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

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

console.log('script end')

运行结果:

// 旧版输出如下,但是请继续看完本文下面的注意那里,新版有改动script start => async2 end => Promise => script end => promise1 => promise2 => async1 end => setTimeout

新版输出结果为下图

 

 

这里讲解旧版的运行方式:

  • 执行代码,输出script start。
  • 执行async1(),会调用async2(),然后输出async2 end,此时将会保留async1函数的上下文,然后跳出async1函数。
  • 遇到setTimeout,产生一个宏任务
  • 执行Promise,输出Promise。遇到then,产生第一个微任务
  • 继续执行代码,输出script end
  • 代码逻辑执行完毕(当前宏任务执行完毕),开始执行当前宏任务产生的微任务队列,输出promise1,该微任务遇到then,产生一个新的微任务
  • 执行产生的微任务,输出promise2,当前微任务队列执行完毕。执行权回到async1
  • 执行await,实际上会产生一个promise返回,执行完成,执行await后面的语句,输出最后,执行下一个宏任务,即执行setTimeout,输出setTimeout

 

OK以上就是自己对于事件处理机制Event Loop的理解 

posted @ 2022-07-23 11:30  jeffmmo  阅读(415)  评论(0)    收藏  举报