事件循环-同步异步-计时器精确问题

消息队列的解释

每个任务都有一个任务类型。
同一个类型的任务必须在一个队列中。
不同类型的任务可以分属于不同的队列中。
在一次事件循环中,浏览器可以【根据实际情况】从不同的队列中取出任务执行。
浏览器必须准备好一个微队列,微队列中的任务优先其他所有类型的任务。

chrome中的常见队列

在 chrome 的实现中,至少包含了下面的队列:
1,延时队列:用于存放计时器到达后的回调任务,优先级 中
2,交互队列:也就我们说的点击事件,浏览器缩放窗口等;优先级高。
3,微队列:用户存放需要最快执行的任务,优先级[最高]

阐述一下 JS的事件循环

事件循环又叫做消息循环,是浏览器渲染主线程的工作方式。
在Chrome 的源码中,它开启一个不会结束的 for 循环;
每次循环从消息队列中取出第一个任务执行。
而其他线程只需要在合适的时候将任务加入到队列末尾即可。
过去把消息队列简单分为宏队列和微队列,
这种说法目前已无法满足复杂的浏览器环境,
取而代之的是一种更加灵活多变的处理方式。
根据 W3C官方的解释:每个任务有不同的类型,同类型的任务必须在同一个队列。
不同的任务可以属于不同的队列。
不同任务队列有不同的优先级。
在一次事件循环中,由浏览器自行决定取哪一个队列的任务。
但浏览器必须有一个微队列,微队列的任务一定具有最高的优先级,必须优先调度执行。

代码执行顺序问题

你现在记住:主线程肯定是最先执行的;
代码必须要等到主线程执行完之后;
才能去消息队列中去拿取任务执行;
然后消息队列中有优先级; 微队列 ==> 交互队列 ==> 延时队列

1,理解先同步-后异步(延时队列)

setTimeout(()=>{
  console.log(1)
},0)
console.log(2)

输出的结果:2 然后是1
为什么呢?
因为:主线程代码肯定是先执行;哪怕你延时0毫秒。
主线程执行完毕后。
然后从消息队列中拿取延时队列执行;
所以先输出了2;然后是1

2,理解先同步-后异步(延时队列)

 function yanShi(time){
  var start = Date.now();
  while(Date.now() - start < time){

  }
}
setTimeout(()=>{
  console.log(2)
},0)
yanShi(1000)
console.log(1)

先执行 yanShi函数;这个函数会延时1s后;
我们去输出1;这个时候我们的主线程已经执行完了;
然后我们去执行延时队列中的代码;输出2;
所以:等待1s后,先输出2;然后是1 

3,new Promise(callback)与 new Promise().then的区别

很多时候,我们都以为new Promise(callback) 是异步的;
其实这个观点是错误的;
当我们调用new Promise(callback)时,它是一个同步代码,回调函数会立即执行。
new Promise().then这个才是异步的;
他们的区别之一就是说:前者是同步的;后者是异步的;
下面我们来看一段代码

4,new Promise(callback)是同步代码的

console.log('start');
const promise1 = new Promise((resolve, reject) => {
  console.log(1)
})
console.log('end');

有些同学认为是 start - end - 1;
有的同学认为是 start- 1 -end
大家可以去输出一下;这里就不说争取答案了.

5,promise.then是异步

console.log('start');
const promise1 = new Promise((resolve, reject) => {
  console.log(1)
  resolve(2)
})
promise1.then(res => {
  console.log(res)
})
console.log('end');
我们先是执行主线的代码;输出start;
new Promise 是一个同步的代码;输出 1;
promise1.then是一个微队列;
需要等待主线程执行完毕之后在执行;所以先输出end;
等待主线执行完毕之后,最后输出 2

6,promise.then要状态发生改变才会执行,否则不会执行then

console.log('start');
const promise1 = new Promise((resolve, reject) => {
  console.log(1)
})
promise1.then(res => {
  console.log(2)
})
console.log('end');

这个输出结果很多小伙伴会认为是: 
start - 1 - end - 2
但是实际2不会输出的;
为啥2不会输出呢?
因为 Promise的then方法必须要等到 padding 状态发生改变时才会触发then方法;
也就时说:要触发then方法必须要调用 resolve 或者 reject才会触发;其他不会触发

7,先主线程 - 计时器 - 微队列

const promise = new Promise((resolve, reject) => {
  console.log(1);
  setTimeout(() => {
    console.log("2");
    resolve("3");
    console.log("4");
  }, 0);
  console.log(5);
});

