Js事件循环(Event Loop)机制

前言

Event Loop是计算机系统的一种运行机制,是个很重要的概念。而Javascript用这种机制来解决单线程运行带来的问题。理解很熟悉将会有利于我们更容易理解Vue的异步事件。

JavaScript是单线程的

1、什么是单线程?

单线程在程序执行时,所走的程序路径按照连续顺序排下来,前面的必须处理好,后面的才会执行。简单来说,即同一时间只能做一件事件。

2、Js为什么是单线程?

Js是一种运行在网页的简单的脚本语言,由于设计的初衷是作为浏览器脚本语言,用于与用户互动,以及操作DOM。这决定它是单线程的。

3、单线程带来的问题?

单线程就意味着,所有任务都需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就需要一直等着。这就会导致IO操作(耗时但cpu闲置)时造成性能浪费的问题。

4、如何解决单线程的性能问题?

采用异步可以解决。主线程完全可以不管IO操作,暂时挂起处于等待中的任务,先运行排在后面的任务。等到IO操作返回了结果,再回过头,把挂起的任务继续执行下去。于是,所有任务可以分成两种,一种是同步任务,另一种是异步任务。

执行栈

Javascript代码执行的时候会将不同的变量存于内存中的不同位置:堆(heap)和栈(stack)中来加以区分。其中,堆里存放着一些对象。而栈中则存放着一些基础类型变量以及对象的指针。但是我们这里说的执行栈和上面这个栈的意义却有些不同。

js 在执行可执行的脚本时,会经过以下步骤:

  1. 首先会创建一个全局可执行上下文globalContext,每当执行到一个函数调用时都会创建一个可执行上下文(execution context)EC
  2. 可执行程序可能会存在很多函数调用,那么就会创建很多EC,所以 JavaScript 引擎创建了执行上下文栈(Execution context stack,ECS)来管理执行上下文。
  3. 当函数调用完成,Js会退出这个执行环境并把这个执行环境销毁,回到上一个方法的执行环境。 这个过程反复进行,直到执行栈中的代码全部执行完毕。

Js事件循环(Event Loop)机制_第1张图片
image

实例

function fun3() {
    console.log('fun3')
}
function fun2() {
    fun3();
}
function fun1() {
    fun2();
}
fun1();

当执行一个函数的时候,就会创建一个执行上下文,并且压入执行上下文栈,当函数执行完毕的时候,就会将函数的执行上下文从栈中弹出。知道了这样的工作原理,让我们来看看如何处理上面这段代码:

1.执行全局代码,创建全局执行上下文,全局上下文被压入执行上下文栈

ECStack = [
    globalContext
];
  1. 全局上下文初始化
   globalContext = {
        VO: [global],
        Scope: [globalContext.VO],
        this: globalContext.VO
    }
  1. 初始化的同时,fun1函数被创建,保存作用域链到函数的内部属性[[scope]]
 fun1.[[scope]] = [
      globalContext.VO
    ];
  1. 执行 fun1 函数,创建fun1函数执行上下文,fun1函数执行上下文被压入执行上下文栈
 ECStack = [
        fun1,
        globalContext
    ];
  1. fun1函数执行上下文初始化:

    1.复制函数 [[scope]] 属性创建作用域链。

    2.用 arguments 创建活动对象。

    3.初始化活动对象,即加入形参、函数声明、变量声明。

    4.将活动对象压入fun1 作用域链顶端。
    同时 f 函数被创建,保存作用域链到 f 函数的内部属性[[scope]]

  checkscopeContext = {
        AO: {
            arguments: {
                length: 0
            },
            scope: undefined,
            f: reference to function f(){}
        },
        Scope: [AO, globalContext.VO],
        this: undefined
    }
  1. 执行 fun2() 函数,重复步骤2。
  2. 最终形成这样的执行栈:
   ECStack = [
        fun3
        fun2,
        fun1,
        globalContext
    ];
  1. fun3执行完毕,从执行栈中弹出...一直到fun1

事件循环(Event Loop)

JavaScript内存模型

在了解事件循环之前,先要弄明白Js的内存模型,这有助于更好的理解事件循环。

  • 调用栈(Call Stack):用于主线程任务的执行。
  • 堆(Heap):用于存放非结构数据,如程序分配的变量和对象。
  • 任务队列(Queue): 用于存放异步任务。

Js异步执行的运行机制

  1. 所有同步任务都在主线程上执行,形成一个执行栈。
  2. 主线程之外,还存在一个任务队列。只要异步任务有了运行结果,就在任务队列之中放置一个事件。
  3. 一旦执行栈中的所有同步任务执行完毕,系统就会读取任务队列,看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
  4. 主线程不断重复上面的第三步。

任务

异步任务存放在任务队列里,异步任务分为 宏任务(macrotask)与微任务(microtask),不同的API注册的任务会依次进入自身对应的队列中,然后等待Event Loop将它们依次压入执行栈中执行。

宏任务主要包含:

  • script(整体代码)
  • setTimeout
  • setInterval
  • I/OUI交互事件
  • setImmediate(Node.js 环境)

微任务主要包含:

  • Promise
  • MutaionObserver
  • process.nextTick(Node.js 环境)

我们的JavaScript的执行过程是单线程的,所有的任务可以看做存放在两个队列中——执行队列和事件队列。

执行队列里面是所有同步代码的任务,事件队列里面是所有异步代码的宏任务,而我们的微任务,是处在两个队列之间。

JavaScript执行时,优先执行完所有同步代码,遇到对应的异步代码,就会根据其任务类型存到对应队列(宏任务放入事件队列,微任务放入执行队列之后,事件队列之前);当执行完同步代码之后,就会执行位于执行队列和事件队列之间的微任务,然后再执行事件队列中的宏任务。

实例

new Promise(resolve => {
    resolve(1);
    
    Promise.resolve().then(() => {
        // t2
        console.log(2)
    });
    console.log(4)
}).then(t => {
    // t1
    console.log(t)
});
console.log(3);

这段代码的流程大致如下:

  1. script 任务先运行。首先遇到Promise实例,构造函数首先执行,所以首先输出了 4。此时microtask 的任务有 t2t1
  2. script 任务继续运行,输出 3。至此,第一个宏任务执行完成。
  3. 执行所有的微任务,先后取出 t2t1,分别输出 21
  4. 代码执行完毕

综上,上述代码的输出是:4321

事件循环

主线程从任务队列中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)。

Js事件循环(Event Loop)机制_第2张图片
image

从上图我们可以看出:

  • 主线程运行的时候,产生堆(heap)和栈(stack)。
  • 栈中的代码调用各种外部API,它们在"任务队列"中加入各种事件(click,load,done)。
  • 栈中的代码执行完毕,主线程就会去读取任务队列,依次执行那些事件所对应的回调函数。

小结

事件循环其实并不难,多查阅资料,多看看相关例子就ok。希望一知半解的童鞋抓紧学习。

相关文章

  • JavaScript中的事件循环 Event Loop
  • 再谈Event Loop
  • 从一道题浅说 JavaScript 的事件循环
  • Event loop 机制简介

你可能感兴趣的:(Js事件循环(Event Loop)机制)