欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

JavaScript 运行机制详解(宏任务与微任务的解读 eventLoop)

程序员文章站 2022-06-30 11:47:06
...

JavaScript 运行机制详解(宏任务与微任务的解读 eventLoop)

前言

1. 为什么JavaScript是单线程?
2. 任务队列
3. 微任务,宏任务

一、为什么JavaScript是单线程?

JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。

二、任务队列
单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着

如果排队是因为计算量大,CPU忙不过来,倒也算了,但是很多时候CPU是闲着的,因为IO设备(输入输出设备)很慢(比如Ajax操作从网络读取数据),不得不等着结果出来,再往下执行。

JavaScript语言的设计者意识到,这时主线程完全可以不管IO设备,挂起处于等待中的任务,先运行排在后面的任务。等到IO设备返回了结果,再回过头,把挂起的任务继续执行下去。

于是,所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。

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

如图展示
JavaScript 运行机制详解(宏任务与微任务的解读 eventLoop)

三,微任务,宏任务

js是单线程的,而浏览器是多线程,浏览器的event loop至少包含两个队列,macrotask 队列和队列。
微任务和宏任务都皆为异步操作,他们都是属于一个队列。主要区别在于他们的执行顺序,eventloop的走向和取值。

常见宏任务与微任务
宏任务

宏任务,macrotask ,又叫 tasks 。一些异步任务的回调会依次进入 ** macro task queur** , 等待后续被调用,这些异步任务包括:

  • setTimeout
  • setInterval
  • setImmediate(Node环境下独有)
  • requestAnimationFrame(浏览器环境独有)
  • I/O
  • UI rendering(浏览器独有)

微任务
微任务,microtask,又叫 jobs。另外一些异步任务的回调会依次进入 micro task queue,等待后续被调用,这些异步任务包括:

  • process.nextTick(Node环境独有)
  • Promise.then()
  • Object.observe
  • MutationObserver

微任务和宏任务的工作流程
JavaScript 运行机制详解(宏任务与微任务的解读 eventLoop)

js异步有一个机制,就是遇到宏任务,先执行宏任务,将宏任务放入eventqueue,然后在执行微任务,将微任务放入eventqueue最骚的是,这两个queue不是一个queue。当你往外拿的时候先从微任务里拿这个回掉函数,然后再从宏任务的queue上拿宏任务的回掉函数。 我当时看到这我就服了还有这种骚操作(记住这么骚,这么秀就好)。

4.理解理论,做这套面试题再进一步理解

// 请将下面这段代码的执行结果写出来
async function async1() {
      console.log('async1 start')
      await async2()
      console.log('async1 end')
    }
    
    async function async2() {
      console.log('async2')
    }
    
    console.log('script start')

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

    async1();

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

    console.log('script end')

这段代码的执行结果会是什么呢?

知识点
考察的是事件循环和回调队列。注意以下几点:
1.Promise 优先于 setTimeout 宏任务,所以 setTimeout 回调会最后执行

2.Promise 一旦被定义就会立即执行

3.Promise 的 resolve 和 reject 是异步执行的回调。所以 resolve() 会被放到回调队列中,在主函数执行完和 setTimeout 之前调用

4.await 执行完后,会让出线程。async 标记的函数会返回一个 Promise 对象

解析:

因为settimeout是宏任务,虽然先执行的他,但是他被放到了宏任务的eventqueue里面,然后代码继续往下检查看有没有微任务,检测到Promise的then函数把他放入了微任务序列。等到主线进程的所有代码执行结束后。先从微任务queue里拿回掉函数,然后微任务queue空了后再从宏任务的queue拿函数。

1.首先,事件循环从宏任务(macrostack)队列开始,这个时候,宏任务队列中,只有一个 script (整体代码)任务。从宏任务队列中取出一个任务来执行。

2.首先执行 console.log(‘script start’),输出 ‘script start’

3.遇到 setTimeout 把 console.log(‘setTimeout’) 放到 macrotask 队列中

4.执行 aync1() 输出 ‘async1 start’ 和 ‘async2’ ,把 console.log(‘async1 end’) 放到 micro 队列中

5.执行到 promise ,输出 ‘promise1’ ,把 console.log(‘promise2’) 放到 micro 队列中
执行 console.log(‘script end’),输出 ‘script end’

6.macrotask 执行完成会执行 microtask ,把 microtask quene 里面的 microtask 全部拿出来一次性执行完,所以会输出 ‘async1 end’ 和 ‘promise2’

7.开始新一轮的事件循环,去除执行一个 macrotask 执行,所以会输出 ‘setTimeout’