promise.then((res) => {
  console.log(res);
});

console.log(6);

我们都知道主线程先执行; 
new Promise下的代码是主线程的;刚刚我们说过:
小技巧:我们可以把 console.log(1) 当作 fn1函数输出1;
所以先输出 1 - 5 - 6
然后我们看 setTimeout[延时队列] 和  promise.then[微队列] 谁先执行;
正常情况下是微任务 >  延时队列
但是 then方法是需要状态改变时才会被触发,要想触发then需要先执行 setTimeout
所以又输出了 2 - 4 -3
//  1 - 5 - 6 - 2 - 4 -3

8,延时队列

const timer1 = setTimeout(() => {
 console.log('1');
  const promise1 = Promise.resolve().then(() => {
    console.log('2')
  })
}, 0)

const timer2 = setTimeout(() => {
  console.log('3')
}, 0)

// 1 - 2 - 3

9,主线程 - 添加到微队列 - 延时队列

setTimeout(()=>{
  console.log(1)
},0)
Promise.resolve().then(function(){
  console.log(2)
})
console.log(3)

同样,我们先去执行主线程的代码;
所以先输出的是3;
然后我们去消息队列中去获取任务;
先拿取微队列,然后是交互队列(这里没有),然后是延时队列;
这里 Promise.resolve().then(Fn)就是把一个函数Fn添加到微队列中;
因此执行2;最后输出1;
3--2--1

把一个任务添加到微队列中的方式

Promise.resolve().then(fn函数)
fn这个函数就会添加到微队列中

10,主线程- 添加到微队列 - 延时队列

function a(){
  console.log(1)
  Promise.resolve().then(function(){
    console.log(2)
  })
}
setTimeout(()=>{
  console.log(3)
  Promise.resolve().then(a)
},0)
Promise.resolve().then(()=>{
  console.log(4)
})
console.log(5)

首先我们执行主线的代码;输出5;
然后我们看是否有微队列;如果有去执行;所以输出4;
微队列执行完后我们看交互队列;没有
我们去执行延时队列;所以输出3;
最后我们执行1;然后数2
最终的结果是:5-4-3-1-2
注意点:延时队列中,有输出语句,有添加到微队列;
它是一个一个去执行;并不会说先去做微队列。
而是按照正常代码执行顺序去做。

11,主线程-立即添加到微队列-延时队列

function a(){
  console.log(1)
  Promise.resolve().then(function(){
    console.log(2)
  })
}

setTimeout(()=>{
  console.log(3)
},0)

Promise.resolve().then(a)
console.log(5)

最后的输出: 5-1-2-3

下面这输出比较难-特别是第2个

console.log('0')

const fn = () => (new Promise((resolve, reject) => {
  console.log(1);
  resolve('ok')
}))

console.log('2')

fn().then(res => {
  console.log(res)
})

console.log('3')

//  微任务 - 延时队列
const promise1 = Promise.resolve().then(() => {
  console.log('1');
  const timer2 = setTimeout(() => {
    console.log('2')
  }, 0)
});
// 延时队列 - 微任务
const timer1 = setTimeout(() => {
  console.log('3')
  const promise2 = Promise.resolve().then(() => {
    console.log('4')
  })
}, 0)

这两个是比较难的;如果小伙伴能正确答对;
就算理解了。
第1个的输出是:0 - 2 - 1 - 3 - ok
第2个的输出是:1 - 3 - 4 - 2

JS中的计时器能够做到精确计时吗?为什么?

不行。因为:
1,计算机硬件没有原子钟,无法做到精确计时。
2,操作系统的计时函数本身就有少量偏差,由于 JS 的计时器最终调用的是操作系统的函数,这又带来了偏差。
3,按照 W3C 的标准,浏览器实现计时器时,如果嵌套层级超过 5 层,至少有4毫秒的偏差。
4,受事件循环的影响,计时器的回调函数只能在主线程空闲时运行,因此又带来了偏差
因此不能够准确计时。 
也就是说:setTimeout,setInterval,的计时是不准确的。

计时器嵌套层级超过 5 层,至少有4毫秒的偏差的解释

setTimeout(function(){
  setTimeout(function(){
    setTimeout(function(){
      setTimeout(function(){
        setTimeout(function(){
          // 这个延时器在第6层,超过了5层,即使我们延时的是0毫秒;
          // 最终也会被修改为4毫秒后执行
          setTimeout(function(){

          },0)
        },0)
      },0)
    },0)
  },0)
},0)

单线程是异步产生的原因。

事件循环是异步的实现方式