宏任务,微任务,事件循环event loop与promise、setTimeout、async、nextTick【超详细示例讲解】

目录

js单线程

宏任务:在主线程上排队执行的任务,顺序执行

宏任务macrotask: setTimeout,setInterval定时事件,Ajax,DOM事件,script 脚本的执行、 I/O 操作、UI 渲染等。

微任务:不进入主线程、而进入"微任务列表"的任务

微任务microtask(异步):Promise 、async/await、alert(高优先级)

事件轮询机制

0.在执行宏任务过程中,遇到微任务,依次加入微任务队列

1.当前宏任务执行完后,会判断微任务列表中是否有任务。

2.如果有,会把所有微任务放到主线程中并执行

3.如果没有,就继续执行下一个宏任务

重复上面loop

setTimeout(delay=0)=setImmediate:下个Event Loop执行

async、await事件轮询执行时机

async隐式返回Promise,会产生一个微任务await xx;后的代码在微任务时执行

event loop 与 浏览器更新渲染时机

宏任务 → 微任务 → 渲染更新

大量宏任务时,可以将DOM->微任务

vue异步更新原理

nextTick:DOM更新后执行回调函数

Node中的process.nextTick

定时器

setTimeout固定时长后执行setInterval间隔固定时间重复执行setTimeout、setInterval最短时长为4ms

定时器不准的原因

setTimeout/setInterval的执行所需时间并不是确定的

setTimeout/setInterval 动画卡顿:刷新频率≠时间间隔

requestAnimationFrame:浏览器专门为动画提供的API

setTimeout、setInterval、requestAnimationFrame 区别

1)引擎层面

setTimeout: JS引擎 ,存在事件轮询requestAnimationFrame 属于 GUI引擎JS引擎与GUI引擎是互斥的: GUI引擎在渲染时会阻塞JS引擎的计算

2)性能层面

当页面被隐藏或最小化时:定时器 setTimeout仍会在后台执行动画任务

当页面处于未激活的状态下,该页面的屏幕刷新任务会被系统暂停:requestAnimationFrame也会停止


js单线程

宏任务:在主线程上排队执行的任务,顺序执行

宏任务macrotask: setTimeout,setInterval定时事件,Ajax,DOM事件,script 脚本的执行、 I/O 操作、UI 渲等。


微任务:不进入主线程、而进入"微任务列表"的任务

微任务microtask(异步):Promise 、async/await、alert(高优先级)

事件轮询机制

0.在执行宏任务过程,遇到微任务,依次加入微任务队列

1.当前宏任务执行完后,会判断微任务列表中是否有任务。

2.如果有,会把所有微任务放到主线程中并执行

3.如果没有,就继续执行下一个宏任务

重复上面loop

setTimeout(delay=0)=setImmediate:下个Event Loop执行

//宏任务队列:[]
//微任务队列:[promise0]
Promise.resolve()
  .then(function() {
    console.log("promise0");
  })
  .then(function() {
    console.log("promise5");
  });
//定时的setTimeout(delay=0)=setImmediate:下个Event Loop执行
//宏任务队列:[timer1]
//微任务队列:[promise0]
setTimeout(() => {
  console.log("timer1");
  
  Promise.resolve().then(function() {
    console.log("promise2");
  });
  Promise.resolve().then(function() {
    console.log("promise4");
  });
}, 0);
//宏任务队列:[timer1,timer2]
//微任务队列:[promise0]
setTimeout(() => {
  console.log("timer2");
  Promise.resolve().then(function() {
    console.log("promise3");
  });
}, 0);
//宏任务队列:[timer1,timer2]
//微任务队列:[promise0,promise1]
Promise.resolve().then(function() {
  console.log("promise1");
});
//执行start
console.log("start");
//执行当前所有微任务队列:[promise0,promise1]
//执行promise0时将promise5放入了微任务队列:[promise1,promise5]
//接着执行微任务队列:输出promise1,promise5
//当微任务队列为空,开始执行宏任务队列[timer1,timer2]队首的timer1
//执行timer1时碰到了微任务promise2,放进微任务队列[promise2]
//宏任务timer1执行完了,开始执行所有当前所有微任务:[promise2]
//执行promise2完碰到微任务promise4,放进微任务队列:[promise4]
//当前微任务队列不为空,接着执行promise4
//微任务队列为空,接着执行宏任务队列队首[timer2]
//执行timer2时碰到了微任务promise3,放进微任务队列[promise3]
//宏任务timer2执行完了,开始执行所有当前所有微任务:[promise3]


