EventLoop整理

192 阅读5分钟

这张图很好的解释了js处理并发的问题,其实主线程就是执行栈里的方法,一个一个的执行,出现异步就放在堆里,然后堆里的异步执行完成之后,就放到事件队列里。等主线程空了之后,js就会去执行事件队列里的回调函数。

而js主线程执行完去执行事件队列,然后再执行主线程的循环过程,叫做事件循环(Event Loop)

具体是怎么做的呢?

主线程先执行同步的任务,执行过程中遇到异步任务,就扔到堆(event table)里去执行主线程会继续执行同步的任务,event table里的异步任务在执行完后,按顺序在任务队列(event queue)里注册回调函数。同步任务执行完之后(即栈空了之后),js引擎的monitoring process进程,会持续不断的检查主线程执行栈是否为空,一旦为空,就会去Event Queue那里检查是否有等待被调用的函数。有就会去取任务队列里的回调函数按顺序执行,执行回调函数的过程中,遇到异步继续放到堆里执行,主线程执行完任务队列里的回调函数之后,继续执行栈里的方法,以此循环。

宏任务(Task)和微任务(microTask)

这两个任务进入的queue是不同的。宏任务进宏任务队列,微任务进微任务队列。

在js主线程在解析文件的时候,会先把执行这个文件设置成为一个宏任务,然后放到宏任务队列,然后一行一行代码的往下执行。

在主线程第一次执行的时候,就是在执行宏任务队列的第一个任务,就是执行当前代码。宏任务结束的时候,去查看微任务队列里是否有可以调用的回调函数。如果有,就执行所有的微任务的回调函数,然后开启新的宏任务。没有的话,就会直接开始新的宏任务。

nodeJs里的事件循环

就单从API层面上来理解,Node新增了两个方法可以用来使用:微任务的process.nextTick以及宏任务的setImmediate。

setImmediate与setTimeout的区别

在官方文档中的定义,setImmediate为一次Event Loop执行完毕后调用。setTimeout则是通过计算一个延迟时间后进行执行。

但是同时还提到了如果在主进程中直接执行这两个操作,很难保证哪个会先触发。 因为如果主进程中先注册了两个任务,然后执行的代码耗时超过XXs,而这时定时器已经处于可执行回调的状态了。 所以会先执行定时器,而执行完定时器以后才是结束了一次Event Loop,这时才会执行setImmediate

  • setTimeout 先执行的方法,在两个方法的后面加一个循环,时间超过setTimeout的时间,则setTimout的回调已经可以调用了,而setImmediate的会等循环结束之后再注册回调函数。
  • 在一个宏任务里,setImmediate一定先执行

process.nextTick

可以认为是一个类似于PromiseMutationObserver的微任务实现,在代码执行的过程中可以随时插入nextTick,并且会保证在下一个宏任务开始之前所执行。

    // eg1:
    console.log(1);
    setTimeout(() => {
    	console.log(2);
    });
    process.nextTick(()=>{
    	console.log(3);
    });
    setImmediate(()=>{
    	console.log(4);
    });
    new Promise(resolve=>{
    	console.log(5);
    	resolve();
    	console.log(6);
    }).then(()=>{
    	console.log(7);
    })
    Promise.resolve().then(()=>{
    	console.log(8);
    	process.nextTick(()=>{
    		console.log(9)
    	})
    })

例1就比较好了解了:JS栈在运行的时候,先把整个代码run script放到宏任务队列里。

  • 打印1,
  • 然后开始往下执行遇到setTimeout,就扔到task table里,因为没有延时,回调函数就直接进入task queue里。
  • 继续往下,遇到process.nextTick,扔到microTask queue里,
  • 遇到setImmediate,回调扔到task queue里。
  • 继续往下,执行new Promise,打印5,6。
  • 往下执行then,把then回调函数扔到microTask queue里,
  • 把then 的回调函数扔到microTask queue里。

第一个宏任务执行完了,发现microTask queue里有好多任务,就调用里面的方法

  • 执行第一个process,打印3
  • 执行第一个then,打印7
  • 执行第二个then,打印8,吧process扔到microTask queue里
  • 执行第二个process 打印9

第一个宏任务执行完,且没有微任务了,执行第二个宏任务

  • 执行setTimeout的回调函数,打印2

第二个宏任务执行完,没有微任务了,执行第三个宏任务

  • 执行setImmediate的回调函数,打印4

所以答案就是: 1,5,6,3,7,8,9,2,4

    // eg2:
    console.log('start');
    
    const interval = setInterval(() => {
      console.log('setInterval');
    }, 0);
    
    setTimeout(() => {
      console.log('setTimeout 1');
      Promise.resolve()
        .then(() => {
          console.log('promise 3');
        })
        .then(() => {
          console.log('promise 4');
        })
        .then(() => {
          setTimeout(() => {
            console.log('setTimeout 2');
            Promise.resolve()
              .then(() => {
                console.log('promise 5');
              })
              .then(() => {
                console.log('promise 6');
              })
              .then(() => {
                clearInterval(interval);
              });
          }, 0);
        });
    }, 0);
    
    Promise.resolve()
      .then(() => {
        console.log('promise 1');
      })
      .then(() => {
        console.log('promise 2');
      });

这道题可以这么看:

js栈第一次执行,把 run script 放到task queue里,然后开始执行代码

  • 打印 start
  • 把setInterval的回调函数扔到task queue里
  • 把setTimeout 的回调函数扔到task queue里
  • 把两个then扔到microtask queue里

第一个宏任务run script 执行完毕,microtask queue里不为空

  • 执行第一个then,打印promise 1
  • 执行第二个then,打印promise 2

第一个宏任务执行完毕,且microtask queue为空,执行第二个宏任务

  • 打印setInterval,并把其放到task queue里

第二个宏任务执行完毕,且microtask queue为空,执行第三个宏任务

  • 打印setTimeout1
  • 把三个then扔到microtask queue里

第三个宏任务执行完,但microtask queue不为空

  • 执行第一个then 打印 promise 3
  • 执行第二个then 打印 promise 4
  • 执行第三个then,遇到setTimeout ,扔到任务队列里

第三个宏任务执行完毕,microtask queue为空,执行下一个宏任务

  • 打印setInterval, 并把其放到task queue里

第四个宏任务执行完毕,且microtask queue为空,执行下一个宏任务

  • 打印setTimeout2
  • 把三个then扔到microtask queue里

第五个宏任务执行完,micortask queue不为空,执行微任务

  • 打印promise 5
  • 打印promise 6
  • 终止掉task queue里的setInterval的回调函数。

所以答案就是:

start promise 1 promise 2 setInterval setTimeout 1 promise 3 promise 4 setInterval setTimeout 2 promise 5 promise 6