阅读 1011

JS篇: 宏任务和微任务

宏任务和微任务

参考文章

参考文章

一. 为什么js是单线程的?

  • JavaScript的单线程,与它的用途有关。

  • 作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。

  • 如果js不是单线程,会带来很复杂的同步问题

    • 比如,假定JavaScript同时有两个线程

    • 一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?

  • 为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。

二. 任务队列

  • 单线程就意味着,所有任务需要排队,按序执行。

  • 但是作为脚本语言,在运行的时候,语言设计人员需要考虑的两件重要的事情,就是执行的实时性和效率。

    • 实时性,就是指在代码执行过程中,代码执行的实效性,当前执行语句任务是否在当前的实效下发挥作用。
    • 效率,在这里指的是代码执行过程中,每个语句执行的造成后续执行的延迟率。
  • 任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)

    同步任务
    • 在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;

    • 在一个函数返回的时候,调用者就能够得到预期结果(即拿到了预期的返回值或者看到了预期的效果),那么这个函数就是同步的。

    • console.log('Hello');
      复制代码
    异步任务
    • 不进入主线程、而进入"任务队列"(task queue)的任务。
    • 只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
    • 在函数返回的时候,调用者还不能够得到预期结果,而是需要在将来通过一定的手段得到,那么这个函数就是异步的。
  • 当触发某个事件时,有单线程线性执行,这时不仅仅可能是线程中正在执行其他任务,使得当前事件不能立即执行,更可能是考虑到直接执行当前事件导致的线程阻塞影响执行效率的原因。

  • 这时事件触发的执行流程,比如函数等,将会进入回调的处理过程,而为了实现不同回调的实现,浏览器提供了一个消息队列。

  • 当主线上下文内容都程执行完成后,会将消息队列中的回调逻辑一一取出,将其执行。这就是一个最简单的事件机制模型。

  • 浏览器的事件回调,其实就是一种异步的回调机制。

    • 一种是setTimeout定时器作为代表的,触发后直接进入事件队列等待执行;
    • 一种是XMLHTTPRequest代表的,触发后需要调用去另一个线程执行,执行完成后封装返回值进入事件队列等待。

三. 宏任务和微任务

(宏)任务是什么?

  • 任何按标准机制调度进行执行的JavaScript代码,都是任务
  • 执行一段程序、执行一个事件回调或interval/timeout触发,这些都在任务队列上被调度。
  • 常见宏任务
    • 包括整体代码script,setTimeout,setInterval, setImmediate

微任务是什么?

  • 当一个任务存在,事件循环都会检查该任务是否正把控制权交给其他 JavaScript 代码。
  • 如果不交予执行,事件循环就会运行微任务队列中的所有微任务。
  • 接下来微任务循环会在事件循环的每次迭代中被处理多次,包括处理完事件和其他回调之后。
  • 其次,如果一个微任务通过调用 queueMicrotask(), 向队列中加入了更多的微任务,则那些新加入的微任务会早于下一个任务运行 。
  • 微任务必须是一个异步的执行的任务,这个执行的时间需要在主函数执行之后,也就是微任务建立的函数执行后,而又需要在当前宏任务结束之前。
  • 常见微任务

执行流程

  • 整段脚本script作为宏任务开始执行

  • 遇到微任务将其推入微任务队列,宏任务推入宏任务队列

  • 宏任务执行完毕,检查有没有可执行的微任务

  • 发现有可执行的微任务,将所有微任务执行完毕

  • 当前宏任务执行完毕,开始检查渲染,然后GUI线程接管渲染

  • 渲染完毕后,JS线程继续接管,开始ho新的宏任务,反复如此直到所有任务执行完毕

四. 实战

  • console.log('global')
    
    for (var i = 1;i <= 5;i ++) {
      setTimeout(function() {
        console.log(i)
      },i*1000)
      console.log(i)
    }
    
    new Promise(function (resolve) {
      console.log('promise1')
      resolve()
     }).then(function () {
      console.log('then1')
    })
    
    setTimeout(function () {
      console.log('timeout2')
      new Promise(function (resolve) {
        console.log('timeout2_promise')
        resolve()
      }).then(function () {
        console.log('timeout2_then')
      })
    }, 1000)
    
    复制代码
  • // 第一步,将全局上下文,即整段script代码压入调用栈,作为宏任务开始执行
    
    // 遇到同步任务console,直接执行,打印'global'
    // for循环遍历五次,每一次触发一个timeout宏任务和一个同步任务console,
    // 将每次遍历触发的宏任务推入宏任务队列,同步任务console,直接执行,打印i
    // 这时宏任务队列有5个timeout宏任务
    // 控制台按序打印'global',1,2,3,4,5
    
    // 遇到promise,注意Promise.then里的才是微任务
    // 遇到同步任务 console.log('promise1'),直接执行,打印'promise1'
    // 将微任务Promise.then推入微任务队列
    // 控制台按序打印'global',1,2,3,4,5,'promise1'
    
    // 遇到Timeout,触发宏任务,将其推入宏任务队列
    
    // 这时第一个宏任务,即整段script代码快要执行完了,需要检查微任务队列
    // 微任务队列里有Promise.then,将其执行,打印'then1'
    // 第一个宏任务执行完后的控制台:'global',1,2,3,4,5,'promise1','then1'
    
    
    // 第二步,执行第二个宏任务
    // 将for循环产生的第一个宏任务Timeout执行,打印'6',这时微任务队列为空,直接执行下一个宏任务
    // 注意,由于这时宏任务队列中接下来的4个宏任务都是6秒后才触发
    // 而第五个宏任务1秒后便触发了,这时浏览器会先执行第五个宏任务
    
    // 先执行第五个宏任务,触发同步任务console.log('timeout2')和  console.log('timeout2_promise')
    // 按序打印'timeout2','timeout2_promise'
    // 触发微任务Promise.then,将其推入微任务队列。
    // 这时该宏任务快执行完了,便检查微任务队列,将Promise.then执行,打印'timeout2_then'
    // 控制台按序打印'global',1,2,3,4,5,'promise1','then1','timeout2','timeout2_promise'
    
    // 这时按序执行剩余四个宏任务Timeout
    // 最终控制台打印'global',1,2,3,4,5,'promise1','then1','timeout2','timeout2_promise',6,6,6,6
    复制代码
二 .
  • function taskOne() {
        console.log('task one ...')
        setTimeout(() => {
            Promise.resolve().then(() => {
                console.log('task one micro in macro ...')
            })
            setTimeout(() => {
                console.log('task one macro ...')
            }, 0)
        }, 0)
        taskTwo()
    }
    
    function taskTwo() {
        console.log('task two ...')
        Promise.resolve().then(() => {
            setTimeout(() => {
                console.log('task two macro in micro...')
            }, 0)
        })
    
        setTimeout(() => {
            console.log('task two macro ...')
        }, 0)
    }
    
    setTimeout(() => {
        console.log('running macro ...')
    }, 0)
    
    taskOne()
    
    Promise.resolve().then(() => {
        console.log('running micro ...')
    })
    
    
    复制代码
  • // 这里注意要先将全局上下文压入调用栈
    // 遇到函数调用也要压入调用栈
    // 从调用栈开始取出执行
    // 或者也可以理解为遇到调用就立即执行
    复制代码
  • task one ...
    task two ...
    running micro ...
    running macro ...
    task one micro in macro ...
    task two macro ...
    task two macro in micro...
    task one macro ...
    复制代码
文章分类
前端
文章标签