// 打印结果: start promise0 promise1 promise5 timer1 promise2 promise4 timer2 promise3

async、await事件轮询执行时机

async隐式返回Promise,会产生一个微任务
await xx;后的代码在微任务时执行


//1.script start(同步)
console.log("script start");

async function async1() {
  await async2(); // await 隐式返回promise
  console.log("async1 end"); // 这里的执行时机:在执行微任务时执行
}

async function async2() {
  console.log("async2 end"); // 这里是同步代码
}
//2.async2 end(同步)
//微任务队列:[async1 end]
async1();
//宏任务队列:[setTimeout],setTimeOut进入下一loop
setTimeout(function() {
  console.log("setTimeout");
}, 0);
//3.Promise(同步)
//宏任务队列:[setTimeout]
//微任务队列:[async1 end,promise1]
new Promise(resolve => {
  console.log("Promise"); // 这里是同步代码
  resolve();
})
  .then(function() {
    console.log("promise1");
  })
  .then(function() {
    console.log("promise2");
  }); 
//4.script end(同步)
console.log("script end");
//当前loop的宏任务(都是同步代码)都执行完毕
//执行所有微任务[async1 end,promise1]
//执行promise1完后碰到了promise2,加入微任务队列,接着执行
//当前所有微任务都执行完毕,开始执行宏任务队列[setTimeout]

// 打印结果:  script start => async2 end => Promise => script end => async1 end => promise1 => promise2 => setTimeout

event loop 与 浏览器更新渲染时机

宏任务 → 微任务 → 渲染更新

浏览器更新渲染会在event loop中的 宏任务 和 微任务 完成后进行,即宏任务 → 微任务 → 渲染更新(先宏任务 再微任务,然后再渲染更新)

大量宏任务时,可以将DOM->微任务

宏任务队列中,如果有大量任务等待执行时,将dom的变动作为微任务,能更快的将变化呈现给用户,这样就可以在这一次的事件轮询中更新dom

vue异步更新原理

1.如果同一个 watcher 被多次触发,只会被推入到更新队列中一次,可以避免重复修改相同的dom,这种去除重复数据,对于避免不必要的计算和 DOM 操作是非常重要的

2.同步任务执行完毕

3.开始执行异步 watcher 队列的任务,一次性更新 DOM

// 定义watcher类
class Watcher {
  update() {
    // 放到watcher队列中,异步更新
    queueWatcher(this);
  }
  // 触发更新
  run() {
    this.get();
  }
}

// 队列中添加watcher
function queueWatcher(watcher) {
  const id = watcher.id;
  // 先判断watcher是否存在 去掉重复的watcher
  if (!has[id]) {
    queue.push(watcher);
    has[id] = true;
    if (!pending) {
      pending = true;
      // 使用异步更新watcher
      nextTick(flushSchedulerQueue);
    }
  }
}

let queue = []; // 定义watcher队列
let has = {}; // 使用对象来保存id,进行去重操作
let pending = false; // 如果异步队列正在执行,将不会再次执行

// 执行watcher队列的任务
function flushSchedulerQueue() {
  queue.forEach((watcher) => {
    watcher.run();
    if (watcher.options.render) {
      // 在更新之后执行对应的回调: 这里是updated钩子函数
      watcher.cb();
    }
  });
  // 执行完成后清空队列 重置pending状态
  queue = [];
  has = {};
  pending = false;
}

nextTick:DOM更新后执行回调函数

vue nextTick的源码实现,异步优先级判断,总结就是Promise > MutationObserver > setImmediate > setTimeout

MutationObserver(变动观察器)是一种Web API,它允许开发者监视DOM树的变化并在这些变化发生时执行回调函数

  1. Promise:如果浏览器支持PromisenextTick会优先使用Promise.then来创建微任务,以确保回调函数在下一个微任务队列中执行。

  2. MutationObserver:如果浏览器不支持PromisenextTick会检查是否支持MutationObserverMutationObserver允许监视DOM树的变化,因此它也可以用于异步任务的调度。nextTick会尝试使用MutationObserver来创建微任务。

  3. setImmediate:如果浏览器既不支持Promise也不支持MutationObservernextTick会检查是否支持setImmediatesetImmediate是一种宏任务,通常比setTimeout执行得更早,因此它用于创建宏任务级别的异步任务。

  4. setTimeout:如果以上方法都不可用,nextTick会回退到使用setTimeout来创建异步任务。setTimeout是一种宏任务,但是优先级较低,可能在其他异步任务之后执行。

