javascript中的事件循环

先上Demo

console.log('script start');

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

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

console.log('script end');
console.log('script start');

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

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

console.log('script end');

对于没有接触过时间循环的同学,当看到这部分的代码时,可能会不知所措,自己对输出的结果会没有把握。

正确的输出结果如下:

script start
script end
promise1
promise2
script start
script end
promise1
promise2

为什么会是这样的输出结果呢? 本文暂时只考虑在浏览器环境下的事件循环

要理解这一点,您需要知道事件循环如何处理任务和微任务。当你第一次遇到它时,这可能会让你头疼。 每个“线程”都有自己的事件循环,因此每个web worker都有自己的事件循环,因此它可以独立执行,在同一源的所有窗口都共享一个事件循环,因为他们可以同步通信,事件循环持续运行,执行任何排队的人物。事件循环有多个任务源,这些任务源保证了执行执行顺序。

setTimeout等待一个给定的延迟,然后为他的回调调度一个新任务。这就是为什么setTimeout被记录在脚本结束之后,因为输出脚本结束是第一个任务的一部分。而setTimeout被记录在一个单独任务中。

微任务通常安排在当前执行的脚本之后立即出发,比如对一批操作做出反应,或者不承担整个新任务的代驾的情况下使用某些异步。只要没有其他javascript在执行中,微任务队列在回调后处理,并在每个任务结束时处理。在微任务期排队的任何其他微任务都被添加到队列的末尾并进行处理。比如mutation observer回调、promise回调等。

只要promise达成,或者如果它已经达成,它就会对伟人进行排队,以获得它的回到。这确保promise回调是异步的,即使promise已经resolve。then 在一个已确定的promise之前,立即排队处理一个微任务。这就是为什么脚本结束后输出promise1和promise2,因为当前运行的脚本必须在处理微任务之前完成,promise1和promise2在setTimeout之前被输出。

宏任务(macro task)

script(全局任务)、setTimeout、setInterval、setImmediate、I/O、UI rendering

微任务(micro task)

process.nextTick、promise,Object.observer,MutationObserver

执行顺序

执行顺序为 script 先进入函数调用栈,然后执行遇到任何其他宏仁务,比如遇到了 setTimeout,就把 setTimeout 放进宏仁务队列中,遇到了微任务就放入微任务队列中,等到函数调用栈的所有内容出栈后,然后执行微任务队列,然后再回头执行宏仁务队列,再进入函数调用栈再执行微任务队列,直到宏仁务队列执行完毕。

异步原理

JavaScript是单线程执行的,无法同时执行多段代码。当某一段代码正在执行的时候,所有后续的任务都必须等待,形成一个队列。一旦当前任务执行完毕,再从队列中取出下一个任务,这也常被称为 “阻塞式执行”。所以一次鼠标点击,或是计时器到达时间点,或是Ajax请求完成触发了回调函数,这些事件处理程序或回调函数都不会立即运行,而是立即排队,一旦线程有空闲就执行。假如当前 JavaScript线程正在执行一段很耗时的代码,此时发生了一次鼠标点击,那么事件处理程序就被阻塞,用户也无法立即看到反馈,事件处理程序会被放入任务队列,直到前面的代码结束以后才会开始执行。如果代码中设定了一个 setTimeout,那么浏览器便会在合适的时间,将代码插入任务队列,如果这个时间设为 0,就代表立即插入队列,但不是立即执行,仍然要等待前面代码执行完毕。所以 setTimeout 并不能保证执行的时间,是否及时执行取决于 JavaScript 线程是拥挤还是空闲。

再分析下面代码的执行顺序

    Promise.resolve().then(()=>{
        console.log('Promise1')
        setTimeout(()=>{
            console.log('setTimeout2')
        },0)
    })

    setTimeout(()=>{
        console.log('setTimeout1')
        Promise.resolve().then(()=>{
            console.log('Promise2')
        })
    },0)

    console.log('start')

    ----------------------------------------------------------------
    // start
    // Promise 1
    // setTimeout1
    // Promise2
    // setTimeout2
    Promise.resolve().then(()=>{
        console.log('Promise1')
        setTimeout(()=>{
            console.log('setTimeout2')
        },0)
    })

    setTimeout(()=>{
        console.log('setTimeout1')
        Promise.resolve().then(()=>{
            console.log('Promise2')
        })
    },0)

    console.log('start')

    ----------------------------------------------------------------
    // start
    // Promise 1
    // setTimeout1
    // Promise2
    // setTimeout2