Friday, April 1, 2022

SegmentFault 最新的文章

SegmentFault 最新的文章


这不是愚人节玩笑,我们回来了:Typecho 1.2.0 发布!

Posted: 31 Mar 2022 09:26 PM PDT

刚刚,Typecho 项目发起人祁宁在官方博客以开发者账号 @joyqi 发文宣布了 Typecho 1.2.0 正式版发布的消息 —— "这不是玩笑,我们回来了!"

此次正式版本的到来,是继去年 9 月份 Typecho 主线版本宣布回归后释出 v1.2.0-rc.1(Release Candidate 版本)的升级版。

Typecho 1.2.0 正式版重要改进

相比 v1.2.0-rc.1,全新的 Typecho 1.2.0 正式版有了不少重要的改进:

  • 新的编辑器实时预览自动跟随效果,以及主题内预览功能
  • 后台界面对移动端的适配
  • 新的安装程序
  • 进入 PHP 7.2 时代,以及对 PHP 8 的更好支持
  • 对容器化的更好支持

除了以上新改进的功能,正式版的版本号规则也有变化:新的版本号会采用常规的3组数字组成(如 1.2.0) ,适合小步快跑的开发模式。

此前由于采用大版本/时间版本的形式(如 1.1/17.10.30 代表在 2017 年 10 月 30 日发表的 1.1 版本,该版本号模仿了 ubuntu 的发行版,比较适合于每年固定日期发行固定版本的形式),与 Typecho 目前的开发状况不太符,因此新的版本号会采用常规的 3 组数字组成。

如 1.2.0,前面两部分是大的版本号,重要更新的时候会变化;最后一位适用于微小更新,针对一些微小的 bug 修复更新。新的版本规则可以让 Typecho 的版本发布更加灵活,小版本更新也可以更频繁。

"再见" PHP 5,"你好" PHP 7.2

据发起人祁宁介绍,Typecho "诞生"之初正值 PHP 5 流行的时代,当时 PHP 7 还是"新秀",转眼间 PHP 8 都已经走上了舞台。因此,Typecho 要保持与时俱进,是时候做出改变了 —— Typecho 项目将整体转向 PHP 7,更确切的说是 PHP 7.2。

"更高的编程语言版本不仅能带来更好的执行效率,关键还能提高编程效率"。Typecho 项目整体转向 PHP 7.2 语言版本的原因,主要还是考虑到兼容性。当然对于用户来说,依旧建议你使用 PHP 7.4 或 PHP 8.0 等更新的 PHP 版本。

升级至 PHP 7.2 后的 Typecho 代码,将会有如下变化:

  • 类名全部由下划线风格转向反斜线的 namespace 风格

// 之前的代码

new Typecho_Widget_Helper_Form();

// 之后的代码

use Typecho\Widget\Helper\Form;
new Form();

  • 编码风格建议统一使用PSR-12
  • 更强的类型约束,所有的接口参数和返回值都将加上类型约束,所以大家在使用的时候也要更加注意规范。
  • 移除了大量过时/无用的代码。

目前,该项目的更新正在 dev 分支开发中,而 Typecho 命名空间的代码则基本已改造完成。得益于新设计的自动加载系统,老旧类命名和使用方法也将得到兼容,因此不必担心之前的插件和主题用不了。

全新的安装脚本

Typecho 1.2.0 正式版基于原生的 UI 组件开发了新的安装脚本,相比以往"代码乱飞"在界面上的改变代码可谓彻底重写。除了交互更加便捷,新安装脚本也支持在命令行通过环境变量静默安装,为后续改动创造了条件。

拥抱容器化

随着近年来微服务的强势崛起,微服务的基础容器化也早已走入寻常开发者的视野。无论是 k8s 还是 Docker,代码部署时针对容器化作出相应设计已成为基本需求。全新 Typecho 1.2.0 正式版在容器化方面也取得了很大的进步,目前官方容器已经正式推出:

Docker Hub: https://hub.docker.com/r/joyq...
Github: https://github.com/typecho/Do...

官方 Docker 镜像支持 PHP 7.3 PHP 7.4 PHP 8.0,且运行环境支持 FPM Apache Cli等模式,操作系统可选 debian 或者更小巧的 alpine。

Typecho 使用了 Github 的流水线做全自动化编译提交,除发布新版本时会自动编译以外,每晚也会自动发布一个 nightly 版本(目前只有这个版本,并非正式代码,建议测试使用),Dockerfile 的文档还在书写中。

Github Actions 等持续集成的工具让开发的发布测试工作更加高效,目前 Typecho 已设计了很多有趣的流水线。

关于 Typecho

Typecho 是由 type 和 echo 两个词合成,来自于开发团队的头脑风暴。

