从Promise本质开始(一):实现Promise前言

注意,由于本文讨论的是使用 TDD 方式实现一个 Promise,所以本文着重描述的是搭建 TDD 环境

Promise 能解决什么问题

先来看一段代码

fs.readdir(source, function (err, files) {
if (err) {
console.log('Error finding files: ' + err)
} else {
files.forEach(function (filename, fileIndex){
console.log(filename)
gm(source + filename).size(function (err, values) {
if (err) {
console.log('Error identifying file size: ' + err)
} else {
console.log(filename + ' : ' + values)
aspect = (values.width / values.height)
widths.forEach(function (width, widthIndex) {
height = Math.round(width / aspect)
console.log('resizing ' + filename + 'to ' + height + 'x' + height)
this.resize(width, height).write(dest + 'w' + width + '_' + filename, function(err) {
if (err) console.log('Error writing file: ' + err)
})
}.bind(this))
}
})
})
}
})

可以看到着里面产生了几个回调,也就是那个著名的 callback hell(回调地狱)

所以这有可能真的是回调地狱的问题么?来看看改善之后的代码吧

fs.readdir(source, (err, files) => {
travalFiles = () => {
if (err) {
return console.log('Error: 找不到目录' + err)
}
files.forEach(gmFile)
}
gmFile = (filename) => {
console.log(filename)
gm(source + filename).size(afterGetSize)
}
afterGetSize = (err, values) => {
if (err) return console.log('无法读取文件尺寸: ' + err)
console.log(filename + ' : ' + values)
aspect = (values.width / values.height)
widths.forEach((width, widthIndex) => resize(width, aspect))
}
resize = (width, aspect) => {
height = Math.round(width / aspect)
console.log('将' + filename + '的尺寸变为' + width + 'x' + height)
this.resize(width, height).write(
dest + 'w' + width + '_' + filename,
(err) => err && console.log('Error writing file: ' + err)
)
}

travalFiles(err, files)
})

上面的代码没有回调么?当然有,那么为什么看起来清晰了呢?是因为这里将 函数当作参数传给要调用的函数了,所以其实对于 Promise 而言也是一样的

Promise 的优点

优点有俩

减少缩进(在整体代码的意义上)

函数套函数 的形式转变成 链式调用

f1(xxx, function f2(a) {
f3(yyy, function f4(b) {
f5(a+b, function f6() {})
})
})

转变成

f1(xxx)
.then(f2) // f2 调用 f3,并同时将参数作为结果输出
.then(f4) // f4 调用 f5,并同时接受到 f2 的结果(就是参数),f5 就得到了 f2 以及 f4 的参数
.then(f6)

消灭 if(err) 形式的代码

  • 关于错误的处理可以单独放到一个函数里面
  • 如果不处理,则一直等到向后抛
f1(xxx)
.then(f2, error1)
.then(f4, error2)
.then(f6, error3)
.then(null, errorAll)
// 这里最后一句可以改成 catch, 意味着它可以处理所有的 error

如何使用 Promise

对于一个异步的代码,以前是这么写

function test(fn) {
setTimeout(() => {
fn('1 秒后调用') // 这里相当于 fn.call(null, '1 秒后调用')
} , 1000)
}

test(() => { console.log('传入 fn') })

如果使用 Promise

function test() {
// new Promise 接受一个参数,返回一个 Promise 实例
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('1 秒后调用')
} , 1000)
})
}

test(() => { console.log('传入 fn') })

Promise 有哪些 API

  • Promise 是一个类(相当于一个特殊的函数)
  • 类属性: length
  • 类方法: all, allSettled, race, reject, resolve
  • 对象属性: then , finally, catch
  • 对象内部属性: state(pending, fullfilled, rejected)

这里注意,statepending 状态的转变是单向的,即只有

  • pending -> fullfilled(成功)
  • pending -> rejected(失败)

并且 fullfilled 以及 rejected 状态不能互相转变,也不能转化成 pending

Promise API 怎么写

  • 参考 promise/A+ 规范 或者其 翻译版文档
  • 在写代码时根据文档写测试用例,规则都通过了说明 Promise 相关的逻辑也就完成了

使用测试工具

使用 chai

先全局安装两个包

yarn add -g ts-node mocha

然后创建项目 promise-easy,再执行

cd promise-easy
yarn init -y

最后再次给项目安装包

yarn add -D ts-node mocha chai @types/chai @types/mocha typescript

然后创建 test/index.ts

touch test/index.ts

最后增加 package.jsonscripts

"scripts": {
"test": "mocha -r ts-node/register test/**/*.ts" // mocha 使用 ts-node/register 模块来对 test 下的 ts 文件进行测试
}

然后执行

yarn test

出现如下打印输出则算配置成功

yarn run v1.21.1
$ mocha -r ts-node/register test/**/*.ts


0 passing (5ms)

Done in 3.18s.

然后说下这几个包的作用

  • chai: chai 是一个 BDD/TDD 的断言库,我们经常使用其 assert api 来做测试种断言的操作
  • mocha: mocha 是一个多功能的测试框架,我们经常使用其 describe 以及 it api 来做相关的测试
  • ts-node, typescript: ts-node 是用来编译 node 中的 typescript 用的,使用 typescript 时需要安装 typescript
  • @types/chai, @types/mocha: chai 和 mocha 的 typescript 版本,方便在 ts 文件中引入

现在就可以在 test/index.ts 中写代码了

import { assert } from 'chai'
import { describe, it } from 'mocha'

describe('测试 Chai 的使用', () => {
it('可以测试相等', () => {
assert(1 === 1)
}
})

其中 describe 表示描述测试的场景,而 it 表示在该测试场景下的对象

注意,若在断言的那段代码的上面加 //@ts-ignore 即可屏蔽掉 typescript 对这段代码的检查,那么代码在编译运行时就不会报错

对于 assert 这个 api 来说,它里面有很多的函数比如经常使用到的有如下几个

  • isXXX: 判断对象或变量的类型用的,比如 isFunctionisObject
  • throw: 这个一般用于你想让其传入的回调函数中的代码报错

使用 sinon

安装 sinon

yarn add -D sinon sinon-chai @types/sinon @types/sinon-chai

然后说下这几个包都是啥

  • sinon: 适用于任何单元测试框架的测试库
  • sinon-chai: 为 sinon 提供了一系列自自定义的断言 api,相当于 chai 的拓展
  • @types/sinon, @types/sinon-chai: sinon 和 sinon-chai 的 typescript 版本

在这里主要是使用 sinon.fake() 提供一个假函数,然后通过判断这个假函数的 called 属性来判断函数是否被调用

你需要这样引入

import * as chai from 'chai'
import * as sinon from 'sinon'
import * as sinonChai from 'sinon-chai

chai.use(sinonChai)

然后像这样使用

it('new Promise(fn) 中的函数立即执行', () => {
const fn = sinon.fake()
new Promise(fn)
assert(fn.called)
})

总结

本文主要介绍了如何搭建测试环境,以及如何使用测试框架来进行开发(TDD)。其中比较重要的还设计到了 Promise 的几个要点,即

  • Promise 要解决什么问题 - why
  • Promise 是怎么解决它的 - how
  • Promise (对比其他技术)有什么有点 - pros
  • Promise有什么缺点 - cons
  • 如何解决这些缺点

框架和环境已经搭建完毕,接下来就是遵循 Promise/A+ 规范实现一个简易 Promise