• JavaScript 的核心机制——event loop(最易懂版)


    前言

    javascript从诞生之日起就是一门单线程的非阻塞的脚本语言。

    非阻塞就是当代码需要进行一项异步任务(无法立刻返回结果,需要花一定时间才能返回的任务,如ajax事件)时,主线程会挂起(pending)这个任务,然后在异步任务返回结果的时候再去执行相应的回调。

    javascript引擎到底是如何实现的这一点呢?

    1.执行栈

    存放当前正在执行的代码的地方被称为执行栈

    当我们调用一个方法的时候,js会生成一个与这个方法对应的执行环境(context),又叫执行上下文。这个context存放着这个方法的私有作用域,上层作用域的指向,方法的参数,这个作用域中定义的变量以及这个作用域的this对象。

    1. 当一个脚本第一次执行的时候,js引擎会解析这段代码,并将其中的同步代码按照执行顺序加入栈中,然后从头开始执行。
    2. 如果当前执行的是一个方法,那么js会向执行栈中添加这个方法的context,然后进入这个context继续执行其中的代码。
    3. 当这个context中的代码 执行完毕并返回结果后,js会退出这个context并把这个context销毁,回到上一个方法的context。(函数的相互调用,就像一个栈!)
    4. 这个过程反复进行,直到执行栈中的代码全部执行完毕。

    这个过程可以是无限进行下去的,除非发生了栈溢出,即超过了所能使用内存的最大值。

    2.事件循环队列

    1. 执行栈遇到一个异步任务后(如ajax请求)并不会一直等待其执行完毕,而是继续执行 栈中的后续任务,这就是js的异步非阻塞特性。
    2. 当一个异步事件完成后,js会将这个事件的回调任务放入事件队列中,而不会影响当前的执行栈
    3. 当前执行栈中的所有任务都执行完毕时,事件队列中如果有任务的话,主线程会从中取出最优先的任务,放入执行栈中执行。
    4. 如此反复,每次执行栈执行完毕,就去任务队列中查找,这样就形成了一个无限的循环;这就是所谓的 event loop 。
    5. 注意:当所有任务都执行完了(事件队列中也没有任务了),js的运行也不会停止。js宿主(浏览器)每一帧都会执行一次上述的过程,从而保证异步回调会快速响应。可以参见浏览器的requestIdleCallback 或 requestAnimationFrame !

    这就是“事件循环(Event Loop)”

    经典示例:

    setTimeout(() => {
        console.log('settimeout')
    }, 0)
    let start = Date.now();
    while (Date.now() - start < 2000);
    console.log('finished');
    /**
     * 运行上面代码,并理解这个过程:
     * 执行栈中同步任务先运行2s的循环,然后输出 finished
     * 然后取出异步任务执行输出settimeout
    */

    3.异步任务的顺序

     不同的异步任务是放在不同的队列里的,当多个队列同时存在待处理的任务时,异步任务之间的执行优先级也有区别!

    总体有两种异步任务—— 微观任务(micro task)和宏观任务(macro task)。微观任务总是先于宏观任务被执行!

    微观任务是 js引擎自己发出的异步任务,如Promise。  es2015之前,js引擎是无法自己创建异步任务的,所以微观任务是es2015才开始有的概念。

    宏观任务是 宿主发起的异步任务,如setTimeout,XMLHttpRequest等 都是宿主window上的对象。

    回到上一节,事件循环的第3点:主线程会去查找事件队列是否有任务! 是先取微观任务队列中的任务,直到微观任务队列为空,再找宏观任务队列

    经典示例:

    setTimeout(() => {
        console.log('settimeout') //宏观异步任务,最后输出
    }, 0)
    Promise.resolve().then(()=>{
        console.log('promise')  //微观异步任务,第二个输出
    })
    console.log('finished')  // 同步的,最先输出

    异步任务优先级 总体上是这样的顺序,如果要更细节的优先级呢?

    微观任务优先级

    微观任务目前只有一种,就是Promise创建的,而且微观任务队列只有一个,再加上js是单线程的,即便有多种也很好理解。

    Promise.resolve().then(() => {
        Promise.resolve().then(() => {
            Promise.resolve().then(() => {
                console.log('1-1-1')
            });
            console.log('1-1')
        });
        console.log('1')
    });
    Promise.resolve().then(() => {
        Promise.resolve().then(() => {        
            console.log('2-1')
        });
        console.log('2')
    });
    // 依次输出: 1  2  1-1  2-1  1-1-1

    宏观任务优先级

    宏观任务有多种,而且是宿主发起(通常是多线程)又该如何确定顺序呢?

    setTimeout(() => {
        console.log('timmer')
    }, 0);
    let start = Date.now();
    while (Date.now() - start < 5000);
    
    function clickDiv(){ //点击按钮时触发
        console.log('click')
    }

    上面代码很有意思,主线程的同步代码执行需要5s,如果用户在第2s点击了按钮,那么5s后 是先输出 click,还是先输出 timmer 呢?

    timmer事件和UI事件,是不同的事件队列,尽管他们都是宏观任务。他们的优先级交给浏览器来决定的 ——  具体宏观任务的优先级,是由具体宿主自己决定的!

    所以这个问题请参考具体的宿主资料吧(nodejs 和 浏览器)

  • 相关阅读:
    Salesforce的数据权限机制
    Java并发编程:Java内存模型和volatile
    Java并发编程:synchronized和锁优化
    权限控制和OAuth
    MySQL explain详解
    ConcurrentHashMap源码阅读
    HashMap源码阅读
    领域驱动设计的基础知识总结
    Chris Richardson微服务翻译:重构单体服务为微服务
    Chris Richardson微服务翻译:微服务部署
  • 原文地址:https://www.cnblogs.com/zhwc-5w4/p/11568583.html
Copyright © 2020-2023  润新知