Typecho 是一款基于 PHP 7 开发构建的内核强健、扩展方便、体验友好、运行流畅的轻量级开源博客程序(前身是一款名为 Magike 的开源博客程序)。Typecho 在 GPLVersion 2 许可证下发行,可以运行在各种平台上,支持多种数据库(Mysql , PostgreSQL , SQLite)。

参考链接:https://joyqi.com/typecho/abo...

祁宁 Joyqi ,SegmentFault 思否创始人、CTO, 毕业于华中科技大学电信系,是华科 Dian 团队第 98 号成员。曾工作于阿里巴巴,在游戏公司担任技术架构师,在大学期间开发了开源博客系统 Typecho ,得到了近百万独立开发者和设计师的喜爱。

按照 Promise/A+ 规范逐行注释并实现 Promise

Posted: 31 Mar 2022 06:05 PM PDT

0. 前言

面试官:「你写个 Promise 吧。」

我:「对不起,打扰了,再见!」

现在前端越来越卷,不会手写 Promise 都不好意思面试了(手动狗头.jpg)。虽然没多少人会在业务中用自己实现的 Promise,但是,实现 Promise 的过程会让你对 Promise 更加了解,出了问题也可以更好地排查。

如果你还不熟悉 Promise,建议先看一下 MDN 文档

在实现 Promise 之前,我建议你先看一遍 Promises/A+ 规范(中文翻译:Promise A+ 规范),本文中不会再次介绍相关的概念。推荐大家优先阅读原版英文,只需高中水平的英语知识就够了,遇到不懂的再看译文。

另外,本文将使用 ES6 中的 Class 来实现 Promise。为了方便大家跟 Promise/A+ 规范对照着看,下文的顺序将按照规范的顺序来行文。

在正式开始之前,我们新建一个项目,名称随意,按照以下步骤进行初始:

  • 打开 CMD 或 VS Code,运行 npm init,初始化项目
  • 新建 PromiseImpl.js 文件,后面所有的代码实现都写在这个文件里

完整代码地址:ashengtan/promise-aplus-implementing

1. 术语

这部分大家直接看规范就好,也没什么好解释的。注意其中对于 value 的描述,value 可以是一个 thenable(有 then 方法的对象或函数) 或者 Promise,这点会在后面的实现中体现出来。

2. 要求

2.1 Promise 的状态

一个 Promise 有三种状态:

  • pending:初始状态
  • fulfilled:成功执行
  • rejected:拒绝执行

一个 Promise 一旦从 pending 变为 fulfilledrejected,就无法变成其他状态。当 fulfilled 时,需要给出一个不可变的值;同样,当 rejected 时,需要给出一个不可变的原因。

根据以上信息,我们定义 3 个常量,用来表示 Promise 的状态:

const STATUS_PENDING = 'pending' const STATUS_FULFILLED = 'fulfilled' const STATUS_REJECTED = 'rejected'

接着,我们先把 Promise 的基础框架先定义出来,这里我使用 ES6 的 Class 来定义:

class PromiseImpl {   constructor() {}    then(onFulfilled, onRejected) {} }

这里我们先回想一下 Promise 的基本用法:

