说一说事件循环?

132 阅读7分钟

涉及到的知识点

  • 多线程
  • awync await
  • vue nextTick
  • react事件切片
  • node事件循环

事件循环(Event Loop):

JavaScript是单线程的,意味着所有任务需要排队,但如果前一个任务耗时很长,后一个任务就要一直等着,会导致网页卡顿。为了解决这个问题,设计了事件循环机制,就是任务的调度机制,先执行同步任务,再执行异步任务。

  • 同步任务: 在主线程上排队执行的任务
  • 异步任务: 放到任务队列中,等待执行的任务

异步任务之宏任务和微任务

宏任务

  • script
  • setTimeout
  • setInterval
  • requestAnimationFrame (浏览器独有)
  • I/O
  • UI rendering (浏览器独有)

微任务

  • Promise.then/resolve/catch/finally
  • MutationObserver: 监听dom发生改变(浏览器独有)
  • node.nextTick

执行顺序

work1(一次同步任务要干的事): 先执行同步代码,遇到宏任务就放入宏任务队列中,遇到微任务,就放入微任务队列中,

  1. 执行work1
  2. 同步任务执行完成后,先检查是否有微任务,有就先执行微任务,执行work1
  3. 微任务队列已空,执行宏任务,按放入顺序执行, 执行work1
  4. 渲染
  • 渲染过程
  • 判断是否调用requestIdleCallback回调,一般在浏览器空闲时会调用。

渲染过程

下面条件至少成立一个, 否则跳过本步骤

  1. 浏览器判断更新渲染会带来视觉上的改变

  2. map of animation frame callbacks 不为空(可以通过requestAnimationFrame添加) 则进行下面几步,

    • 如果窗口的大小发生了变化,执行监听的 resize 方法。
    • 如果页面发生了滚动,执行 scroll 方法。
    • 执行requestAnimationFrame的回调,并传入now作为时间戳。
    • 执行 IntersectionObserver 的回调,并传入now作为时间戳。
    • 重新渲染绘制用户界面。

IntersectionObserver

这个方法是用来检查元素是否在视图窗口内,兼容性较差,

  • 第一个参数是回调函数,
  • 第二个参数中 root属性指定目标元素所在的容器节点 threshold决定了什么时候触发回调函数
const intersectionObserver = new IntersectionObserver(
  (entries) => {
    console.log("触发:");
    entries.forEach((item) => {
      console.log(item.target, item.intersectionRatio);
    });
  },
  {
    threshold: [0.5, 1],
  }
);

intersectionObserver.observe(document.querySelector("#box1"));
intersectionObserver.observe(document.querySelector("#box2"));

注意:

  • IntersectionObserver是异步的
  • IntersectionObserver的实现,应该采用requestIdleCallback(),即只有线程空闲下来,才会执行观察器。这意味着,这个观察器的优先级非常低,只在其他任务执行完,浏览器有了空闲才会执行。

alert阻塞问题

alert会阻塞JS引擎线程和GUI渲染线程, 也就是阻塞代码的执行和页面渲染,

setTimeout执行问题

setTimeout(() => {
  console.log("setTimeout");
}, 3000);
alert("haha");

问: 我们都知道点击弹窗确认后才会执行输出,那么setTimeout的计时是从代码执行时开始计时还是从点击确认后开始计时?

答: 从代码执行时开始计时,setTimeout是由定时触发器线程单独计时,但是由JS引擎线程负责执行,所以setTimeout的执行时间往往有误差

渲染进程:

  1. GUI渲染线程: 负责渲染浏览器界面,解析和渲染。注意,GUI渲染线程与JS引擎线程是互斥的

  2. JS引擎线程: 无论什么时候都只有一个JS线程在运行JS程序, 同样注意,GUI渲染线程与JS引擎线程是互斥的,所以如果JS执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞。

  3. 事件触发线程: DomEvent,如onclick事件,页面阻塞时,该线程会把事件添加到任务队列末尾,等待JS引擎的处理

  4. 定时触发器线程: setIntervalsetTimeout所在线程, 只负责计时,有JS线程负责执行, 注意,W3C在HTML标准中规定,规定要求setTimeout中低于4ms的时间间隔算为4ms。

  5. 异步http请求线程: 在XMLHttpRequest在连接后是通过浏览器新开一个线程请求,检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中。再由JS引擎执行。

debugger会产生阻塞吗?

debugger不会阻塞线程,只是把代码一行一行执行而已,所以可以看到页面的实时更改

async await执行顺序

await同行代码是会立即执行,await后面的代码(当前函数作用域)会变成微任务放入微任务队列中

demo:

const timeoutPromise = (fn, timer = 2000) => {
  console.log("timeoutPromise");
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(fn());
    }, timer);
  });
};

console.log("start");

const fn = async () => {
  console.log("fn start");
  await timeoutPromise(() => {
    console.log("延迟2秒执行");
  });
  console.log("fn end");
};
fn();

new Promise((resolve) => {
  console.log("Promise");
  resolve();
}).then(() => {
  console.log("then");
});

setTimeout(() => {
  console.log("setTimeout");
});

console.log("end");

