跳到主要内容

详解 微任务、宏任务、异步阻塞、事件循环、单例模式

本文适合前端进阶,从简单 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