const promise = new Promise((resolve, reject) => {   // ...do something   resolve(value) // or reject(error) })  // 多次调用 const p1 = promise.then() const p2 = promise.then() const p3 = promise.then()

好了,继续完善 PromiseImpl,先完善一下构造方法:

class PromiseImpl {   constructor() {     // `Promise` 当前的状态,初始化时为 `pending`     this.status = STATUS_PENDING     // fulfilled 时的值     this.value = null     // rejected 时的原因     this.reason = null   } }

另外,我们还要定义两个方法,用于 fulfilledrejected 时回调:

class PromiseImpl {   constructor() {     // ...其他代码      // 2.1.2 When `fulfilled`, a `promise`:     //  2.1.2.1 must not transition to any other state.     //  2.1.2.2 must have a value, which must not change.     const _resolve = value => {       // 如果 `value` 是 `Promise`(即嵌套 `Promise`),       // 则需要等待该 `Promise` 执行完成       if (value instanceof PromiseImpl) {         return value.then(           value => _resolve(value),           reason => _reject(reason)         )       }              if (this.status === STATUS_PENDING) {         this.status = STATUS_FULFILLED         this.value = value       }     }      // 2.1.3 When `rejected`, a `promise`:     //  2.1.3.1 must not transition to any other state.     //  2.1.3.2 must have a reason, which must not change.     const _reject = reason => {       if (this.status === STATUS_PENDING) {         this.status = STATUS_REJECTED         this.reason = reason       }     }   } }

注意,在 _resolve() 中,如果 valuePromise 的话(即嵌套 Promise),则需要等待该 Promise 执行完成。这点很重要,因为后面的其他 API 如 Promise.resolvePromise.allPromise.allSettled 等均需要等待嵌套 Promise 执行完成才会返回结果。

最后,别忘了在 new Promise() 时,我们需要将 resolvereject 传给调用者:

class PromiseImpl {   constructor(executor) {     // ...其他代码      try {       executor(_resolve, _reject)     } catch (e) {       _reject(e)     }   } }

使用 trycatchexecutor 包裹起来,因为这部分是调用者的代码,我们无法保证调用者的代码不会出错。

2.2 Then 方法

一个 Promise 必须提供一个 then 方法,其接受两个参数:

promise.then(onFulfilled, onRejected)
class PromiseImpl {   then(onFulfilled, onRejected) {} }

2.2.1 onFulfilledonRejected

从规范 2.2.1 中我们可以得知以下信息:

  • onFulfilledonRejected 是可选参数
  • 如果 onFulfilledonRejected 不是函数,则必须被忽略

因此,我们可以这样实现:

class PromiseImpl {   then(onFulfilled, onRejected) {     // 2.2.1 Both `onFulfilled` and `onRejected` are optional arguments:     //   2.2.1.1 If `onFulfilled` is not a function, it must be ignored     //   2.2.1.2 If `onRejected` is not a function, it must be ignored     onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : () => {}     onRejected = typeof onRejected === 'function' ? onRejected : () => {}   } }

2.2.2 onFulfilled 特性

从规范 2.2.2 中我们可以得知以下信息:

  • 如果 onFulfilled 是一个函数,则必须在 fulfilled 后调用,第一个参数为 promise 的值
  • 只能调用一次
class PromiseImpl {   then(onFulfilled, onRejected) {     // ...其他代码          // 2.2.2 If `onFulfilled` is a function:     //   2.2.2.1 it must be called after `promise` is fulfilled,     // with promise's value as its first argument.     //   2.2.2.2 it must not be called before `promise` is fulfilled.     //   2.2.2.3 it must not be called more than once.     if (this.status === STATUS_FULFILLED) {       onFulfilled(this.value)     }   } }

2.2.3 onRejected 特性

onFulfilled 同理,只不过是在 rejected 时调用,第一个参数为 promise 失败的原因

class PromiseImpl {   then(onFulfilled, onRejected) {     // ...其他代码      // 2.2.3 If onRejected is a function:     //   2.2.3.1 it must be called after promise is rejected,     // with promise's reason as its first argument.     //   2.2.3.2 it must not be called before promise is rejected.     //   2.2.3.3 it must not be called more than once.     if (this.status === STATUS_REJECTED) {       onRejected(this.reason)     }   } }

2.2.4 异步执行

在日常开发中,我们经常使用 Promise 来做一些异步操作,规范 2.2.4 就是规定异步执行的问题,具体的可以结合规范里的注释阅读,重点是确保 onFulfilledonRejected 要异步执行。

需要指出的是,规范里并没有规定 Promise 一定要用 micro-task 机制来实现,因此你使用 macro-task 机制来实现也是可以的。当然,现在浏览器用的是 micro-task 来实现。这里为了方便,我们使用 setTimeout(属于 macro-task)来实现。

因此,我们需要稍微改造下上面的代码:

class PromiseImpl {   then(onFulfilled, onRejected) {     // ...其他代码          // fulfilled     if (this.status === STATUS_FULFILLED) {       setTimeout(() => {         onFulfilled(this.value)       }, 0)     }      // rejected     if (this.status === STATUS_REJECTED) {       setTimeout(() => {         onRejected(this.reason)       }, 0)     }   } }

2.2.5 onFulfilledonRejected 必须作为函数被调用

这个已经在上面实现过了。

2.2.6 then 可被多次调用

举个例子:

const promise = new Promise((resolve, reject) => {   // ...do something   resolve(value) // or reject(error) })  promise.then() promise.then() promise.catch()

因此,必须确保当 Promise fulfilledrejected 时,onFulfilledonRejected 按照其注册的顺序逐一回调。还记得最开始我们定义的 resolvereject 吗?这里我们需要改造下,保证所有的回调都被执行到:

const invokeArrayFns = (fns, arg) => {   for (let i = 0; i < fns.length; i++) {     fns[i](arg)   } }  class PromiseImpl {   constructor(executor) {     // ...其他代码      // 用于存放 `fulfilled` 时的回调,一个 `Promise` 对象可以注册多个 `fulfilled` 回调函数     this.onFulfilledCbs = []     // 用于存放 `rejected` 时的回调,一个 `Promise` 对象可以注册多个 `rejected` 回调函数     this.onRejectedCbs = []      const resolve = value => {       if (this.status === STATUS_PENDING) {         this.status = STATUS_FULFILLED         this.value = value         // 2.2.6.1 If/when `promise` is fulfilled,          // all respective `onFulfilled` callbacks must execute          // in the order of their originating calls to `then`.         invokeArrayFns(this.onFulfilledCbs, value)       }     }      const reject = reason => {       if (this.status === STATUS_PENDING) {         this.status = STATUS_REJECTED         this.reason = reason         // 2.2.6.2 If/when `promise` is rejected,          // all respective `onRejected` callbacks must execute          // in the order of their originating calls to `then`.         invokeArrayFns(this.onRejectedCbs, reason)       }     }   } }

看到这里你可能会有疑问,什么时候往 onFulfilledCbsonRejectedCbs 里存放对应的回调,答案是在调用 then 时:

class PromiseImpl {   then(onFulfilled, onRejected) {     // ...其他代码      // pending     if (this.status === STATUS_PENDING) {       this.onFulfilledCbs.push(() => {         setTimeout(() => {           onFulfilled(this.value)         }, 0)       })        this.onRejectedCbs.push(() => {         setTimeout(() => {           onRejected(this.reason)         }, 0)       })     }   } }

此时 Promise 处于 pending 状态,无法确定其最后是 fulfilled 还是 rejected,因此需要将回调函数存放起来,待状态确定后再执行相应的回调函数。

注:invokeArrayFns 来源于 Vue.js 3 中的源码。

2.2.7 then 必须返回 Promise

promise2 = promise1.then(onFulfilled, onRejected)

那么,在我们上面的代码中,then 怎么才能返回 Promise 呢?很简单:

class PromiseImpl {   then(onFulfilled, onRejected) {     let promise2 = new PromiseImpl((resolve, reject) => {       if (this.status === STATUS_FULFILLED) {         // ...相关代码       }        if (this.status === STATUS_REJECTED) {         // ...相关代码       }        if (this.status === STATUS_PENDING) {         // ...相关代码       }     })      return promise2   } }

因为调用 then 之后返回一个新的 Promise 对象,使得我们也可以进行链式调用:

Promise.resolve(42).then().then()...

2.2.7.1 ~ 2.2.7.4 这四点比较重要,我们下面分别来看看。

2.2.7.1 如果 onFulfilledonRejected 返回一个值 x,则运行 Promise 解决过程,[[Resolve]](promise2, x)

解释:其实所谓运行 Promise 解决过程就是执行某个操作,我们把这个操作抽取成一个方法,并命名为:promiseResolutionProcedure(promise, x, resolve, reject)。为了方便,我们把 resolvereject 一并透传进去。

class PromiseImpl {   then(onFulfilled, onRejected) {     // ...其他代码          let promise2 = new PromiseImpl((resolve, reject) => {       if (this.status === STATUS_FULFILLED) {         setTimeout(() => {           // 2.2.7.1           let x = onFulfilled(this.value)           promiseResolutionProcedure(promise2, x, resolve, reject)         }, 0)       }        if (this.status === STATUS_REJECTED) {         setTimeout(() => {           // 2.2.7.1           let x = onRejected(this.reason)           promiseResolutionProcedure(promise2, x, resolve, reject)         }, 0)       }        if (this.status === STATUS_PENDING) {         this.onFulfilledCbs.push(() => {           setTimeout(() => {             // 2.2.7.1              let x = onFulfilled(this.value)             promiseResolutionProcedure(promise2, x, resolve, reject)           }, 0)         })          this.onRejectedCbs.push(() => {           setTimeout(() => {             // 2.2.7.1             let x = onRejected(this.reason)             promiseResolutionProcedure(promise2, x, resolve, reject)           }, 0)         })       }     })      return promise2   } }

2.2.7.2 如果 onFulfilledonRejected 抛出一个异常 e,则 promise2 必须 rejected,并返回原因 e

解释:实现上面体现在执行 onFulfilledonRejected 时使用 trycatch 包括起来,并在 catch 时调用 reject(e)

class PromiseImpl {   then(onFulfilled, onRejected) {     // ...其他代码          let promise2 = new PromiseImpl((resolve, reject) => {       if (this.status === STATUS_FULFILLED) {         setTimeout(() => {           try {             // 2.2.7.1             let x = onFulfilled(this.value)             promiseResolutionProcedure(promise2, x, resolve, reject)           } catch (e) {             // 2.2.7.2             reject(e)           }         }, 0)       }        if (this.status === STATUS_REJECTED) {         setTimeout(() => {           try {             // 2.2.7.1             let x = onRejected(this.reason)             promiseResolutionProcedure(promise2, x, resolve, reject)           } catch (e) {             // 2.2.7.2             reject(e)           }         }, 0)       }        if (this.status === STATUS_PENDING) {         this.onFulfilledCbs.push(() => {           setTimeout(() => {             try {               // 2.2.7.1               let x = onFulfilled(this.value)               promiseResolutionProcedure(promise2, x, resolve, reject)             } catch (e) {               // 2.2.7.2               reject(e)             }           }, 0)         })          this.onRejectedCbs.push(() => {           setTimeout(() => {             try {               // 2.2.7.1               let x = onRejected(this.reason)               promiseResolutionProcedure(promise2, x, resolve, reject)             } catch (e) {               // 2.2.7.2               reject(e)             }           }, 0)         })       }     })      // 2.2.7 `then` must return a promise     return promise2   } }

2.2.7.3 如果 onFulfilled 不是函数且 promise1 已经 fulfilled,则 promise2 必须 fulfilled 且返回与 promise1 相同的值。

解释:值的透传,例如:

Promise.resolve(42).then().then(value => console.log(value)) // 42

在第一个 then 时,我们忽略了 onFulfilled,那么在链式调用的时候,需要把值透传给后面的 then

class PromiseImpl {   then(onFulfilled, onRejected) {     onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value      // ...其他代码   } }

2.2.7.4 如果 onRejected 不是函数且 promise1 已经 rejected,则 promise2 必须 rejected 且返回与 promise1 相同的原因。

解释:同理,原因也要透传:

Promise.reject('reason').catch().catch(reason => console.log(reason)) // 'reason'
class PromiseImpl {   then(onFulfilled, onRejected) {     onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason }      // ...其他代码   } }

2.3 Promise 解决过程

Promise 解决过程是一个抽象的操作,输入一个 promise 和一个值,这里我们将其命名为 promiseResolutionProcedure(promise, x, resolve, reject),并在调用时传入 resolvereject 两个方法, 分别用于在 fulfilledrejected 时调用。

2.3.1 如果 promisex 为同一个对象

如果 promisex 为同一个对象,拒绝该 promise,原因为 TypeError

const promiseResolutionProcedure = (promise, x, resolve, reject) => {   // 2.3.1 If `promise` and `x` refer to the same object,   // reject `promise` with a `TypeError` as the reason   if (promise === x) {     return reject(new TypeError('`promise` and `x` refer to the same object, see: https://promisesaplus.com/#point-48'))   }    // ...其他代码 }

2.3.2 如果 x 是一个 Promise 对象

如果 x 是一个 Promise 对象,则需要递归执行:

const promiseResolutionProcedure = (promise, x, resolve, reject) => {   // ...其他代码    // 2.3.2 If `x` is a promise, adopt its state:   //   2.3.2.1 If `x` is pending, `promise` must remain pending until `x` is fulfilled or rejected.   //   2.3.2.2 If/when `x` is fulfilled, fulfill `promise` with the same value.   //   2.3.2.3 If/when `x` is rejected, reject `promise` with the same reason.   if (x instanceof PromiseImpl) {     return x.then(       value => promiseResolutionProcedure(promise, value, resolve, reject),       reason => reject(reason)     )   } }

2.3.3 如果 x 是一个对象或函数

如果 x 是一个对象或函数:

2.3.3.1x.then 赋值给 x

解释:这样做有两个目的:

  • 避免对 x.then 的多次访问(这也是日常开发中的一个小技巧,当要多次访问一个对象的同一属性时,通常我们会使用一个变量将该属性存储起来,避免多次进行原型链查找)
  • 执行过程中 x.then 的值可能被改变
const promiseResolutionProcedure = (promise, x, resolve, reject) => {   // ...其他代码    // 2.3.3 Otherwise, if x is an object or function   if ((x !== null && typeof x === 'object') || typeof x === 'function') {          // 2.3.3.1 Let `then` be `x.then`     let then = x.then   } }

2.3.3.2 如果在对 x.then 取值时抛出异常 e,则拒绝该 promise,原因为 e

const promiseResolutionProcedure = (promise, x, resolve, reject) => {   // ...其他代码    if ((x !== null && typeof x === 'object') || typeof x === 'function') {          try {       // 2.3.3.1 Let `then` be `x.then`     } catch (e) {       // 2.3.3.2 If retrieving the property `x.then` results in a thrown exception `e`,       // reject `promise` with `e` as the reason.       reject(e)     }   } }

2.3.3.3 如果 then 是函数

如果 then 是函数,则将 x 作为 then 的作用域,并调用 then,同时传入两个回调函数,第一个名为 resolvePromise,第二个名为 rejectPromise

解释:意思就是在调用 then 的时候要指定其 this 值为 x,同时需要传入两个回调函数。这时候用 call() 来实现是最好不过了:

then.call(x, resolvePromise, rejectPromise)

2.3.3.3.1 ~ 2.3.3.3.4 总结起来的意思如下:

  • 2.3.3.3.1 如果 resolvePromise 被调用,则递归调用 promiseResolutionProcedure,值为 y。因为 Promise 中可以嵌套 Promise

    then.call(   x,   y => promiseResolutionProcedure(promise, y, resolve, reject),   rejectPromise )
  • 2.3.3.3.2 如果 rejectPromise 被调用,参数为 r,则拒绝执行 Promise,原因为 r

    then.call(   x,   y => promiseResolutionProcedure(promise, y, resolve, reject), // resolvePromise   r => reject(r) // rejectPromise )
  • 2.3.3.3.3 如果 resolvePromiserejectPromise 均被调用,或者被同一参数调用了多次,则只执行首次调用

    解释:这里我们可以通过设置一个标志位来解决,然后分别在 resolvePromiserejectPromise 这两个回调函数内做判断:

    // 初始化时设置为 false let called = false  if (called) {     return }  // `resolvePromise` 和 `rejectPromise` 被调用时设置为 true called = true
  • 2.3.3.3.4 调用 then 抛出异常 e 时,如果这时 resolvePromiserejectPromise 已经被调用,则忽略;否则拒绝该 Promise,原因为 e
const promiseResolutionProcedure = (promise, x, resolve, reject) => {   // ...其他代码      let called = false    if ((x !== null && typeof x === 'object') || typeof x === 'function') {     try {       let then = x.then        if (typeof then === 'function') {         // 2.3.3.3 If `then` is a function, call it with `x` as `this`,         // first argument `resolvePromise`, and second argument `rejectPromise`         then.call(           // call it with `x` as `this`           x,            // `resolvePromise`           // 2.3.3.3.1 If/when `resolvePromise` is called with a value `y`,           // run `[[Resolve]](promise, y)`.           y => {             // 2.3.3.3.3 If both `resolvePromise` and `rejectPromise` are called,             // or multiple calls to the same argument are made,             // the first call takes precedence, and any further calls are ignored.             if (called) {               return             }             called = true              promiseResolutionProcedure(promise, y, resolve, reject)           },            // `rejectPromise`           // 2.3.3.3.2 If/when `rejectPromise` is called with a reason `r`,           // reject `promise` with `r`           r => {             // 2.3.3.3.3             if (called) {               return             }             called = true              reject(r)           }         )       } else {         // 2.3.3.4 If `then` is not a function, fulfill `promise` with `x`         resolve(x)       }     } catch (e) {       // 2.3.3.3.3       if (called) {         return       }       called = true        // 2.3.3.2 If retrieving the property `x.then` results in a thrown exception `e`,       // reject `promise` with `e` as the reason.        // 2.3.3.3.4 If calling `then` throws an exception `e`       //   2.3.3.3.4.1 If `resolvePromise` or `rejectPromise` have been called, ignore it       //   2.3.3.3.4.2 Otherwise, reject `promise` with `e` as the reason        reject(e)     }   } }

2.3.3.4 如果 then 不是函数,则执行该 promise,参数为 x

const promiseResolutionProcedure = (promise, x, resolve, reject) => {   // ...其他代码    if ((x !== null && typeof x === 'object') || typeof x === 'function') {     try {       let then = x.then        if (typeof then === 'function') {         // 2.3.3.3       } else {         // 2.3.3.4 If `then` is not a function, fulfill `promise` with `x`         resolve(x)       }     } catch (e) {     }   } }

2.3.4 如果 x 既不是对象也不是函数

如果 x 既不是对象也不是函数,执行该 promise,参数为 x

const promiseResolutionProcedure = (promise, x, resolve, reject) => {   // ...其他代码    if ((x !== null && typeof x === 'object') || typeof x === 'function') {      // 2.3.3   } else {     // 2.3.4 If `x` is not an object or function, fulfill `promise` with `x`     resolve(x)   } }

至此,我们的自定义 Promise 已经完成。这是源码:promise-aplus-implementing

4. 如何测试

Promise/A+ 规范提供了一个测试脚本:promises-tests,你可以用它来测试你的实现是否符合规范。

PromiseImpl.js 里添加以下代码:

// PromiseImpl.js  const STATUS_PENDING = 'pending' const STATUS_FULFILLED = 'fulfilled' const STATUS_REJECTED = 'rejected'  const invokeArrayFns = (fns, arg) => {   // ...相关代码 }  const promiseResolutionProcedure = (promise, x, resolve, reject) => {   // ...相关代码 }  class PromiseImpl {   // ...相关代码 }  PromiseImpl.defer = PromiseImpl.deferred = () => {   const dfd = {}   dfd.promise = new PromiseImpl((resolve, reject) => {     dfd.resolve = resolve     dfd.reject = reject   })    return dfd }  module.exports = PromiseImpl

然后在 package.json 里添加 scripts

// package.json  {   "scripts": {     "test": "promises-aplus-tests PromiseImpl.js"   } }

然后,运行 npm run test,执行测试脚本,如果你的实现符合 Promise/A+ 规范的话,所有测试用例均会通过。

5. 其他 API 的实现

Promise/A+ 规范只规定了 then() 方法的实现,其他的如 catch()finally() 等方法并不在规范里。但就实现而言,这些方法可以通过对 then() 方法或其他方式进行二次封装来实现。

另外,像是 all()race() 等这些方法,其参数为一个可迭代对象,如 ArraySetMap 等。那么,什么是可迭代对象根据 ES6 中的规范,要成为可迭代对象,一个对象必须实现 @@iterator 方法,即该对象必须有一个名为 @@iterator 的属性,通常我们使用常量 Symbol.iterator 来访问该属性。

根据以上信息,判断一个参数是否为可迭代对象,其实现如下:

const isIterable = value => !!value && typeof value[Symbol.iterator] === 'function'

Promise.resolve

Promise.resolve(value)静态方法,其参数有以下几种可能:

  • 参数是 Promise 对象
  • 参数是 thenable 对象(拥有 then() 方法的对象)
  • 参数是原始值或不具有 then() 方法的对象
  • 参数为空

因此,其返回值由其参数决定:有可能是一个具体的值,也有可能是一个 Promise 对象:

class PromiseImpl {    static resolve(value) {     return new PromiseImpl((resolve, reject) => resolve(value))   } }

Promise.reject

Promise.reject(reason)静态方法,参数为 Promise 拒绝执行时的原因,同时返回一个 Promise 对象,状态为 rejected

class PromiseImpl {    static reject(reason) {     return new PromiseImpl((resolve, reject) => reject(reason))   } }

Promise.prototype.catch

Promise.prototype.catch(onRejected) 其实就是 then(null, onRejected) 的语法糖:

class PromiseImpl {   catch(onRejected) {     return this.then(null, onRejected)   } }

Promise.prototype.finally

顾名思义,不管 Promise 最后的结果是 fulfilled 还是 rejectedfinally 里的语句都会执行:

class PromiseImpl {   finally(onFinally) {     return this.then(       value => PromiseImpl.resolve(onFinally()).then(() => value),        reason => PromiseImpl.resolve(onFinally()).then(() => { throw reason })     )   } }

Promise.all

Promise.all(iterable)静态方法,参数为可迭代对象:

  • 只有当 iterable 里所有的 Promise 都成功执行后才会 fulfilled,回调函数的返回值为所有 Promise 的返回值组成的数组,顺序与 iterable 的顺序保持一致。
  • 一旦有一个 Promise 拒绝执行,则状态为 rejected,并且将第一个拒绝执行的 Promise 的原因作为回调函数的返回值。
  • 该方法会返回一个 Promise
class PromiseImpl {   static all(iterable) {     if (!isIterable(iterable)) {       return new TypeError(`TypeError: ${typeof iterable} is not iterable (cannot read property Symbol(Symbol.iterator))`)     }      return new PromiseImpl((resolve, reject) => {       // `fulfilled` 的 Promise 数量       let fulfilledCount = 0       // 收集 Promise `fulfilled` 时的值       const res = []          for (let i = 0; i < iterable.length; i++) {         const iterator = iterable[i]         iterator.then(           value => {             res[i] = value             fulfilledCount++             if (fulfilledCount === iterable.length) {               resolve(res)             }           },           reason => reject(reason)         )       }     })   } }

测试一下:

const promise1 = Promise.resolve(42) const promise2 = new PromiseImpl((resolve, reject) => setTimeout(() => resolve('value2'), 1000))  PromiseImpl.all([   promise1,   promise2 ]).then(values => console.log('values:', values))

结果:

values: [42, 'value2']

好像挺完美的,但是事实果真如此吗?仔细看看我们的代码,假如 iterable 是一个空的数组呢?假如 iterable 里有不是 Promise 的呢?就像这样:

PromiseImpl.all([]) PromiseImpl.all([promise1, promise2, 'value3'])

这种情况下执行前面的代码,是得不到任何结果的。因此,代码还要再改进一下:

class PromiseImpl {   static all(iterable) {     if (!isIterable(iterable)) {       return new TypeError(`TypeError: ${typeof iterable} is not iterable (cannot read property Symbol(Symbol.iterator))`)     }      return new PromiseImpl((resolve, reject) => {       // `fulfilled` 的 Promise 数量       let fulfilledCount = 0       // 收集 Promise `fulfilled` 时的值       const res = []              // - 填充 `res` 的值       // - 增加 `fulfilledCount`       // - 判断所有 `Promise` 是否已经全部成功执行       const processRes = (index, value) => {         res[index] = value         fulfilledCount++         if (fulfilledCount === iterable.length) {           resolve(res)         }       }        if (iterable.length === 0) {         resolve(res)       } else {         for (let i = 0; i < iterable.length; i++) {           const iterator = iterable[i]                      if (iterator && typeof iterator.then === 'function') {             iterator.then(               value => processRes(i, value),               reason => reject(reason)             )           } else {             processRes(i, iterator)           }         }       }     })   } }

现在再来测试一下:

const promise1 = PromiseImpl.resolve(42) const promise2 = 3 const promise3 = new PromiseImpl((resolve, reject) => setTimeout(() => resolve('value3'), 1000))  PromiseImpl.all([   promise1,   promise2,   promise3,   'a' ]).then(values => console.log('values:', values)) // 结果:values: [42, 3, 'value3', 'a']  PromiseImpl.all([]).then(values => console.log('values:', values)) // 结果:values: []

Promise.allSettled

Promise.allSettled(iterable)静态方法,参数为可迭代对象:

  • iterable 里所有的 Promise 都成功执行或拒绝执行后才完成,返回值是一个对象数组。
  • 如果有嵌套 Promise,需要等待该 Promise 完成。
  • 返回一个新的 Promise 对象。

对于 allSettled,我们可以在 all 的基础上进行封装:

class PromiseImpl {   static allSettled(iterable) {     if (!isIterable(iterable)) {       return new TypeError(`TypeError: ${typeof iterable} is not iterable (cannot read property Symbol(Symbol.iterator))`)     }      const promises = iterable.map(iterator => PromiseImpl.resolve(iterator).then(       value => ({ status: STATUS_FULFILLED, value }),       reason => ({ status: STATUS_REJECTED, reason })     ))      return PromiseImpl.all(promises)   } }

测试结果:

PromiseImpl.allSettled([   PromiseImpl.resolve(42),   PromiseImpl.reject('Oops!'),   PromiseImpl.resolve(PromiseImpl.resolve(4242)),   'a' ]).then(values => console.log(values)) // 结果: // [ //   { status: 'fulfilled', value: 42 }, //   { status: 'rejected', reason: 'Oops!' }, //   { status: 'fulfilled', value: 4242 }, //   { status: 'fulfilled', value: 'a' } // ]

Promise.race

Promise.race(iterable)静态方法,参数为可迭代对象:

  • iterable 里的任一 Promise 成功执行或拒绝执行时,使用该 Promise 成功返回的值或拒绝执行的原因
  • 如果 iterable 为空,则处于 pending 状态
  • 返回一个新的 Promise 对象

例如:

Promise.race([   Promise.resolve(42),   Promise.reject('Oops!'),   'a' ]).then(values => console.log(values))   .catch(reason => console.log(reason)) // 结果:42  Promise.race([   Promise.reject('Oops!'),   Promise.resolve(42),   'a' ]).then(values => console.log(values))   .catch(reason => console.log(reason)) // 结果:Oops!

实现如下:

class PromiseImpl {   static race(iterable) {     if (!isIterable(iterable)) {       return new TypeError(`TypeError: ${typeof iterable} is not iterable (cannot read property Symbol(Symbol.iterator))`)     }      return new PromiseImpl((resolve, reject) => {       if (iterable.length === 0) {         return       } else {         for (let i = 0; i < iterable.length; i++) {           const iterator = iterable[i]                      if (iterator && typeof iterator.then === 'function') {             iterator.then(               value => resolve(value),               reason => reject(reason)             )             return           } else {             resolve(iterator)             return           }         }       }     })   } }

这里要注意一点:别忘了使用 return 来结束 for 循环

测试结果:

PromiseImpl.race([   PromiseImpl.resolve(42),   PromiseImpl.reject('Oops!'),   'a' ]).then(values => console.log(values))   .catch(reason => console.log(reason)) // 结果:42  PromiseImpl.race([   PromiseImpl.reject('Oops!'),   PromiseImpl.resolve(42),   'a' ]).then(values => console.log(values))   .catch(reason => console.log(reason)) // 结果:'Oops!'  PromiseImpl.race([   'a',   PromiseImpl.reject('Oops!'),   PromiseImpl.resolve(42) ]).then(values => console.log(values))   .catch(reason => console.log(reason)) // 结果:'a'

6. 共同探讨

  1. 2.3.3.1 把 x.then 赋值给 then 中,什么情况下 x.then 的指向会被改变?
  2. 2.3.3.3 如果 then 是函数 中,除了使用 call() 之外,还有什么其他方式实现吗?

7. 总结

实现 Promise,基本分为三个步骤:

  1. 定义 Promise 的状态
  2. 实现 then 方法
  3. 实现 Promise 解决过程

8. 写在最后

以前,我在意能不能自己实现一个 Promise,到处找文章,这块代码 Ctrl+C,那块代码 Ctrl+V。现在,我看重的是实现的过程,在这个过程中,你不仅会对 Promise 更加熟悉,还可以学习如何将规范一步步转为实际代码。做对的事,远比把事情做对重要

如果你觉得这篇文章对你有帮助,还请:点赞、收藏、转发;如果你有疑问,请在评论区写出来,我们一起探讨。同时也欢迎关注我的公众号:前端笔记

参考资料

No comments:

Post a Comment