【前端进阶】详解 微任务、宏任务、异步阻塞、事件循环、单例模式

732 阅读7分钟

本文适合前端进阶,从简单demo到实现原理的详细代码复现。

从最简单的开始

console.log('1');
console.log('2');
console.log('3');
// 1
// 2
// 3

什么是 宏任务、微任务

当上面的代码加上一点点修改后

const getDetail = (id) => {
    Promise.resolve().then(() => console.log('微任务'));
    console.log('2');
};
console.log('1');
getDetail();
console.log('3');
// 1
// 2
// 3
// 微任务

解释:上面这段代码编译后会变成 👇

console.log('1');
Promise.resolve().then(() => console.log('微任务'));
console.log('2');
console.log('3');

随后会把.then()回调函数会抛入微任务队列 👇

// 宏任务
const macroTask = [
    console.log('1'),
    Promise.resolve(),
    console.log('2'),
    console.log('3'),
];
// 微任务
const microTask = [
    () => console.log('微任务'),
];

引出概念 then() 回调函数会进入 微任务队列

引出概念 微任务 会在 宏任务 执行完后再执行

所以这里会在最后才打印微任务。

什么是 异步阻塞

再给上面的代码加上关键字 async、await

const getDetail = async (id) => {// 这里加上 async 语法糖
  await Promise.resolve().then(() => console.log('微任务'));// 这里加上 await 语法糖
  console.log('2');
};
console.log('1');
getDetail();
console.log('3');
// 1
// 3
// 微任务
// 2

那么这段代码的 微/宏任务 会解析成这样

// 宏任务
const macroTask = [
    console.log('1'),
    getDetail(),
    console.log('3'),
];
// 微任务
const microTask = [
  await Promise.resolve().then(() => console.log('微任务')),
  console.log('2'),
];

可以发现执行顺序 console.log('2') 在 await 后面了。

引出概念 await 关键字会阻塞后面代码的执行。

什么是 事件循环 Event loop

这里同样再给上面的代码加上关键字 await

const getDetail = async (id) => {
  await Promise.resolve().then(() => console.log('异步阻塞'));
  console.log('2');
};
const fn = async () => {
  console.log('1');
  await getDetail();// 在这里加上了 async await
  Promise.resolve().then(() => console.log('微任务里面又抛了个微任务'));
  console.log('3');
};

fn();

解释:上面这段代码的 async await 关键字编译后会变成 👇

await getDetail();
Promise.resolve().then(() => console.log('微任务里面又抛了个微任务'));
console.log('3');
// 👆相当于👇
getDetail().then((res) => {
    Promise.resolve().then(() => console.log('微任务里面又抛了个微任务'));
    console.log('3');
});

那么结合前面的概念: then 回调进入微任务队列

就变成了这样

Event loop 第一遍
// 宏任务
const macroTask = [
    console.log('1'),
    await getDetail(),
];
// 微任务
const microTask = [
    Promise.resolve().then(() => console.log('微任务里面又抛了个微任务')),
    console.log('3'),
];

随后解析getDetail函数

Event loop 第一遍
// 宏任务
const macroTask = [
    console.log('1'), 
    await(async (id) => {
        await Promise.resolve().then(() => console.log('异步阻塞'));
        console.log('2');
    }),
];
// 微任务
const microTask = [
    Promise.resolve().then(() => console.log('微任务里面又抛了个微任务')),
    console.log('3'),
];

那么结合前面的概念: await 关键字会阻塞后面代码的执行

就变成了这样

执行宏任务
// console.log('1')

await 异步阻塞,所以宏任务里面的then回调没有进微任务,而是等待执行
// console.log('异步阻塞') 

// console.log('2')

执行完毕,任务如下
const macroTask= [

];
const microTask = [
    Promise.resolve().then(() => console.log('微任务里面又抛了个微任务')),
    console.log('3'),
];

那么结合前面的概念: then 回调进入微任务队列

那么这句Promise.resolve().then(() => console.log('微任务里面又抛了个微任务'))是什么意思呢,要怎么执行呢?

const microTask = [
    Promise.resolve().then(() => console.log('微任务里面又抛了个微任务')),
    console.log('3'),
];

可以理解为

微任务 本身当作 宏任务

微任务抛的微任务 当作 微任务

那么结合前面的概念:微任务 会在 宏任务 执行完后再执行

形成一个循环

这个过程就叫 Event loop,事件轮询

下面代码可以直观看出效果

Event loop 第一遍
const macroTask= [

];
const microTask = [
    Promise.resolve().then(() => console.log('微任务里面又抛了个微任务')),
    console.log('3'),
];

👇

Event loop 第二遍
const macroTask= [
     console.log('3'),
];
const microTask = [
    () => console.log('微任务里面又抛了个微任务'),
];

所以这个打印结果就是

// 1
// 异步阻塞
// 2
// 3
// 微任务里面又抛了个微任务

setTimeout

回到最初的例子

console.log('1');
console.log('2');
console.log('3');
// 1
// 2
// 3

加上 setTimeout 函数后👇

console.log('1');
setTimeout(() => {
    console.log('2');
},0);// 设置 0 毫秒
console.log('3');
// 1
// 3
// 2

会发现 console.log('2') 在最后执行。

这是 setTimeout 因为跟 then 一样

那么结合前面的概念: then 回调进入微任务队列

引出概念 setTimeout 回调会进入微任务

// 宏任务
const macroTask = [
    console.log('1'),
    setTimeout(),
    console.log('3'),
];
// 微任务
const microTask = [
    () => console.log('2'),
];
// 1
// 3
// 2

