-- What i can't create, i don't understant

前言

实现Promise的目的是为了深入的理解Promies,以在项目中游刃有余的使用它。完整的代码见gitHub

Promise标准

Promise的标准有很多个版本,本文采用ES6原生Promise使用的Promise/A+标准。完整的Promise/A+标准见这里,总结如下:

  1. promise具有状态state(status),状态分为pending, fulfilled(我比较喜欢叫做resolved), rejected。初始为pending,一旦状态改变,不能再更改为其它状态。当promise为fulfilled时,具有value;当promise为rejected时,具有reason;value和reason都是一旦确定,不能改变的。
  2. promise具有then方法,注意了,只有then方法是必须的,其余常用的catch,race,all,resolve等等方法都不是必须的,其实这些方法都可以用then方便的实现。
  3. 不同的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)
}

很明显,这个构造函数还有很多问题们一个一个来看

  1. resolve和reject并没有什么卵用。

    首先,用过promise的都知道,resolve和reject是用来改变promise的状态的:

       function Promise(executor) {this.status = 'pending'this.value = void 0 // 为了方便把value和reason合并const resolve = value => {this.value = valuethis.status = 'resolved'}const reject = reason => {this.value = reasonthis.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 = valuethis.status = 'resolved'notify('resolved', value)}const reject = reason => {this.value = reasonthis.status = 'rejected'notify('rejected', reason)} executor(resolve, reject)}
    
  2. 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.valueconst resolve = val => {value = valstatus = 'resolved'notify('resolved', val)}const reject = reason => {value = reasonstatus = '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 = valstatus = 'resolved'notify('resolved', val)}
  3. 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') returnstatus = 'resolved'value = valnotify('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 0const 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') returnstatus = 'resolved'value = valnotify('resolved', val)}, 0)}const reject = reason => {setTimeout(() => {if (status !== 'pending') returnstatus = 'rejected'value = reasonnotify('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()
}

随后,一步一步的完善它:

  1. 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回调的运行结果来决定。

  2. 如果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)}
    })
    
  3. 如果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也是相同的逻辑
    
  4. 如果调用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.statusconst value = this.valuelet thenPromisethenPromise = 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 = falsetry {// 这里有一个有意思的技巧。标准里解释了,如果then是一个getter,那么通过赋值可以保证getter只被触发一次,避免副作用const then = x.thenif (typeof then !== 'function') {return resolve(x)}then.call(x, v => {if (called) returncalled = trueresolveThenable(promise, v, resolve, reject)}, r => {if (called) returncalled = truereject(r)})} catch (e) {if (called) returnreject(e)}
}

到这里,一个符合标准的Promise就完成了,完整的代码如下:

function Promise(executor) {if (typeof executor !== 'function') {throw new Error('Promise executor must be fucntion')}let status = 'pending'let value = void 0const 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') returnstatus = 'resolved'value = valnotify('resolved', val)}, 0)}const reject = reason => {setTimeout(() => {if (status !== 'pending') returnstatus = 'rejected'value = reasonnotify('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 = falsetry {// 这里有一个有意思的技巧。标准里解释了,如果then是一个getter,那么通过赋值可以保证getter只被触发一次,避免副作用const then = x.thenif (typeof then !== 'function') {return resolve(x)}then.call(x, v => {if (called) returncalled = trueresolveThenable(promise, v, resolve, reject)}, r => {if (called) returncalled = truereject(r)})} catch (e) {if (called) returnreject(e)}
}Promise.prototype.then = function (resCb, rejCb) {const status = this.statusconst value = this.valuelet thenPromisethenPromise = 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的一些零散知识

  1. Promise.resolve就是本文所实现的resolveThenable,并不是简单的用来返回一个resolved状态的函数,它返回的promise对象的状态也并不一定是resolved。
  2. promise.then(rs, rj)和promise.then(rs).catch(rj)是有区别的,区别在于当rs出错时,后一种方法可以进行错误处理。

感想与总结

实现Promise的过程其实并没有我预想的那么难,所谓的Promise的原理我感觉就是类似于观察者模式,so,不要有畏难情绪,我上我也行^_^。

实现一个符合标准的Promise相关推荐

  1. 如何实现一个符合规范的Promise

    异步发展历程 我们知道Promise的出现是为了处理js中的异步操作.在Promise出现之前,多用回调来处理异步操作.一旦异步操作稍微复杂起来,就很容易出现传说中的回调地狱,会导致代码的可读性和可维 ...

  2. dreamweaver8_Dreamweaver 8符合标准!

    dreamweaver8 If you're reading this article, you probably already have an interest in the subject of ...

  3. 一个符合SEO优化标准的网站应具备哪些特征?

    我们在进行网站建设时,都希望自己的网站能在搜索引擎中获得一个好的排名,都希望自己的网站能有很多的网页被百度等主流搜索引擎收录.要想获得搜索引擎的青睐,前提是要做好网站的SEO优化.那么,一个符合SEO ...

  4. 实现一个符合 Promise/A+ 规范的 MyPromise

    Promise 实现一个符合 Promise/A+ 规范的 MyPromise,并实现 resolve.reject.all.race.defer.deferred等静态方法. MyPromise 作 ...

  5. 方法 手写promise_实现一个符合 Promise/A+规范的 Promise(typescript 版)

    (给前端大全加星标,提升前端技能) 转自:Col0ring juejin.cn/post/6886360224308035598 写在前面 没错,这又是一篇关于手写 Promise 的文章,想必大家已 ...

  6. 深入理解Promise并写一个符合Promise a+规范的Promise代码

    深入理解Promise并写一个符合Promise a+规范的Promise代码 关于Promise函数可以参考我写的这篇文章https://www.cnblogs.com/qiaohong/p/770 ...

  7. html基于web2.0标准,晕倒:“用web2.0来制作符合标准的页面”

    晕倒:"用web2.0来制作符合标准的页面" 互联网   发布时间:2008-10-17 19:58:15   作者:佚名   我要评论 今天有人和我谈一个网站开发项目,当说到具体 ...

  8. 《C专家编程》一1.6 它很棒,但它符合标准吗

    本节书摘来自异步社区<C专家编程>一书中的第1章,第1.6节,作者 [美]Perter Van Der Linde,更多章节内容可以访问云栖社区"异步社区"公众号查看 ...

  9. C语言函数,根据身高计算体重是否符合标准的实例

    1:c语言函数,函数分为库函数(library funchtion)和自定义函数,库函数由编译器提供 例如printf就是系统的库函数用 函数可以没有返回值,只是完成一个功能,一额可以有一个返回值,使 ...

最新文章

  1. keras 的 example 文件 neural_doodle.py 解析
  2. R语言使用pwr包的pwr.r.test函数对相关信息分析进行效用分析(power analysis)、在已知效应量(effect size)、显著性水平、效用值的情况下计算需要的样本量
  3. 最近工作好忙,自己的软件又得落下很长一段时间了~
  4. Java通过JDBC连接SQL Server2017数据库
  5. [120_移动开发Android]005_android开发之数据存储之文件操作
  6. 微信小程序编译 tunneling socket could not be established,cause=connect ······
  7. python拆堆和堆叠的操作_python - 如何合并不同的DFS并堆叠值? - 堆栈内存溢出
  8. 重磅!李飞飞 CS231n 最全学霸笔记精炼版来了
  9. Qt修炼手册6_图形:图形视图框架
  10. 非常适合新手的redis cluster搭建过程
  11. UILabel 宽高自适应
  12. JavaScript学习---JavaScript基础知识
  13. 【clickhouse】ClickHouse表引擎 MergeTree 索引与数据存储方式 一级索引 二级索引
  14. Linux 命令(93)—— updatedb 命令
  15. 公差与配合查询计算过程讲解
  16. wiki(维基)系统
  17. 基于随机森林的偏置-方差分解实验
  18. <2021SC@SDUSC>【Overload游戏引擎】OvCore源码模块分析(三)——GlobalHelpers
  19. Unity给小鳄鱼洗澡2D流体水实现
  20. LeeCode(C++):买卖股票的最佳时机

热门文章

  1. 你真的懂软件测试人员的痛苦吗?——目前软件测试5大误区
  2. python中凯撒密码加密_凯撒密码加密
  3. 剑指offer面试题[51]-数组中重复的数字
  4. fiddler模拟不同的IP
  5. idea findbugs使用_IDEA如何协同开发统一代码风格?编码不规范如何解决?
  6. matlab虚拟现实之工具介绍(修改)
  7. PDE12 wave equation: charactistics
  8. std在汇编语言是什么指令_汇编语言程序指令整理
  9. XDeepFM高阶特征交互,特征交互:一种极深因子分解机模型
  10. 昔年浅谈化工平台网站怎么吸引客户咨询入驻呢?