Vue 2.x 中,nextTick的实现主要依赖于宏任务(如setTimeout)和微任务(如Promise

Vue 3 利用了新的 JavaScript 特性——Promise 和微任务(microtask)来实现异步任务的调度。

// 定义nextTick的回调队列
let callbacks = [];

// 批量执行nextTick的回调队列
function flushCallbacks() {
  callbacks.forEach((cb) => cb());
  callbacks = [];
  pending = false;
}

//定义异步方法,优先使用微任务实现
let timerFunc;

// 优先使用promise 微任务
if (Promise) {
  timerFunc = function () {
    return Promise.resolve().then(flushCallbacks);
  };
  // 如不支持promise,再使用MutationObserver 微任务
} else if (MutationObserver) {
  timerFunc = function () {
    const textNode = document.createTextNode('1');
    const observer = new MutationObserver(() => {
      flushCallbacks();
      observer.disconnect();
    });
    const observe = observer.observe(textNode, { characterData: true });
    textNode.textContent = '2';
  };
  // 微任务不支持,再使用宏任务实现
} else if (setImmediate) {
  timerFunc = function () {
    setImmediate(flushCallbacks);
  };
} else {
  timerFunc = function () {
    setTimeout(flushCallbacks);
  };
}

// 定义nextTick方法
export function nextTick(cb) {
  callbacks.push(cb);
  if (!pending) {
    pending = true;
    timerFunc();
  }
}

Node中的process.nextTick

process.nextTick执行顺序早于微任务

console.log("start");
//定时进入下一loop,宏任务队列:[timeout]
setTimeout(() => {
  console.log("timeout");
}, 0);
//微任务队列:[promise]
Promise.resolve().then(() => {
  console.log("promise");
});
//process.nextTick在微任务队首
//微任务队列:[nextTick,promise]
process.nextTick(() => {
  console.log("nextTick");
  Promise.resolve().then(() => {
    console.log("promise1");
  });
});
console.log("end");
// 执行结果 start end nextTick  promise promise1 timeout 

定时器

setTimeout固定时长后执行
setInterval间隔固定时间重复执行
setTimeout、setInterval最短时长为4ms

定时器不准的原因

setTimeout/setInterval的执行所需时间并不是确定的

setTimeout/setInterval是宏任务,根据事件轮询机制,其他任务会阻塞或延迟js任务的执行

考虑极端情况,假如定时器里面的代码需要进行大量的计算,或者是DOM操作,代码执行时间超过定时器的时间,会出现定时器不准的情况

setTimeout/setInterval 动画卡顿:刷新频率≠时间间隔

不同设备的屏幕刷新频率可能不同, setTimeout/setInterval只能设置固定的时间间隔,这个时间和屏幕刷新间隔可能不同

setTimeout/setInterval通过设置一个间隔时间,来不断改变图像实现动画效果,在不同设备上可能会出现卡顿、抖动等现象

requestAnimationFrame:浏览器专门为动画提供的API

requestAnimationFrame刷新频率与显示器的刷新频率保持一致,使用该api可以避免使用setTimeout/setInterval造成动画卡顿的情况

requestAnimationFrame:告诉浏览器在下次重绘之前执行传入的回调函数(通常是操纵dom,更新动画的函数)

setTimeout、setInterval、requestAnimationFrame 区别

1)引擎层面

setTimeout: JS引擎 ,存在事件轮询
requestAnimationFrame 属于 GUI引擎
JS引擎与GUI引擎是互斥的: GUI引擎在渲染时会阻塞JS引擎的计算

这样设计的原因,如果在GUI渲染的时候,JS同时又改变了dom,那么就会造成页面渲染不同步

2)性能层面

当页面被隐藏或最小化时:定时器 setTimeout仍会在后台执行动画任务
当页面处于未激活的状态下,该页面的屏幕刷新任务会被系统暂停:requestAnimationFrame也会停止

猜你喜欢

转载自blog.csdn.net/qq_28838891/article/details/133053470