详解 微任务、宏任务、异步阻塞、事件循环、单例模式
本文适合前端进阶,从简单 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