duuliy

你不知道的 event loop

2021-5-9

前景:

  是不是以为自己懂了event loop, 但是做题的时候总发现会错一点点,来我们一起从头到尾捋捋!

正文:

  浏览器每一个 tab 标签都代表一个独立的进程,其包含的线程有:GUI 渲染线程(负责渲染页面,解析 HTML,CSS 构成 DOM 树)、JS 引擎线程、事件触发线程、定时器触发线程、http 请求线程等主要线程。其中GUI和JS线程是互斥的。
主线程:也就是 js 引擎执行的线程,这个线程只有一个,页面渲染、函数处理都在这个主线程上执行。
工作线程:也称幕后线程,这个线程可能存在于浏览器或js引擎内,与主线程是分开的,处理文件读取、网络请求等异步事件。

1.主线程按上下顺序执行同步任务并把异步任务放进队列挂起。

2.当主线程任务全部执行完毕后,会去异步队列里读取对应任务,推进主线程执行。
Event Loop: 事件循环就是2

3.在事件循环中,每进行一次循环操作称为tick:
A. 选择宏任务( macro task)(oldest task),有就执行一个。
B. 检查是否存在微任务( micro tasks ),如果存在则不停执行,直到清空微任务。
C.主线程 重复上述步骤。

tips:
如果在执行微任务的过程中,产生了新的微任务,一样会将该微任务添加到微任务队列中,V8 引擎一直循环执行微任务队列中的任务,直到队列清空才算执行结束。
微任务和宏任务是绑定的,每个宏任务在执行时,会创建自己的微任务队列。
微任务的执行时长会影响当前宏任务的时长。比如一个宏任务在执行过程中,产生了 10 个微任务,执行每个微任务的时间是 10ms,那么执行这 10 个微任务的时间就是 100ms,也可以说这 10 个微任务让宏任务的执行时间延长了 100ms。
在一个宏任务中,分别创建一个用于回调的宏任务和微任务,无论什么情况下,微任务都早于宏任务执行。

来试试:

例子:

console.log('script start')

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

new Promise(resolve => {
    console.log('promise1');
    resolve();
    setTimeout(() => console.log('timeout2'), 0);
}).then(function() {
    console.log('then1')
})

console.log('script end');

// start ,  p1 ,  end  , then1 ,  t1 , t2

解析:第一个宏任务是script ,内部第一层微任务先执行完,再执行第一层宏任务。。。再第一层宏任务中,再先把微任务执行完。。再执行第三层宏任务….
按照顺序先把第一个setTimeout进去异步队列,再把 promise嵌套setTimeout放进异步队列,而event Loop先取取oldest task。

function test () {
   console.log('start')
    setTimeout(() => {
        console.log('children2')
        Promise.resolve().then(() => {console.log('children2-1')})
    }, 0)
    setTimeout(() => {
        console.log('children3')
        Promise.resolve().then(() => {console.log('children3-1')})
    }, 0)
    Promise.resolve().then(() => {console.log('children1')})
    console.log('end') 
}

//start, end ,1, 2, 2-1 ,3 ,3-1

解析: 宏任务执行后会把跟随本次宏任务的微任务队列清空,再去执行下一个宏任务。
node11之前:// start ,end,1,2,3,2-1,3-1

  async function async1() {
        console.log("async1 start");
        await async2();
        console.log("async1 end");
      }
      async function async2() {
        console.log("async2");
      }
      async1();
      setTimeout(() => {
        console.log("timeout");
      }, 0);
      new Promise(function (resolve) {
        console.log("promise1");
        resolve();
      }).then(function () {
        console.log("promise2");
      });
      console.log("script end");

//async1 start, async2,  p1,  script end, async1 end, p2,  t

解析:这里await async2 相当于包了层promise, async1 end在这个promise的then返回后才去执行下一个console。


new Promise((res,rej)=>{
      console.log(1)
      setTimeout(() => {
        console.log(2)
      });
      res()
    }).then(()=>{
      console.log(3)
    }).then(()=>{
      return new Promise((res,rej)=>{
        console.log(4)
      }).then(()=>{
        console.log(5)
      })
    }).then(()=>{
      console.log(6)
    })

//1,3,4,2

解析:内部promise没有执行resolve,导致后面没法运行并且影响外部promise。

Promise.resolve().then(() => {
      console.log(0);
      return Promise.resolve(4) //这步是同步,但是返回promise,并且绑定在第一个promise上面(这里会消耗一个tick),在下一个then里面需要转化出4(第二个tick),所以一共是2个。
    }).then((res) => {
      console.log(res)
    })
    Promise.resolve().then(() => {
      console.log(1);
    }).then(() => {
      console.log(2);
    }).then(() => {
      console.log(3);
    }).then(() => {
      console.log(5);
    }).then(() => {
      console.log(6);
    })

//0 1 2 3 4 5 6

解析: 在题目上面
参考了本文提供的stackoverflow和阮一峰的链接,Promise(暂且成为A)返回Promise(暂且称为B)的时候,
A需要等待B的状态转换,因此原生Promise似乎是多用了一个tick将A的状态bind到B上,关键在这步,多了一个tick,
接下来一个tick是等待B的执行结果,所以总共需要等待2个tick。但是似乎Promise A + 并没有对这个规范做要求,
所以会出现你这个结果,另外如果用async/await的话似乎也不会出现这个问题

小总结:

而宏任务一般是:包括整体代码script(整体代码),setTimeout,setInterval,postMessage,IO操作,UI render(解析dom,布局等),setImmerediate(Node.js环境),requestAnimationFrame。
微任务:Promise.then(),MutaionOberver(监听dom变化的api),process.nextTick(Node.js环境)。
记住就行了。

process.nextTick快于Promise.then()快于postMessage快于setTimeout快于setimmediate

题外话:

1.setTimeout倒计时为什么会出现误差?
setTimeout() 只是将事件插入了“任务队列”,必须等当前代码(执行栈)执行完,主线程才会去执行它指定的回调函数。要是当前代码消耗时间很长,也有可能要等很久,所以并没办法保证回调函数一定会在 setTimeout() 指定的时间执行。所以, setTimeout() 的第二个参数表示的是最少时间,并非是确切时间。
HTML5标准规定了 setTimeout() 的第二个参数的最小值不得小于4毫秒,如果低于这个值,则默认是4毫秒。在此之前。老版本的浏览器都将最短时间设为10毫秒。另外,对于那些DOM的变动(尤其是涉及页面重新渲染的部分),通常是间隔16毫秒执行。这时使用 requestAnimationFrame() 的效果要好于 setTimeout();

2.HTML5标准规定
setTimeout的最短时间间隔是4毫秒;
规范:1.小于0s算0s,嵌套超过5层,最小时间小于4ms,算4ms.
谷歌测试出一个比较合适的时间。
setInterval的最短间隔时间是10毫秒,也就是说,小于10毫秒的时间间隔会被调整到10毫秒
在实际中 加上渲染,我做过延时器,最先间隔时间是90ms

setTimeout返回值id是定时器的id,避免多个定时器重合浏览器分不开。
第三个和第四个参数作为第一个参数函数的实参

3.node 和 浏览器 eventLoop的主要区别
浏览器中是一次执行只取异步队列的一个宏任务,然后执行完毕本层所有微任务,再进行下一个宏任务。
而在node中是直接将宏任务中的同步事件全部执行完毕,再依次去执行宏任务中的微任务。

node11 之后一些特性已经向浏览器看齐了,宏微的事件循环一样了

node8 的版本打印结果:也跟11之后一样,原因还没去探究。