摘要:不同的的实现需要可以相互调用,搞清楚了标准之后,开始动手吧构造函数产生一个对象有很多种方法,构造函数是看起来最面向对象的一种,而且原生实现也是使用的构造函数,因此我也决定使用构造函数的方法。
-- What i can"t create, i don"t understant
前言实现Promise的目的是为了深入的理解Promies,以在项目中游刃有余的使用它。完整的代码见gitHub
Promise标准Promise的标准有很多个版本,本文采用ES6原生Promise使用的Promise/A+标准。完整的Promise/A+标准见这里,总结如下:
promise具有状态state(status),状态分为pending, fulfilled(我比较喜欢叫做resolved), rejected。初始为pending,一旦状态改变,不能再更改为其它状态。当promise为fulfilled时,具有value;当promise为rejected时,具有reason;value和reason都是一旦确定,不能改变的。
promise具有then方法,注意了,只有then方法是必须的,其余常用的catch,race,all,resolve等等方法都不是必须的,其实这些方法都可以用then方便的实现。
不同的promise的实现需要可以相互调用
OK,搞清楚了promise标准之后,开始动手吧
Promise构造函数产生一个对象有很多种方法,构造函数是看起来最面向对象的一种,而且原生Promise实现也是使用的构造函数,因此我也决定使用构造函数的方法。
首先,先写一个大概的框架出来:
// 总所周知,Promise传入一个executor,有两个参数resolve, reject,用来改变promise的状态 function Promise(executor) { this.status = "pending" this.value = void 0 // 为了方便把value和reason合并 const resolve = function() {} const reject = function() {} executor(resolve, reject) }
很明显,这个构造函数还有很多问题们一个一个来看
resolve和reject并没有什么卵用。
首先,用过promise的都知道,resolve和reject是用来改变promise的状态的:
function Promise(executor) { this.status = "pending" this.value = void 0 // 为了方便把value和reason合并 const resolve = value => { this.value = value this.status = "resolved" } const reject = reason => { this.value = reason this.status = "rejected" } executor(resolve, reject) }
然后,当resolve或者reject调用的时候,需要执行在then方法里传入的相应的函数(通知)。有没有觉得这个有点类似于事件(发布-订阅模式)呢?
function Promise(executor) { this.status = "pending" this.value = void 0 // 为了方便把value和reason合并 this.resolveListeners = [] this.rejectListeners = [] // 通知状态改变 const notify(target, val) => { target === "resolved" ? this.resolveListeners.forEach(cb => cb(val)) : this.rejectListeners.forEach(cb => cb(val)) } const resolve = value => { this.value = value this.status = "resolved" notify("resolved", value) } const reject = reason => { this.value = reason this.status = "rejected" notify("rejected", reason) } executor(resolve, reject) }
status和value并没有做到一旦确定,无法更改。这里有两个问题,一是返回的对象暴露了status和value属性,并且可以随意赋值;二是如果在executor里多次调用resolve或者reject,会使value更改多次。
第一个问题,如何实现只读属性:
function Promise(executor) { if (typeof executor !== "function") { throw new Error("Promise executor must be fucntion") } let status = "pending" // 闭包形成私有属性 let value = void 0 ...... // 使用status代替this.value const resolve = val => { value = val status = "resolved" notify("resolved", val) } const reject = reason => { value = reason status = "rejected" notify("rejected", reason) } // 通过getter和setter设置只读属性 Object.defineProperty(this, "status", { get() { return status }, set() { console.warn("status is read-only") } }) Object.defineProperty(this, "value", { get() { return value }, set() { console.warn("value is read-only") } })
第二个问题,避免多次调用resolve、reject时改变value,而且标准里(2.2.2.3 it must not be called more than once)也有规定,then注册的回调只能执行一次。
const resolve = val => { if (status !== "pending") return // 避免多次运行 value = val status = "resolved" notify("resolved", val) }
then注册的回调需要异步执行。
说到异步执行,对原生Promise有了解的同学都知道,then注册的回调在Micro-task中,并且调度策略是,Macro-task中执行一个任务,清空所有Micro-task的任务。简而言之,promise异步的优先级更高。
其实,标准只规定了promise回调需要异步执行,在一个“干净的”执行栈执行,并没有规定一定说要用micro-task,并且在低版本浏览器中,并没有micro-task队列。不过在各种promise的讨论中,由于原生Promise的实现,micro-task已经成成为了事实标准,而且promise回调在micro-task中也使得程序的行为更好预测。
在浏览器端,可以用MutationObserver实现Micro-task。本文利用setTimeout来简单实现异步。
const resolve = val => { if (val instanceof Promise) { return val.then(resolve, reject) } // 异步执行 setTimeout(() => { if (status !== "pending") return status = "resolved" value = val notify("resolved", val) }, 0) }
最后,加上错误处理,就得到了一个完整的Promise构造函数:
function Promise(executor) { if (typeof executor !== "function") { throw new Error("Promise executor must be fucntion") } let status = "pending" let value = void 0 const notify = (target, val) => { target === "resolved" ? this.resolveListeners.forEach(cb => cb(val)) : this.rejectListeners.forEach(cb => cb(val)) } const resolve = val => { if (val instanceof Promise) { return val.then(resolve, reject) } setTimeout(() => { if (status !== "pending") return status = "resolved" value = val notify("resolved", val) }, 0) } const reject = reason => { setTimeout(() => { if (status !== "pending") return status = "rejected" value = reason notify("rejected", reason) }, 0) } this.resolveListeners = [] this.rejectListeners = [] Object.defineProperty(this, "status", { get() { return status }, set() { console.warn("status is read-only") } }) Object.defineProperty(this, "value", { get() { return value }, set() { console.warn("value is read-only") } }) try { executor(resolve, reject) } catch (e) { reject(e) } }
总的来说,Promise构造函数其实只干了一件事:执行传入的executor,并构造了executor的两个参数。
实现then方法首先需要确定的是,then方法是写在构造函数里还是写在原型里。
写在构造函数了里有一个比较大的好处:可以像处理status和value一样,通过闭包让resolveListeners和rejectListeners成为私有属性,避免通过this.rejectListeners来改变它。
写在构造函数里的缺点是,每一个promise对象都会有一个不同的then方法,这既浪费内存,又不合理。我的选择是写在原型里,为了保持和原生Promise有一样的结构和接口。
ok,还是先写一个大概的框架:
Promise.prototype.then = function (resCb, rejCb) { this.resolveListeners.push(resCb) this.rejectListeners.push(rejCb) return new Promise() }
随后,一步一步的完善它:
then方法返回的promise需要根据resCb或rejCb的运行结果来确定状态。
Promise.prototype.then = function (resCb, rejCb) { return new Promise((res, rej) => { this.resolveListeners.push((val) => { try { const x = resCb(val) res(x) // 以resCb的返回值为value来resolve } catch (e) { rej(e) // 如果出错,返回的promise以异常为reason来reject } }) this.rejectListeners.push((val) => { try { const x = rejCb(val) res(x) // 注意这里也是res而不是rej哦 } catch (e) { rej(e) // 如果出错,返回的promise以异常为reason来reject } }) }) }
ps:众所周知,promise可以链式调用,说起链式调用,我的第一个想法就是返回this就可以了,但是then方法不可以简单的返回this,而要返回一个新的promise对象。因为promise的状态一旦确定就不能更改,而then方法返回的promise的状态需要根据then回调的运行结果来决定。
如果resCb/rejCb返回一个promiseA,then返回的promise需要跟随(adopt)promiseA,也就是说,需要保持和promiseA一样的status和value。
this.resolveListeners.push((val) => { try { const x = resCb(val) if (x instanceof Promise) { x.then(res, rej) // adopt promise x } else { res(x) } } catch (e) { rej(e) } }) this.rejectListeners.push((val) => { try { const x = resCb(val) if (x instanceof Promise) { x.then(res, rej) // adopt promise x } else { res(x) } } catch (e) { rej(e) } })
如果then的参数不是函数,需要忽略它,类似于这种情况:
new Promise(rs => rs(5)) .then() .then(console.log)
其实就是把value和状态往后传递
this.resolveListeners.push((val) => { if (typeof resCb !== "function") { res(val) return } try { const x = resCb(val) if (x instanceof Promise) { x.then(res, rej) // adopt promise x } else { res(x) } } catch (e) { rej(e) } }) // rejectListeners也是相同的逻辑
如果调用then时, promise的状态已经确定,相应的回调直接运行
// 注意这里需要异步 if (status === "resolved") setTimeout(() => resolveCb(value), 0) if (status === "rejected") setTimeout(() => rejectCb(value), 0)
最后,就得到了一个完整的then方法,总结一下,then方法干了两件事,一是注册了回调,二是返回一个新的promise对象。
// resolveCb和rejectCb是相同的逻辑,封装成一个函数 const thenCallBack = (cb, res, rej, target, val) => { if (typeof cb !== "function") { target === "resolve" ? res(val) : rej(val) return } try { const x = cb(val) if (x instanceof Promise) { x.then(res, rej) // adopt promise x } else { res(x) } } catch (e) { rej(e) } } Promise.prototype.then = function (resCb, rejCb) { const status = this.status const value = this.value let thenPromise thenPromise = new Promise((res, rej) => { /** * 这里不能使用bind来实现柯里画,规范里规定了: * 2.2.5: onFulfilled and onRejected must be called as functions (i.e. with no this value)) */ const resolveCb = val => { thenCallBack(resCb, res, rej, "resolve", val) } const rejectCb = val => { thenCallBack(rejCb, res, rej, "reject", val) } if (status === "pending") { this.resolveListeners.push(resolveCb) this.rejectListeners.push(rejectCb) } if (status === "resolved") setTimeout(() => resolveCb(value), 0) if (status === "rejected") setTimeout(() => rejectCb(value), 0) }) return thenPromise }不同的Promise实现可以互相调用
首先要明白的是什么叫互相调用,什么情况下会互相调用。之前实现then方法的时候,有一条规则是:如果then方法的回调返回一个promiseA。then返回的promise需要adopt这个promiseA,也就是说,需要处理这种情况:
new MyPromise(rs => rs(5)) .then(val => { return Promise.resolve(5) // 原生Promise }) .then(val => { return new Bluebird(r => r(5)) // Bluebird的promise })
关于这个,规范里定义了一个叫做The Promise Resolution Procedure的过程,我们需要做的就是把规范翻译一遍,并替代代码中判断promise的地方
const resolveThenable = (promise, x, resolve, reject) => { if (x === promise) { return reject(new TypeError("chain call found")) } if (x instanceof Promise) { return x.then(v => { resolveThenable(promise, v, resolve, reject) }, reject) } if (x === null || (typeof x !== "object" && typeof x !== "function")) { return resolve(x) } let called = false try { // 这里有一个有意思的技巧。标准里解释了,如果then是一个getter,那么通过赋值可以保证getter只被触发一次,避免副作用 const then = x.then if (typeof then !== "function") { return resolve(x) } then.call(x, v => { if (called) return called = true resolveThenable(promise, v, resolve, reject) }, r => { if (called) return called = true reject(r) }) } catch (e) { if (called) return reject(e) } }
到这里,一个符合标准的Promise就完成了,完整的代码如下:
function Promise(executor) { if (typeof executor !== "function") { throw new Error("Promise executor must be fucntion") } let status = "pending" let value = void 0 const notify = (target, val) => { target === "resolved" ? this.resolveListeners.forEach(cb => cb(val)) : this.rejectListeners.forEach(cb => cb(val)) } const resolve = val => { if (val instanceof Promise) { return val.then(resolve, reject) } setTimeout(() => { if (status !== "pending") return status = "resolved" value = val notify("resolved", val) }, 0) } const reject = reason => { setTimeout(() => { if (status !== "pending") return status = "rejected" value = reason notify("rejected", reason) }, 0) } this.resolveListeners = [] this.rejectListeners = [] Object.defineProperty(this, "status", { get() { return status }, set() { console.warn("status is read-only") } }) Object.defineProperty(this, "value", { get() { return value }, set() { console.warn("value is read-only") } }) try { executor(resolve, reject) } catch (e) { reject(e) } } const thenCallBack = (cb, res, rej, target, promise, val) => { if (typeof cb !== "function") { target === "resolve" ? res(val) : rej(val) return } try { const x = cb(val) resolveThenable(promise, x, res, rej) } catch (e) { rej(e) } } const resolveThenable = (promise, x, resolve, reject) => { if (x === promise) { return reject(new TypeError("chain call found")) } if (x instanceof Promise) { return x.then(v => { resolveThenable(promise, v, resolve, reject) }, reject) } if (x === null || (typeof x !== "object" && typeof x !== "function")) { return resolve(x) } let called = false try { // 这里有一个有意思的技巧。标准里解释了,如果then是一个getter,那么通过赋值可以保证getter只被触发一次,避免副作用 const then = x.then if (typeof then !== "function") { return resolve(x) } then.call(x, v => { if (called) return called = true resolveThenable(promise, v, resolve, reject) }, r => { if (called) return called = true reject(r) }) } catch (e) { if (called) return reject(e) } } Promise.prototype.then = function (resCb, rejCb) { const status = this.status const value = this.value let thenPromise thenPromise = new Promise((res, rej) => { const resolveCb = val => { thenCallBack(resCb, res, rej, "resolve", thenPromise, val) } const rejectCb = val => { thenCallBack(rejCb, res, rej, "reject", thenPromise, val) } if (status === "pending") { this.resolveListeners.push(resolveCb) this.rejectListeners.push(rejectCb) } if (status === "resolved") setTimeout(() => resolveCb(value), 0) if (status === "rejected") setTimeout(() => rejectCb(value), 0) }) return thenPromise }
测试脚本
关于promise的一些零散知识Promise.resolve就是本文所实现的resolveThenable,并不是简单的用来返回一个resolved状态的函数,它返回的promise对象的状态也并不一定是resolved。
promise.then(rs, rj)和promise.then(rs).catch(rj)是有区别的,区别在于当rs出错时,后一种方法可以进行错误处理。
感想与总结实现Promise的过程其实并没有我预想的那么难,所谓的Promise的原理我感觉就是类似于观察者模式,so,不要有畏难情绪,我上我也行^_^。
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/97944.html
摘要:今天我们来自己手写一个符合规范的库。是异步编程的一种解决方案,比传统的解决方案回调函数和事件更合理和更强大。我们可以看到,其实就是一个构造函数。所以说我们的数组里存的是一个一个的的回调函数,也就是一个一个。 今天我们来自己手写一个符合PromiseA+规范的Promise库。大家是不是很激动呢?? showImg(https://segmentfault.com/img/bV6t4Z?...
摘要:在将来的其他规范中可能会涉及这些没有提及的内容。它禁止被触发多次。如果到了状态,那么所有的回调函数都必须按照他们原有的顺序进行调用执行。 概述 自从准备晋级之后,就拖更了很久了,既然晋级弄完了,那么也恢复更新了。 在面试别人的过程中,发现基本上没有人对整个Promise完全了解,因此希望通过这篇文章来帮助大家了解下Promise的全貌。本文的主要内容是Promise/A+规范的译文,主...
摘要:前端开发中的中的中,是一个构造函数,通过它创建一个错误对象。是核心对象,表示调用一个时发生的异常。将回调函数包裹一层接下来可以将统一进行处理。中的错误捕获在以前,可以使用来处理捕获的错误。研究结果在这里中的错误捕获的源码中,在关 本文首发于公众号:符合预期的CoyPan 写在前面 在前端项目中,由于JavaScript本身是一个弱类型语言,加上浏览器环境的复杂性,网络问题等等,很容易...
摘要:结果证明,对于以上浏览器,在生产环境使用是可行的。后面可以跟对象,表示等待才会继续向下执行,如果被或抛出异常则会被外面的捕获。,,都是现在和未来解决异步的标准做法,可以完美搭配使用。这也是使用标准一大好处。只允许外部传入成功或失败后的回调。 showImg(https://cloud.githubusercontent.com/assets/948896/10188666/bc9a53...
摘要:换句话说该静态函数返回个处于状态的对象。等价于构造函数的静态函数,创建一个对象并以值作为参数调用句柄函数。等价于介绍构造函数的静态函数,参数是对象组成的可迭代数据集合。 一、概述 ES2015 Promise函数是对PromiseA+标准的实现,并且严格遵守该标准。 二、APIs 2.1 创建Promise对象 Promise构造函数的参数是个包含两个参数的函数,并且该函数的参数分别对...
阅读 717·2023-04-25 16:55
阅读 2772·2021-10-11 10:59
阅读 2039·2021-09-09 11:38
阅读 1741·2021-09-03 10:40
阅读 1455·2019-08-30 15:52
阅读 1068·2019-08-30 15:52
阅读 904·2019-08-29 15:33
阅读 3423·2019-08-29 11:26