美文网首页
JavaScript的运行机制及宏任务和微任务

JavaScript的运行机制及宏任务和微任务

作者: Greatiga | 来源:发表于2020-08-18 23:16 被阅读0次

    原文链接

    一篇文章搞懂浏览器Js事件循环机制(推荐阅读!)

    前言

    在初次入门学习和使用 JavaScript 的过程中,相信遇到过许多程序执行顺序及结果与预期不一致的问题,在查阅资料的过程中了解到原来是程序的执行有同步与异步之分;与此同时也会看到许多有关概念,例如回调函数、执行栈、任务队列、事件循环机制(Event Loop)、宏任务、微任务、Promise(ES6)等等。此时对于一个刚入门不久的小白来说,要理解消化这些概念真的不容易。对于入门不久的我来说也一样,所以写一篇博客记录一下,有关 JavaScript 的运行机制,以及上述的这些概念为什么会出现,又解决了什么问题。

    一、JavaScript 是单线程

    我们知道多线程是可以并行执行程序的,能提高程序运行效率。但是 JS 是一门单线程语言,同一时间内做一件事。

    最初作为服务于浏览器的脚本语言,很多时候都是在与用户交互,这个过程涉及了许多 DOM 的操作,倘若使用多线程,那么就容易出现几个线程同时操作一个 DOM 的问题,那么浏览器此时要以哪一个线程为主呢?这样一来无疑增加了复杂性,所以 JS 成为了单线程。虽然说多线程处理起来也很高效,但对于当时直接服务于浏览器用户的 JS 来说,尽可能避免过度复杂,能更简单的处理相对好点吧。

    二、异步任务及其回调函数

    虽然单线程降低了复杂性,但是也有了新的问题。单线程是顺序执行程序,每一个任务要等待上一个任务执行完毕才执行,如果遇到执行时间太长或者出现了别的问题,那么就会一直卡在那,导致整个程序无法顺利执行完毕。为了解决问题,语言设计者希望在程序执行时,将一些耗时、有延迟的任务先挂起,让能快速执行完毕的任务先执行;按照这样的方式执行完整个程序后,在返回去执行那些被挂起的任务。因此有了同步任务与异步任务之分;在执行过程中,当前执行程序的线程称为主线程,同步任务直接在主线程立即执行,而那些异步任务,先给它挂在一边放着,等到主线程执行完了所有同步任务,再回来读取挂在一旁的异步任务,并且执行他们。

    (1) 任务队列

    任务队列是一系列事件组成的一个队列,也就是上面说到的异步任务挂起的地方。程序执行时会将定义的异步任务送入任务队列,或者用户点击鼠标触发的异步任务送入队列。等待主线程来执行它们。例如常见的各种事件(鼠标点击、键盘敲击、滚动等等)、又或者是 Ajax 那样等待响应的异步任务。

    实际上,任务队列不止一种,因为处理的异步任务种类可能不同

    (2) 回调函数 (callback)

    回调函数往往就是异步任务所定义的代码。主线程执行完同步任务,就会回来开始读取任务队列中的异步任务并执行这些代码,同时也称为回调函数。

    (3) 宏任务和微任务

    异步任务又可以看为两种,通常由宿主环境(浏览器、node)提供的为宏任务,由语言标准提供的为微任务。 JavaScript 可能会在不同的宿主环境下运行,所以宏任务来自于宿主环境,而微任务作为语言标准,在任何环境下都可以使用。

    常见宏任务

    • setTimeout
    • setInterval
    • setImmediate (仅 node 提供)
    • requestAnimationFrame (仅浏览器提供)
    • 各种交互 (鼠标点击、滚动等等)
    • I/O

    常见微任务

    • Promise.then catch finally
    • MutationObserver (仅浏览器提供)
    • process.nextTick (仅 node 提供)

    三、事件循环机制 (Event Loop)

    主线程执行程序时会将定义的异步任务放入任务队列中,宏任务会放在宏任务队列,微任务放在微任务队列,当触发 UI 事件时,也会把相应任务放入队列。为了确保事件处理正常进行,主线程不阻塞。所以有了解决方案 Event Loop,事件循环线程是独立于主线程的,并且一直存在直到整个脚本环境被关闭。无论是主线程执行时添加的异步任务,还是 UI 交互触发后添加的异步任务,事件循环机制都会按一定规则循环读取并且执行。

    那么该循环机制如何运行呢?

    • (1) 打开某个宿主环境时,主线程执行同步任务的所有代码,形成一个执行栈;把遇到的异步任务放入相应的队列里;同时一个独立于主线程的事件循环线程也被创建并一直存在。

    • (2) 当主线程执行完同步任务,会将该执行过程中添加的微任务全部执行完,之后由事件循环机制协调。

    • (3) 事件循环读取当前宏任务队列的一个宏任务,并放入执行栈中执行

    • (4) 在执行过程中遇到宏任务和微任务,按照相同的方式放入相应队列

    • (5) 该宏任务执行完毕后立即执行此次宏任务中所添加的所有微任务

    • (6) 回到第 (3) 步开始重复后面步骤。

    • 说那么多,看个例子

    console.log('1-1');
    
    Promise.resolve().then(() => console.log('微任务 1-1'));
    
    new Promise((resolve) => {
      console.log('1-2');
      resolve();
    }).then(() => {
      console.log('微任务 1-2')
    });
    
    setTimeout(() => console.log('宏任务 1-1'), 100);
    
    console.log('1-3');
    //1-1
    //1-2
    //1-3
    //微任务 1-1
    //微任务 1-2
    //宏任务 1-1
    
    • 主线程开始执行,形成一个执行栈

    • 碰到第一个 console.log('1-1'),并打印 -> 1-1

    • 碰到第一个 Promise,已为成功状态,将其 then() 加到微任务中

    • 碰到第二个 Promise,先执行其中的 console.log('1-2'),打印 -> 1-2,并将其 then() 放入微任务队列

    • 碰到第一个宏任务,放入宏任务队列

    • 碰到 console.log('1-3'),打印 -> 1-3

    • 主线程执行完所有同步任务,开始执行本次添加的所有微任务

    • 读取微任务队列

    • 遇到先进去的第一个 then() ,打印 -> 微任务 1-1

    • 遇到后进去的 then() 打印 -> 微任务 1-2

    • 本次主线程任务完成,下面由事件循环机制来协调。开始读取宏任务队列

    • 遇到第一个放入的宏任务 setTimeout(),将其丢到执行栈延时 100ms 执行,打印 -> 宏任务 1-1

    • 第一次宏任务执行完毕,读取微任务队列,发现没有微任务。进入第二次循环

    • 读取宏任务队列,发现没有宏任务。JS 执行栈开始摸鱼...

    到这里其实会发现,微任务都会紧跟在当前执行栈执行同步任务后执行,而存好的宏任务被放在下次执行,好似重新开始一样。

    按个人总结来就是(不一定对),主线程的执行栈是专门用来执行代码的;当事件循环线程读取到一个宏任务时,将其放入执行栈执行,主线程会执行其中定义的同步任务,将遇到的宏任务和微任务存起来,在本次同步任务执行完之后立即执行微任务。而此次存好的宏任务又会按照相同的方式在下一次循环中进行。因为事件循环机制一次循环只读取执行一个宏任务。

    由此看来其实整个程序也可以看成是一个宏任务,而首次添加的宏任务和微任务是按照上面的方式一层层刨开,按照一次执行一个宏任务和里面所有微任务的规则进行

    • 再看个例子说明宏任务是一次循环读取一次,并且会执行宏任务下所有微任务
    console.log('开始执行主线程');
    console.log('0-1');
    
    Promise.resolve().then(() => console.log('微任务 0-1\n-----'));
    
    setTimeout(() => {//宏任务 1
      console.log('第一个宏任务');
      console.log('宏任务 1-1');
      Promise.resolve().then(() => console.log('微任务 1-1'));
      Promise.resolve().then(() => console.log('微任务 1-2\n-----'));
    
      setTimeout(() => {//宏任务3
        console.log('第三个宏任务');
        console.log('宏任务 3-1')
        Promise.resolve().then(() => console.log('微任务 3-1\n-----'))
      },10);
    
    },100);
    
    setTimeout(() => {//宏任务2
      console.log('第二个宏任务');
      console.log('宏任务2-1');
      Promise.resolve().then(() => console.log('微任务 2-1\n-----'));
    },100);
    
    console.log('0-2');
    
    ***************************
    
    执行结果
    
    开始执行主线程
    0-1
    0-2
    微任务 0-1
    -----
    第一个宏任务
    宏任务 1-1
    微任务 1-1
    微任务 1-2
    -----
    第二个宏任务
    宏任务 2-1
    微任务 2-1
    -----
    第三个宏任务
    宏任务 3-1
    微任务 3-1
    -----
    
    • 开始执行主线程后,将 微任务 0-1 、 宏任务1 、 宏任务2 存入队列,并先打印其同步任务代码,又打印微任务代码
    • 开始第一次事件循环,读取宏任务1(第一个定时),将 微任务 1-1 、微任务 1-2、和宏任务3 存入队列。打印方式如上一条。
    • 开始第二次事件循环,读取宏任务2(第二个定时),将 微任务 2-1 存入队列,打印方式如上。
    • 开始第三次事件循环,读取宏任务队列中最后一个进去的宏任务3(宏任务1中定义的定时器),将 微任务 3-1 存入队列,打印方式如上。

    大概流程图

    流程图

    提示,虽然说是一次循环只读取一个宏任务,但是他没说要等当前宏任务执行完才进行下一次循环哦!!,事件循环读取到队列中的任务并且让它开始执行后,就可以开始下次循环,不需要等待

    • 下面改动的例子,留给自己做练习吧
    console.log(1);
    
    Promise.resolve().then(() => console.log(2));
    
    setTimeout(() => {
      console.log(3);
      Promise.resolve().then(() => console.log(4));
    
      setTimeout(() => {
        console.log(5);
      },10);
    
    },200);
    
    setTimeout(() => {
      console.log(6);
      Promise.resolve().then(() => console.log(7));
      setTimeout(() => {
        console.log(8)
      }, 300);
    },100);
    
    console.log(9);
    

    自己在纸上写了一下,将代码在浏览器上运行之后对比,发现完全正确。你也可以自己写一下哦。

    2020/9/22 更新

    有一种情况,那就是 then() 之后接着 then() ,那么此时的顺序呢?

    console.log('1-1');
    
    Promise.resolve().then(() => console.log('微任务 1-1')).then(() => console.log('微任务 1-3'));
    
    new Promise((resolve) => {
      console.log('1-2');
      resolve();
    }).then(() => {
      console.log('微任务 1-2')
    }).then(() => console.log('微任务 1-4'));
    
    setTimeout(() => console.log('宏任务 1-1'), 100);
    
    //1-1
    //1-2
    //1-3
    //微任务 1-1
    //微任务 1-2
    //微任务 1-3
    //微任务 1-4
    //宏任务 1-1
    

    可以看到他的运行顺序,说明在 then() 执行之后,如果后面还接着 then() 那么按照同样的方式添加到微任务队列,等到之前添加的第一层 then() 都执行完后,在到微任务队列里面读取后面添加的 then(),运行方式如上。并且只有当微任务队列为空时,事件循环机制才会进行到下一轮并读取新的宏任务。

    参考链接

    阮一峰的网络日志
    JavaScript 运行机制详解:再谈Event Loop

    知乎作者:tigerHee
    js中的宏任务与微任务

    博客园作者:daisy,gogogo
    JavaScipt 中的事件循环 event loop,以及微任务和宏任务的概念

    国外作者写的一篇文章
    Tasks, microtasks, queues and schedules

    未完,后续文章记录异步编程的学习笔记...

    第一次学习理解JS运行机制、同步异步的问题。如果文章中有理解错误的地方欢迎提出,我会及时改正,谢谢!

    相关文章

      网友评论

          本文标题:JavaScript的运行机制及宏任务和微任务

          本文链接:https://www.haomeiwen.com/subject/fkuljktx.html