输出顺序:
start
fn start
timeoutPromise
Promise
end
then
setTimeout
延迟2秒执行
fn end

微任务嵌套微任务

需要先执行完微任务队列中的任务

console.log("start");
Promise.resolve()
  .then(() => {
    console.log("111");
  })
  .then(() => {
    console.log("222");
  })
  .then(() => {
    console.log("333");
  })
  .finally(() => {
    console.log("444");
  });

setTimeout(() => {
  console.log("setTimeout");
});
console.log("end");

// 输出
start
end
111
222
333
444
setTimeout

setTimeout和requestAnimationFrame执行顺序

console.time("test");
setTimeout(() => {
  console.log("setTimeout");
}, 13);

requestAnimationFrame(() => {
  console.timeEnd("test");
  console.log("raf");
});


输出顺序:
不确定, 取决于代码执行到渲染步骤之前setTimeout是否已被推入执行队列

实际应用

vue nextTick

nextTick是使用微任务和宏任务模拟

为什么nextTick可以使用微任务和宏任务模拟

更改dom是同步任务,渲染是异步,正常我们直接修改dom, 不使用nextTick也可以获取更改后的内容。由于vue使用了异步更新策略,我们修改this.data上的数据时,其实会把所有更新放入一个微任务队列中,我们需要借助nextTick确保获取的代码在更改代码后执行,

react 事件切片

react自己模拟实现了requestIdelcallback

let frameDeadline; // 当前帧的结束时间
let channel = new MessageChannel();

// requestAnimationFrame的回调执行是在当前的主线程中,
channel.port2.onmessage = function () {
  let timeRema = frameDeadline - performance.now();
  if (timeRema > 0) {
    // 有空闲时间
  }
};
// 计算当前帧的剩余时间
function timeRemaining() {
  // 当前帧结束时间 - 当前时间
  // 如果结果 > 0 说明当前帧还有剩余时间
  return;
}
window.requestIdleCallback = function (callback) {
  requestAnimationFrame((rafTime) => {
    // 算出当前帧的结束时间 这里按照16.66ms一帧来计算
    frameDeadline = rafTime + 16.66;

    // MessageChannel是一个宏任务,也就是说上面onmessage方法会在当前帧执行完成后才执行
    // 这样就可以计算出当前帧的剩余时间了
    channel.port1.postMessage("haha"); 
  });
};

为什么不使用RequestIdelCallback

  • 实验 api,兼容情况一般。
  • 实验结论: requestIdleCallback FPS只有20ms,
  • RequestIdelCallback定位是不紧急或不重要的任务,

为什么使用MessageChannel,为什么不使用微任务

setTimeout有最小延迟4ms, 兜底使用setTimeout,

微任务会优先执行,会导致无法让出线程给浏览器

const box1 = document.getElementById("box1");

console.log("start");

box1.innerHTML = "end";

const callback = (rafTime) => {
  console.log("requestAnimationFrame", performance.now());
  new Promise(function (resolve) {
    resolve();
  }).then(function () {
    // 还没渲染完成
    alert(" promise ui 已经渲染完毕了吗? ");
  });
  setTimeout(function () {
    // 渲染完成
    alert("setTimeout ui 已经渲染完毕了吗? ");
  });
};
requestAnimationFrame(callback);

判断输出顺序1

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

输出
script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout


判断输出顺序2

说明: 某厂的面试题,最好知道一下

Promise.resolve()
  .then(() => {
    console.log(0);
    return Promise.resolve(4);
  })
  .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

看输出是不是突然怀疑之前学到的知识了?
正常理解 输出: 0 1 2 4 3 5 6

  1. 执行Promise.resolve(4) 第一个微任务
  2. 返回值是4, 这是第二个微任务

实际: 输出: 0 1 2 3 4 5 6

  1. 发现返回值是一个promise, 将Promise.resolve(4)放入一个微任务队列中,这是第一个微任务,
  2. 执行Promise.resolve(4),会自动调用then方法,这是第二个微任务
  3. 返回值是4, 这是第三个微任务

参考文章:
www.zhihu.com/question/45…

node事件循环

image.png

  • process.nextTick:微任务,但是优先级比setImmediate和setTimeout高
  • setTimeout: 最小为1,浏览器最小为4ms
  • setImmediate: 尽快执行的异步,和延迟为0的setTimeout输出顺序不一定

为什么setImmediate和延迟时间为0的setTimeout执行顺序不一定?

  1. 执行宏任务的时间不一定,如果小于1ms,先执行setImmediate,如果大于1ms,先执行setTimeout
  2. 如果在poll回调(I/O相关)中,顺序一定是setImmediate先执行,因为poll阶段后面是check阶段,setTimeout会在下一次事件循环中执行
setTimeout(() => {
  setTimeout(() => {
    console.log("setTimeout");
  }, 0);
  setImmediate(() => {
    console.log("setImmediate");
  });
}, 0);


var fs = require("fs");

fs.readFile(__filename, () => {
  setTimeout(() => {
    console.log("setTimeout");
  }, 0);
  setImmediate(() => {
    console.log("setImmediate");
  });
});