但是 setTimeout 区别于 Promise

不同点

🎯 setTimeout 是同步,在执行到 setTimeout 的时候就会抛入一个独立的定时器模块,倒计时到了后会把回调抛入微任务,且倒计时的最低值是 4ms 也就是说最低会在 4ms 后进入微任务队列,这也是为什么 promise 优先级比 setTimeout 高的原因。

setTimeout 函数返回的 id 也就是 独立的定时器模块 的 id,所以我们在调用 clearTimeout 函数传递 id 的时候也就会删除 id 对应的这个定时器模块。

🎯 new Promise 本身是同步, resolve,reject 是异步,await promise 阻塞下面代码的执行。

相同点

🎯 本身都是宏任务,回调都是微任务

由于 setTimeout 用起来打印顺序跟 Promise 一样,有人会觉得 setTimeout 是异步,但其实不是。

例子1

const fn = async () => {
  // setTimeout
  await setTimeout(async () => console.log('setTimeout'),1000);
  
  console.log('1');
};

fn();
// 1
// setTimeout
setTimeout 不能使用 await ,没有阻塞代码 所以不是异步

例子2

const fn = async () => {
  // setTimeout
  setTimeout(() => console.log('setTimeout'), 3000); // 定时3秒

  Promise.resolve().then(() => console.log('promise'));

  // 循环10万次,也就是宏任务里面有十万句 console.log('');
  console.log('for start');

  new Array(100000).fill().map((itm, idx) => console.log('for', idx));

  console.log('for end');
};

fn();

// for start
// for i * 10,0000 ...
// for end
// 等待宏任务for循环10万次后,会立即打印 setTimeout ,而不是等待3秒打印
// 因为执行 10 次语句的超过了3秒,定时器模块在3秒后把settimeout的回调抛入了微任务队列中
// 所以宏任务执行完毕,开始执行微任务就会立即打印
// setTimeout

“new Promise 本身是同步”,这怎么理解?Promise不是异步的嘛,怎么又变成同步了

new Promise 本身是一个实例化对象的操作

同步👇

const p = new Promise()

异步👇

const p =await new Promise();// await

const p =Promise.resolve();// resolve or reject

const p =Promise.resolve().then();// callback

console.log('1');
console.log('2');
new Promise((resolve, reject) => {
  console.log('Promise');
});
console.log('3');
// 1
// 2
// Promise
// 3

更多 Promise 实现可以看另一篇文章:【前端进阶】用 Typescript 手写 Promise,A+ 规范,可用 async、await 语法糖 - 掘金 (juejin.cn)

什么是 单例模式

上一步刚好讲到实例化对象,拓展一下对象的单例模式

单例模式是为了解决内存开销问题,多次实例化对象,只返回同一个实例

常见应用于状态管理库等,去维护单一数据源,也就是 store。

一个简单的例子

class User {
  userInfo = {
    name: 'ddd',
    age: 22,
  };
}
class SingleTonUser {
  instance = null;
  // ??= 空值赋值运算符,这里判断是否存在instance,不存在就赋值 new User()
  static init = () => (this.instance ??= new User());
  constructor() {
    // 在实例化对象的时候,调用init方法判断是否存在instance然后返回出去
    return SingleTonUser.init();
  }
}

const obj1 = new User();
const obj2 = new User();

const obj3 = new SingleTonUser();
const obj4 = new SingleTonUser();

console.log('obj1', obj1); // User { userInfo: { name: 'ddd', age: 22 } }
console.log('obj3', obj3); // User { userInfo: { name: 'ddd', age: 22 } }
console.log('obj1 === obj2', obj1 === obj2); // false
console.log('obj3 === obj4', obj3 === obj4); // true

一个更简单的例子去理解 instance 对象

const obj1 = {
  name: 'ddd',
  age: 22,
};
const obj2 = {
  name: 'ddd',
  age: 22,
};
console.log('obj1 === obj2', obj1 === obj2); // false
// 这里是创建了2个内存地址,虽然数据一样,但是存放数据的内存地址不同,所以判断不一样。
// 创建了2个内存地址就可以理解为上面的 new User();
const obj1 = {
  name: 'ddd',
  age: 22,
};
const obj2 = obj1;
console.log('obj1 === obj2', obj1 === obj2); // true
// 这里虽然创建了2个变量: obj1 , obj2 。
// 但是只创建了1个内存地址,因为在定义变量 obj2 的时候只是指向了 obj1 的变量内存地址,所以相等。
// 这里的内存地址,就可以理解为上面的 instance 。
// 创建了1个内存地址就可以理解为上面的 new SingleTonUser();
// 虽然 new SingleTonUser() 执行了两次,但是返回的是一个 instance 。

process.nextTick

process.nextTick(fn)

从字面意思理解就是 流程.下一步(方法)

会在宏任务执行完后立即执行里面的方法。

运行顺序

macroTask => process.nextTick => microTask

例子

// setTimeout
setTimeout(() => console.log('setTimeout'));

// Promise
Promise.resolve()
  .then(() => Promise.resolve(1))
  .then(() => Promise.resolve(2))
  .then(() => Promise.resolve(3))
  .then(() => Promise.resolve(4))
  .then((val) => console.log('promise val:' + val));

// 同步
console.log('同步');

// nextTick
process.nextTick(() => console.log('process.nextTick'));
优先级如下
// 同步
// process.nextTick
// promise val:4
// setTimeout