56. 为什么Promise要引入微任务?
解决效果
实现链式调用,解决多层嵌套问题实现错误冒泡后一站式处理,解决每次任务中判断错误、增加代码混乱度的问题
- 为什么Promise要引入微任务?
Promise 中的执行函数是同步进行的,但是里面存在着异步操作,在异步操作结束后会调用 resolve 方法,或者中途遇到错误调用 reject 方法,这两者都是作为微任务进入到 EventLoop 中。但是你有没有想
过,Promise 为什么要引入微任务的方式来进行回调操作?
解决方式
回到问题本身,其实就是如何处理回调的问题。总结起来有三种方式:
1)使用同步回调,直到异步任务进行完,再进行后面的任务。
2)使用异步回调,将回调函数放在进行宏任务队列的队尾。
3)使用异步回调,将回调函数放到当前宏任务中的最后面。
优劣对比
第一种方式显然不可取,因为同步的问题非常明显,会让整个脚本阻塞住,当前任务等待,后面的任务都无法得到执行,而这部分等待的时间是可以拿来完成其他事情的,导致 CPU 的利用率非常低,而且还
有另外一个致命的问题,就是无法实现延迟绑定的效果。
如果采用第二种方式,那么执行回调(resolve/reject)的时机应该是在前面所有的宏任务完成之后,倘若现在的任务队列非常长,那么回调迟迟得不到执行,造成应用卡顿。
为了解决上述方案的问题,另外也考虑到延迟绑定的需求,Promise 采取第三种方式, 即引入微任务, 即把 resolve(reject) 回调的执行放在当前宏任务的末尾。
这样,利用微任务解决了两大痛点:
采用异步回调替代同步回调解决了浪费 CPU 性能的问题。
放到当前宏任务最后执行,解决了回调执行的实时性问题。
好,Promise 的基本实现思想已经讲清楚了,相信大家已经知道了它为什么这么设计,接下来就让我们一步步弄清楚它内部到底是怎么设计的。
- Promise 如何实现链式调用?
简易版实现
首先写出第一版的代码:
//定义三种状态
self.error = null;
self.status = PENDING;
self.onFulfilled = null; //成功的回调函数 self.onRejected = null; //失败的回调函数
const resolve = (value) => { if(self.status !== PENDING) return;
setTimeout(() => { self.status = FULFILLED;
self.value = value;
self.onFulfilled(self.value);//resolve时执行成功回调 });
};
const reject = (error) => { if(self.status !== PENDING) return;
setTimeout(() => { self.status = REJECTED;
self.error = error;
self.onRejected(self.error);//resolve时执行成功回调 });
};
executor(resolve, reject);
} MyPromise.prototype.then = function(onFulfilled, onRejected) {
if (this.status === PENDING) { this.onFulfilled = onFulfilled;
this.onRejected = onRejected;
} else if (this.status === FULFILLED) { //如果状态是fulfilled,直接执行成功回调,并将成功值传入
onFulfilled(this.value)
} else { //如果状态是rejected,直接执行失败回调,并将失败原因传入
onRejected(this.error)
} return this;
}
可以看到,Promise 的本质是一个有限状态机,存在三种状态:
PENDING(等待)
FULFILLED(成功)
REJECTED(失败)
状态改变规则如下图:

对于 Promise 而言,状态的改变不可逆,即由等待态变为其他的状态后,就无法再改变了。
不过,回到目前这一版的 Promise, 还是存在一些问题的。
设置回调数组
首先只能执行一个回调函数,对于多个回调的绑定就无能为力,比如下面这样:
let promise1 = new MyPromise((resolve, reject) => { fs.readFile('./001.txt', (err, data) => {
if(!err){ resolve(data);
}else { reject(err);
} })
});
let x1 = promise1.then(data => { console.log("第一次展示", data.toString());
});
let x2 = promise1.then(data => { console.log("第二次展示", data.toString());
});
let x3 = promise1.then(data => { console.log("第三次展示", data.toString());
});
这里绑定了三个回调,想要在 resolve() 之后一起执行,那怎么办呢?
需要将 onFulfilled 和 onRejected 改为数组,调用 resolve 时将其中的方法拿出来一一执行即可。
self.onFulfilledCallbacks = [];
self.onRejectedCallbacks = [];
MyPromise.prototype.then = function(onFulfilled, onRejected) { if (this.status === PENDING) {
this.onFulfilledCallbacks.push(onFulfilled);
return this;
}
接下来将 resolve 和 reject 方法中执行回调的部分进行修改:
// resolve 中 self.onFulfilledCallbacks.forEach((callback) => callback(self.value));
//reject 中 self.onRejectedCallbacks.forEach((callback) => callback(self.error));
链式调用完成
我们采用目前的代码来进行测试:
let fs = require('fs');
let readFilePromise = (filename) => { return new MyPromise((resolve, reject) => {
fs.readFile(filename, (err, data) => { if(!err){
resolve(data);
}else { reject(err);
} })
})
} readFilePromise('./001.txt').then(data => {
console.log(data.toString());
return readFilePromise('./002.txt');
}).then(data => { console.log(data.toString());
})
// 001.txt的内容 // 001.txt的内容
咦?怎么打印了两个 001,第二次不是读的 002 文件吗?
问题出在这里:
MyPromise.prototype.then = function(onFulfilled, onRejected) { //...
return this;
}
这么写每次返回的都是第一个 Promise。then 函数当中返回的第二个 Promise 直接被无视了!
说明 then 当中的实现还需要改进, 我们现在需要对 then 中返回值重视起来。
try { // 看到了吗?要拿到 then 中回调返回的结果。
let x = onFulfilled(value);
resolve(x);
} catch (e) { reject(e);
} });
self.onRejectedCallbacks.push((error) => { try {
let x = onRejected(error);
resolve(x);
} catch (e) { reject(e);
} });
});
} //...
}
假若当前状态为 PENDING,将回调数组中添加如上的函数,当 Promise 状态变化后,会遍历相应回调数组并执行回调。
但是这段程度还是存在一些问题:
1)首先 then 中的两个参数不传的情况并没有处理,
2)假如 then 中的回调执行后返回的结果(也就是上面的x)是一个 Promise, 直接给 resolve 了,这是我们不希望看到的。
怎么来解决这两个问题呢?
先对参数不传的情况做判断:
// 成功回调不传给它一个默认函数 onFulfilled = typeof onFulfilled === "function" ? onFulfilled : value => value;
// 对于失败回调直接抛错 onRejected = typeof onRejected === "function" ? onRejected : error => { throw
error };
然后对返回Promise的情况进行处理:
function resolvePromise(bridgePromise, x, resolve, reject) { //如果x是一个promise
if (x instanceof MyPromise) { // 拆解这个 promise,直到返回值不为 promise 为止
if (x.status === PENDING) { x.then(y => {
resolvePromise(bridgePromise, y, resolve, reject);
}, error => {
resolve(x);
} }
然后在 then 的方法实现中作如下修改:
resolve(x)->resolvePromise(bridgePromise, x, resolve, reject);
在这里大家好好体会一下拆解 Promise 的过程,其实不难理解,要强调的是其中的递归调用始终传入的 resolve和reject这两个参数是什么含义,其实他们控制的是最开始传入的bridgePromise的状态,
这一点非常重要。
紧接着,我们实现一下当 Promise 状态不为 PENDING 时的逻辑。
成功状态下调用then:
if (self.status === FULFILLED) { return bridgePromise = new MyPromise((resolve, reject) => {
try { // 状态变为成功,会有相应的 self.value
let x = onFulfilled(self.value);
// 暂时可以理解为 resolve(x),后面具体实现中有拆解的过程 resolvePromise(bridgePromise, x, resolve, reject);
} catch (e) { reject(e);
} })
}
失败状态下调用then:
if (self.status === REJECTED) { return bridgePromise = new MyPromise((resolve, reject) => {
try { // 状态变为失败,会有相应的 self.error
let x = onRejected(self.error);
resolvePromise(bridgePromise, x, resolve, reject);
} catch (e) { reject(e);
} });
}
Promise A+中规定成功和失败的回调都是微任务,由于浏览器中 JS 触碰不到底层微任务的分配,可以直接拿 setTimeout (属于宏任务的范畴) 来模拟,用 setTimeout将需要执行的任务包裹,当然,上面
的 resolve 实现也是同理, 大家注意一下即可,其实并不是真正的微任务。
//...
})
}
if (self.status === REJECTED) { return bridgePromise = new MyPromise((resolve, reject) => {
setTimeout(() => { //...
})
}
好了,到这里, 我们基本实现了 then 方法,现在我们拿刚刚的测试代码做一下测试, 依次打印如下:
001.txt的内容 002.txt的内容
可以看到,已经可以顺利地完成链式调用。
错误捕获及冒泡机制分析
现在来实现 catch 方法:
Promise.prototype.catch = function (onRejected) { return this.then(null, onRejected);
}
对,就是这么几行,catch 原本就是 then 方法的语法糖。
相比于实现来讲,更重要的是理解其中错误冒泡的机制,即中途一旦发生错误,可以在最后用 catch 捕获错误。
我们回顾一下 Promise 的运作流程也不难理解,贴上一行关键的代码:
// then 的实现中 onRejected = typeof onRejected === "function" ? onRejected : error => { throw
error };
一旦其中有一个PENDING状态的 Promise 出现错误后状态必然会变为失败, 然后执行 onRejected函数,而这个 onRejected 执行又会抛错,把新的 Promise 状态变为失败,新的 Promise 状态变为失败后
又会执行onRejected......就这样一直抛下去,直到用catch 捕获到这个错误,才停止往下抛。
这就是 Promise 的错误冒泡机制。
至此,Promise 三大法宝: 回调函数延迟绑定、回调返回值穿透和错误冒泡。
全站分卷索引 · 147 卷
- 1HTML
- 22. canvas 相关
- 36. 说说 title 和 alt 属性
- 42. 浮动布局的优点?有什么缺点?清除浮动有哪些方式?
- 53. 使用display:inline-block会产生什么问题?解决方法?
- 64. 布局题:div垂直居中,左右10px,高度始终为宽度一半
- 75. 盒模型
- 87. CSS如何进行圣杯布局
- 98. CSS如何实现双飞翼布局?
- 109. 什么是BFC?
- 1112. 应用场景
- 1213. 画一个对话框
- 1314. 画一个平行四边形
- 1415. 用一个div画五角星
- 15JavaScript
- 163. null是对象吗?为什么?
- 178. 如何创建并使用BigInt?
- 189. typeof 是否能正确判断类型?
- 1910. instanceof能否判断基本数据类型?
- 2012. Object.is和===的区别?
- 2115. == 和 ===有什么区别?
- 2217. 如何让if(a == 1 && a == 2)条件成立?
- 2320. 闭包有哪些表现形式?
- 2421. 如何解决下面的循环输出问题?
- 2522. 原型对象和构造函数有何关系?
- 2623. 能不能描述一下原型链?
- 2725. 函数的arguments为什么不是数组?如何转化成数组?
- 2826. forEach中return有效果吗?如何中断forEach循环?
- 2928. JS中flat---数组扁平化
- 3029. 什么是高阶函数
- 3131. 能不能实现数组map方法 ?
- 3232. 能不能实现数组reduce方法 ?
- 3333. 能不能实现数组 push、pop 方法 ?
- 3434. 能不能实现数组filter方法 ?
- 3536. 能不能实现数组sort方法?
- 3637. 能不能模拟实现一个new的效果?
- 3739. 能不能实现一个 call/apply 函数?
- 3840. 谈谈你对JS中this的理解
- 3941. JS中浅拷贝的手段有哪些?
- 4042. 能不能写一个完整的深拷贝?
- 4143. 数据是如何存储的?
- 4244. V8 引擎如何进行垃圾内存的回收?
- 4345. 描述一下 V8 执行一段JS代码的过程?
- 4446. 宏任务(MacroTask)引入
- 4547. 微任务(MicroTask)引入
- 4649. 理解EventLoop:nodejs
- 4750. nodejs 和 浏览器关于eventLoop的主要区别
- 4853. JS异步编程有哪些方案?为什么会出现这些方案?
- 4954. 能不能简单实现一下 node 中回调函数的机制?
- 5055. Promise 凭借什么消灭了回调地狱?
- 5156. 为什么Promise要引入微任务?
- 5258. 实现Promise的 resolve、reject 和 finally
- 5359. 现Promise的 all 和 race
- 5460. 谈谈你对生成器以及协程的理解
- 5561. 如何让 Generator 的异步代码按顺序执行完毕?
- 5662. 解释一下async/await的运行机制。
- 5763. forEach 中用 await 会产生什么问题?怎么解决这个问题?
- 5864. 关于JS中一些重要的api实现
- 5965. 事件流向
- 602. HTTP有哪些请求方法?
- 615. 如何理解 HTTP 状态码?
- 626. 简要概括一下 HTTP 的特点?HTTP 有哪些缺点?
- 637. 对 Accept 系列字段了解多少?
- 648. 对于定长和不定长的数据,HTTP 是怎么传输的?
- 659. HTTP 如何处理大文件的传输?
- 6610. HTTP 中如何处理表单数据的提交?
- 6711. HTTP1.1 如何解决 HTTP 的队头阻塞问题?
- 6812. 对 Cookie 了解多少?
- 6913. 如何理解 HTTP 代理?
- 7014. 如何理解 HTTP 缓存及缓存代理?
- 7117. 客户端的缓存控制
- 7219. 传统 RSA 握手
- 7320. RSA 和 ECDHE 握手过程的区别
- 7422. HTTP/2 有哪些改进?
- 7523. HTTP/2 中的二进制帧是如何设计的?
- 76TCP协议
- 771. 能不能说一说 TCP 和 UDP 的区别?
- 783. 为什么是三次而不是两次、四次?
- 794. 三次握手过程中可以携带数据么?
- 806. 说说 TCP 四次挥手的过程
- 817. 为什么是四次挥手而不是三次?
- 829. 说说半连接队列和 SYN Flood 攻击的关系
- 8310. 如何应对 SYN Flood 攻击?
- 8412. 说说 TCP 快速打开的原理(TFO)
- 8513. 能不能说说TCP报文中时间戳的作用?
- 8614. TCP 的超时重传时间是如何计算的?
- 8715. 能不能说一说 TCP 的流量控制?
- 8816. 能不能说说 TCP 的拥塞控制?
- 8917. 能不能说说 Nagle 算法和延迟确认?
- 9018. 如何理解 TCP 的 keep-alive?
- 911. 能不能说一说浏览器缓存?
- 922. 能不能说一说浏览器的本地存储?各自优劣如何?
- 933. 说一说从输入URL到页面呈现发生了什么?(网络)
- 944. 说一说从输入URL到页面呈现发生了什么?(解析算法)
- 955. 说一说从输入URL到页面呈现发生了什么?(渲染过程)
- 966. 谈谈你对重绘和回流的理解
- 977. 能不能说一说XSS攻击?
- 988. 能不能说一说CSRF攻击?
- 999. HTTPS为什么让数据传输更安全?
- 10010. 能不能实现事件的防抖和节流?
- 10111. 能不能实现图片懒加载?
- 102Vue
- 1032. mvvm和mvc区别?它和其它框架(jquery)的区别是什么?哪些场景适合?
- 1045. 描述下 vue 从初始化页面--修改数据--刷新页面 UI 的过程?
- 1057. 虚拟 DOM 实现原理
- 10610. Vue 组件间通信有哪些方式?
- 10714. route 和 router 的区别是什么?
- 10816. Vue 的 nextTick 的原理是什么?
- 10919. vuex
- 1101. 简单的反转链表
- 1112. 区间反转
- 1123. 两个一组翻转链表
- 1134. K个一组翻转链表
- 1145. 如何检测链表形成环?
- 1156. 如何找到环的起点
- 1167. 合并两个有序链表
- 1178. 合并 K 个有序链表
- 1189. 判断回文链表
- 119栈和队列
- 1203. 普通的层次遍历
- 1214. 二叉树的锯齿形层次遍历
- 1225. 二叉树的右视图
- 1236. 完全平方数
- 1247. 单词接龙
- 1258. 优先队列
- 1269. 关于堆的说明
- 12711. 实现优先队列
- 12812. 前 K 个高频元素
- 12913. 合并 K 个排序链表
- 13014. 什么是双端队列?
- 13116. 栈实现队列
- 13217. 队列实现栈
- 133二叉树
- 1342. 中序遍历
- 1353. 后序遍历
- 1364. 最大深度
- 1375. 最小深度
- 1386. 对称二叉树
- 1397. 二叉树的最近公共祖先
- 1408. 二叉搜索树的最近公共祖先
- 1419. 二叉树的直径
- 14210. 二叉树的所有路径
- 14311. 二叉树的最大路径和
- 14412. 验证二叉搜索树
- 14513. 将有序数组转换为二叉搜索树
- 14614. 二叉树展开为链表
- 14715. 不同的二叉搜索树II