Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Js事件循环(Event Loop)机制 #2

Open
youdeliang opened this issue Nov 23, 2019 · 0 comments
Open

Js事件循环(Event Loop)机制 #2

youdeliang opened this issue Nov 23, 2019 · 0 comments

Comments

@youdeliang
Copy link
Owner

前言

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会退出这个执行环境并把这个执行环境销毁,回到上一个方法的执行环境。 这个过程反复进行,直到执行栈中的代码全部执行完毕。


实例

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(事件循环)。

从上图我们可以看出:

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

小结

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

相关文章

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant