您现在的位置是:网站首页> 编程资料编程资料
Promise+async+Generator的实现原理_javascript技巧_
2023-05-24
406人已围观
简介 Promise+async+Generator的实现原理_javascript技巧_
前言
笔者刚接触async/await时,就被其暂停执行的特性吸引了,心想在没有原生API支持的情况下,await居然能挂起当前方法,实现暂停执行,我感到十分好奇。好奇心驱使我一层一层剥开有关JS异步编程的一切。阅读完本文,读者应该能够了解:
Promise的实现原理async/await的实现原理Generator的实现原理
在成文过程中,笔者查阅了很多讲解Promise实现的文章,但感觉大多文章都很难称得上条理清晰,有的上来就放大段Promise规范翻译,有的在Promise基础使用上浪费篇幅,又或者把一个简单的东西长篇大论,过度讲解,我推荐头铁的同学直接拉到本章小结看最终实现,结合着注释直接啃代码也能理解十之八九
回归正题,文章开头我们先点一下Promise为我们解决了什么问题:在传统的异步编程中,如果异步之间存在依赖关系,我们就需要通过层层嵌套回调来满足这种依赖,如果嵌套层数过多,可读性和可维护性都变得很差,产生所谓“回调地狱”,而Promise将回调嵌套改为链式调用,增加可读性和可维护性。下面我们就来一步步实现一个Promise:
1. 观察者模式
我们先来看一个最简单的Promise使用:
const p1 = newPromise((resolve, reject) => { setTimeout(() => { resolve('result') }, 1000); }) p1.then(res =>console.log(res), err => console.log(err))观察这个例子,我们分析Promise的调用流程:
Promise的构造方法接收一个executor(),在new Promise()时就立刻执行这个executor回调executor()内部的异步任务被放入宏/微任务队列,等待执行then()被执行,收集成功/失败回调,放入成功/失败队列executor()的异步任务被执行,触发resolve/reject,从成功/失败队列中取出回调依次执行
其实熟悉设计模式的同学,很容易就能意识到这是个**「观察者模式」**,这种收集依赖 -> 触发通知 -> 取出依赖执行 的方式,被广泛运用于观察者模式的实现,在Promise里,执行顺序是then收集依赖 -> 异步触发resolve -> resolve执行依赖。依此,我们可以勾勒出Promise的大致形状:
class MyPromise { // 构造方法接收一个回调 constructor(executor) { this._resolveQueue = [] // then收集的执行成功的回调队列 this._rejectQueue = [] // then收集的执行失败的回调队列 // 由于resolve/reject是在executor内部被调用, 因此需要使用箭头函数固定this指向, 否则找不到this._resolveQueue let _resolve = (val) => { // 从成功队列里取出回调依次执行 while(this._resolveQueue.length) { const callback = this._resolveQueue.shift() callback(val) } } // 实现同resolve let _reject = (val) => { while(this._rejectQueue.length) { const callback = this._rejectQueue.shift() callback(val) } } // new Promise()时立即执行executor,并传入resolve和reject executor(_resolve, _reject) } // then方法,接收一个成功的回调和一个失败的回调,并push进对应队列 then(resolveFn, rejectFn) { this._resolveQueue.push(resolveFn) this._rejectQueue.push(rejectFn) } }写完代码我们可以测试一下:
const p1 = new MyPromise((resolve, reject) => { setTimeout(() => { resolve('result') }, 1000); }) p1.then(res =>console.log(res)) //一秒后输出result我们运用观察者模式简单的实现了一下then和resolve,使我们能够在then方法的回调里取得异步操作的返回值,但我们这个Promise离最终实现还有很长的距离,下面我们来一步步补充这个Promise:
2. Promise A+规范
上面我们已经简单地实现了一个超低配版Promise,但我们会看到很多文章和我们写的不一样,他们的Promise实现中还引入了各种状态控制,这是由于ES6的Promise实现需要遵循Promise/A+规范,是规范对Promise的状态控制做了要求。Promise/A+的规范比较长,这里只总结两条核心规则:
❝
Promise本质是一个状态机,且状态只能为以下三种:
Pending(等待态)、Fulfilled(执行态)、Rejected(拒绝态),状态的变更是单向的,只能从Pending -> Fulfilled 或 Pending -> Rejected,状态变更不可逆
then方法接收两个可选参数,分别对应状态改变时触发的回调。then方法返回一个promise。then 方法可以被同一个 promise 调用多次。❞

根据规范,我们补充一下Promise的代码:
//Promise/A+规范的三种状态 const PENDING = 'pending' const FULFILLED = 'fulfilled' const REJECTED = 'rejected' class MyPromise { // 构造方法接收一个回调 constructor(executor) { this._status = PENDING // Promise状态 this._resolveQueue = [] // 成功队列, resolve时触发 this._rejectQueue = [] // 失败队列, reject时触发 // 由于resolve/reject是在executor内部被调用, 因此需要使用箭头函数固定this指向, 否则找不到this._resolveQueue let _resolve = (val) => { if(this._status !== PENDING) return// 对应规范中的"状态只能由pending到fulfilled或rejected" this._status = FULFILLED // 变更状态 // 这里之所以使用一个队列来储存回调,是为了实现规范要求的 "then 方法可以被同一个 promise 调用多次" // 如果使用一个变量而非队列来储存回调,那么即使多次p1.then()也只会执行一次回调 while(this._resolveQueue.length) { const callback = this._resolveQueue.shift() callback(val) } } // 实现同resolve let _reject = (val) => { if(this._status !== PENDING) return// 对应规范中的"状态只能由pending到fulfilled或rejected" this._status = REJECTED // 变更状态 while(this._rejectQueue.length) { const callback = this._rejectQueue.shift() callback(val) } } // new Promise()时立即执行executor,并传入resolve和reject executor(_resolve, _reject) } // then方法,接收一个成功的回调和一个失败的回调 then(resolveFn, rejectFn) { this._resolveQueue.push(resolveFn) this._rejectQueue.push(rejectFn) } }3. then的链式调用
补充完规范,我们接着来实现链式调用,这是Promise实现的重点和难点,我们先来看一下then是如何链式调用的:
const p1 = newPromise((resolve, reject) => { resolve(1) }) p1 .then(res => { console.log(res) //then回调中可以return一个Promise returnnewPromise((resolve, reject) => { setTimeout(() => { resolve(2) }, 1000); }) }) .then(res => { console.log(res) //then回调中也可以return一个值 return3 }) .then(res => { console.log(res) })输出:
1
2
3
我们思考一下如何实现这种链式调用:
- 显然
.then()需要返回一个Promise,这样才能找到then方法,所以我们会把then方法的返回值包装成Promise。 .then()的回调需要顺序执行,以上面这段代码为例,虽然中间return了一个Promise,但执行顺序仍要保证是1->2->3。我们要等待当前Promise状态变更后,再执行下一个then收集的回调,这就要求我们对then的返回值分类讨论
// then方法 then(resolveFn, rejectFn) { //return一个新的promise returnnewPromise((resolve, reject) => { //把resolveFn重新包装一下,再push进resolve执行队列,这是为了能够获取回调的返回值进行分类讨论 const fulfilledFn = value => { try { //执行第一个(当前的)Promise的成功回调,并获取返回值 let x = resolveFn(value) //分类讨论返回值,如果是Promise,那么等待Promise状态变更,否则直接resolve x instanceofPromise ? x.then(resolve, reject) : resolve(x) } catch (error) { reject(error) } } //把后续then收集的依赖都push进当前Promise的成功回调队列中(_rejectQueue), 这是为了保证顺序调用 this._resolveQueue.push(fulfilledFn) //reject同理 const rejectedFn = error => { try { let x = rejectFn(error) x instanceofPromise ? x.then(resolve, reject) : resolve(x) } catch (error) { reject(error) } } this._rejectQueue.push(rejectedFn) }) }然后我们就能测试一下链式调用:
const p1 = new MyPromise((resolve, reject) => { setTimeout(() => { resolve(1) }, 500); }) p1 .then(res => { console.log(res) return2 }) .then(res => { console.log(res) return3 }) .then(res => { console.log(res) }) //输出 1 2 34.值穿透 & 状态已变更的情况
我们已经初步完成了链式调用,但是对于 then() 方法,我们还要两个细节需要处理一下
- 「值穿透」:根据规范,如果 then() 接收的参数不是function,那么我们应该忽略它。如果没有忽略,当then()回调不为function时将会抛出异常,导致链式调用中断
- 「处理状态为resolve/reject的情况」:其实我们上边 then() 的写法是对应状态为
padding的情况,但是有些时候,resolve/reject 在 then() 之前就被执行(比如Promise.resolve().then()),如果这个时候还把then()回调push进resolve/reject的执行队列里,那么回调将不会被执行,因此对于状态已经变为fulfilled或rejected的情况,我们直接执行then回调:
// then方法,接收一个成功的回调和一个失败的回调 then(resolveFn, rejectFn) { // 根据规范,如果then的参数不是function,则我们需要忽略它, 让链式调用继续往下执行 typeof resolveFn !== 'function' ? resolveFn = value => value : null typeof rejectFn !== 'function' ? rejectFn = error => error : null // return一个新的promise returnnewPromise((resolve, reject) => { // 把resolveFn重新包装一下,再push进resolve执行队列,这是为了能够获取回调的返回值进行分类讨论 const fulfilledFn = value => { try { // 执行第一个(当前的)Promise的成功回调,并获取返回值 let x = resolveFn(value) // 分类讨论返回值,如果是Promise,那么等待Promise状态变更,否则直接resolve x instanceofPromise ? x.then(resolve, reject) : resolve(x) } catch (error) { reject(error) } } // reject同理 const rejectedFn = error => { try { let x = rejectFn(error) x instanceofPromise ? x.then(resolve, reject) : resolve(x) } catch (error) { reject(error) } } switch (this._status) { // 当状态为pending时,把then回调push进resolve/reject执行队列,等待执行 case PENDING: this._resolveQueue.push(fulfilledFn) this._rejectQueue.push(rejectedFn) break; // 当状态已经变为resolve/reject时,直接执行then回调 case FULFILLED: fulfilledFn(this._value) // this._value是上一个then回调return的值(见完整版代码) break; case REJECTED: rejectedFn(this._value) break; } }) }5.兼容同步任务
完成了then的链式调用以后,我们再处理一个前边的细节,然后放出完整代码。上文我们说过,Promise的执行顺序是new Promise -> then()收集回调 -> resolve/reject执行回调,这一顺序是建立在**「executor是异步任务」**的前提上的,如果executor是一个同步任务,那么顺序就会变成new Promise -> resolve/reject执行回调 -> then()收集回调,resolve的执行跑到then之前去了,为了兼容这种情况,我们给resolve/reject执行回调的操作包一个setTimeout,让它异步执行。
❝
这里插一句,有关这个setTimeout,其实还有一番学问。虽然规范没有要求回调应该被放进宏任务队列还是微任务队列,但其实Promise的默认实现是放进了微任务队列,我们的实现(包括大多数Promise手动实现和polyfill的转化)都是使用setTimeout放入了宏任务队列(当然我们也可以用MutationObserver模拟微任务)
❞
相关内容
- React 性能优化方法总结_React_
- 一文详解node.js有哪些全局对象呢_node.js_
- Node 文件查找优先级及 Require 方法文件查找策略_node.js_
- 如何处理elementUI中表格多选框禁用的问题_vue.js_
- vue3的ref、isRef、toRef、toRefs、toRaw详细介绍_vue.js_
- Vue路由配置方法详细介绍_vue.js_
- Vue首页界面加载优化实现方法详解_vue.js_
- Vue tagsview实现多页签导航功能流程详解_vue.js_
- js如何读取csv内容拼接成json_javascript技巧_
- React路由参数传递与嵌套路由的实现详细讲解_React_
点击排行
本栏推荐
