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