学习笔记—Promise的介绍与实现

日常的学习笔记,包括 ES6、Promise、Node.js、Webpack、http 原理、Vue 全家桶,后续可能还会继续更新 Typescript、Vue3 和 常见的面试题 等等。

Promise

参考文献 Promise|MDN

Promise 出现的原因:处理多个并发请求,利用 链式调用 解决了 回调地狱 的问题。

Promise 存在三种状态,成功(resolve)失败(reject)等待(pending)

首先, Promise是一个类 ,需要通过关键字 new 来进行实例化。

Promise接受一个 executor 函数作为执行器,执行器是立即执行的。同时又会接受两个参数作为 成功失败 的回调。

当我们不去执行 成功失败 的回调,当前这个Promise的状态就会维持在 等待 状态。Promise类会返回一个Promise类,方便下一次调用。

1
2
let promise = new Promise((resolve, reject)=>{})
console.log(promise) // Promise {<pending>}

Promise 实例的返回值会根据调用的函数,来判断当前返回的是 成功状态失败状态,并且会将传入参数返回。在调用函数时,若不传入参数,则会返回 undefined

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 什么都不传
let promise = new Promise((resolve, reject) => {
resolve()
})
console.log(promise); // Promise { undefined }

// 成功状态
let promise = new Promise((resolve, reject) => {
resolve('success')
})
console.log(promise); // Promise { 'success' }

// 失败状态
let promise = new Promise((resolve, reject) => {
reject('failed')
})
console.log(promise); // Promise { <rejected> 'failed' }

每一个Promise的实例上,都有一个 .then 方法输出上一个实例传入的结果。当前实例状态被改变后,将无法再进行改变。

1
2
3
4
5
6
7
let promise = new Promise((resolve, reject) => {
resolve('success')
}).then((result) => {
console.log(result); // success
}, (error) => {
console.log(error);
})

这样的话,我们就可以总结出来 Promise 的几个特点。

特点

  1. Promise 是一个类,无需考虑兼容性等问题。
  2. Promise 会传入一个函数(executor)作为执行器,此执行器是立即执行的。
  3. executor 提供了两个函数(resolvereject)用来描述当前 Promise 的状态,而当前实例存在三种状态,成功状态失败状态等待状态 ,当前实例默认为 等待状态*。如果调用 resolve 则状态变为 *成功状态 ,调用 reject 或 发生异常 则状态变为 失败状态
  4. Promise 一旦状态变化后,则不能再更改。
  5. 每个 Promise 实例都有一个 .then 方法。

我们可以根据 Promise 的几个特点,手写一套属于自己的 Promise

手写实现 Promises/A+ 规范

文档规范 Promises/A+

注:代码内容为连续内容,请依序观看。谢谢

Promise的基础功能

根据上述特点,我们就可以简单实现出 Promise 的效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
const PEDDING = 'PEDDING'; // 等待状态
const FULFILLED = 'FULFILLED'; // 成功状态
const REJECTED = 'REJECTED'; // 失败状态
class Promise {
constructor(executor) {
this.status = PEDDING; // 默认状态
this.result = undefined; // 成功的回调
this.reason = undefined; // 失败的回调
const resolve = (result) => { // 成功 resolve 函数
if (this.status === PEDDING) {
this.status = FULFILLED; // 修改状态
this.result = result; // 添加回调
}
}
const reject = (reason) => { // 失败 reject 函数
if (this.status === PEDDING) {
this.status = REJECTED; // 修改状态
this.reason = reason; // 添加回调
}
}
try {
executor(resolve, reject)
} catch (error) {
this.reason = error;
}
}
then(onFulfilled, onRejected) {
if (this.status === FULFILLED) { // 成功时调用的方法
onFulfilled(this.result)
}
if (this.status === REJECTED) { // 失败时调用的方法
onRejected(this.reason)
}
}
}
module.exports = Promise

参考 Promise A+规范,我们可以简单实现出来一版 Promise 类的简易实现版。

实现Promise的异步功能

实现 Promise 的异步,我们需要先明确,Promise中只有在触发 .then 方法时(也就是resolvereject ),才是异步的。所以我们利用这样一个思路。

当用户调用 .then 方法时,Promise 此时可能是 等待状态,我们需要先将其暂存起来。后续调用 resolvereject 时,再去触发对应的 onFulfilledonRejected

根据上面的描述,我们可以捕捉到 暂存触发 这两个关键词,那么我们就可以使用 发布订阅 的设计模式来实现此功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// ...
class Promise {
constructor(executor) {
// ...
this.onResolveCallbacks = []; // 用来存储 成功的回调
this.onRejectCallbacks = []; // 用来存储 失败的回调
const resolve = (result) => {
if (this.status === PEDDING) {
// ...
this.onResolveCallbacks.forEach(fn => fn())
}
}
const reject = (reason) => {
if (this.status === PEDDING) {
// ...
this.onRejectCallbacks.forEach(fn => fn())
}
}
// ...
}
then(onFulfilled, onRejected) {
if (this.status === PEDDING) {
this.onResolveCallbacks.push(() => {
onFulfilled(this.result)
})
this.onRejectCallbacks.push(() => {
onRejected(this.reason)
})
}
// ...
}
}
module.exports = Promise

建立两个 用来存储回调函数的数组,先将需要执行的函数存储进数组 中。当异步执行完后,再依次 执行数组内存储的函数

Promise链式调用

首先我们先要清楚,Promise的出现解决了哪些问题?

  • 处理多个并发请求
  • 链式调用解决了回调地狱的问题

回调地狱是什么? 回调地狱就是我们平时在处理业务代码时,下一个接口的api参数需要用到上一个接口的参数。代码上可能就会出现多级嵌套的情况,导致代码阅读起来十分困难。

这里我们就需要用到Promise的链式调用,也就是 .then 方法的循环调用,当调用 .then 方法后,会返回一个新的Promise。

我们先封装一个Promise的异步函数

1
2
3
4
5
6
7
8
9
let fs = require('fs');
function readFile(path, encoding) {
return new Promise((resolve, reject) => {
fs.readFile(path, encoding, (err, data) => {
if (err) reject(err)
resolve(data)
})
})
}

现在我们需要清楚链式调用出现的几种情况。

  1. .then 方法返回的是一个 普通值(不是 Promise) 的情况下,会作为外层下一次.then 方法的 成功结果

    1
    2
    3
    4
    5
    6
    7
    8
    9
    readFile('./a.txt', 'utf8').then((result) => {
    return 1;
    }, (err) => {
    console.log(err);
    }).then((result) => {
    console.log(result); // 1
    }, (err) => {
    console.log(err);
    })
  2. .then 方法执行出错,会走到外层下一次.then 方法的 失败结果

    1
    2
    3
    4
    5
    6
    7
    8
    9
    readFile('./a.txt', 'utf8').then((result) => {
    throw new Error('error')
    }, (err) => {
    console.log(err);
    }).then((result) => {
    console.log(result);
    }, (err) => {
    console.log(err); // Error: error
    })

    (注:执行错误需要 throw new Error(),如果直接使用 return new Error(),属于返回一个Error对象,会执行下一次的成功结果)

  3. 无论上一次 .then 方法执行结果是 成功 还是 失败,只要返回的是普通值,都会执行下一次.then 方法的 成功结果

    路径填写错误,Promise会默认执行第一层.then方法的错误结果,并返回undefined。则下一层的执行结果是成功结果值为undefined

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 路径填写错误
    readFile('./a.txt1', 'utf8').then((result) => {
    console.log(result)
    }, (err) => {
    // 相当于在此处 return undefined
    console.log(err); // 错误原因
    }).then((result) => {
    console.log(result); // undefined
    }, (err) => {
    console.log(err);
    })
  4. 如果 .then 方法返回的是一个 Promise 对象,此时会根据 Promise 的结果来处理是成功结果还是失败结果(传入的是成功或失败的内容)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    readFile(`${bathPath}a.txt`, 'utf8').then((result) => {
    return readFile(`${bathPath}${result}`, 'utf8')
    }, (err) => {
    console.log('err1', err);
    }).then((result) => {
    console.log('success2', result); // success2 b
    }, (err) => {
    console.log('err2', err); // error
    })

(总结:如果返回的是一个普通值(不是 Promise),就会传递给下一次 .then 方法的成功。如果返回的是一个失败的Promise 或者 抛出异常,就会传递给下一次 .then 方法的失败。)

手写实现promise链式调用

根据上述特点和情况,我们每次在 .then 方法调用后都要返回一个新的 Promise 实例。所以我们可以对之前写好的 .then 方法进行相应的修改。

我们首先来处理 普通值(不是 Promise) 的情况。

注:在这里我们单独提出来了一个 x,用来进行后续处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 对 .then() 方法进行改写
then(onFulfilled, onRejected) {
let promise = new Promise((resolve, reject) => { // 返回一个 promise 实例
if (this.status === FULFILLED) {
try {
let x = onFulfilled(this.result)
resolve(x);
} catch (e) {
reject(e)
}
}
if (this.status === REJECTED) {
try {
let x = onRejected(this.reason)
resolve(x)
} catch (e) {
reject(e)
}
}
if (this.status === PEDDING) {
this.onResolveCallbacks.push(() => {
try {
let x = onFulfilled(this.result)
resolve(x);
} catch (e) {
reject(e)
}
})
this.onRejectCallbacks.push(() => {
try {
let x = onRejected(this.reason)
resolve(x)
} catch (e) {
reject(e)
}
})
}
})
return promise
}

利用上述思路对之前的方法进行改造,这样我们就可以对 普通值 进行处理。

上述处理 普通值 的情况,我们可以稍加改动,使其可以处理更多的情况。为此我们需要封装一个 resolvePromise() 函数来进行处理。

resolvePromise()需要接受四个参数,分别是 当前实例promise结果x成功回调resolve失败回调reject

为了可以将当前实力promise作为参数传递,我们需要先用异步方法 setTimeout (其他方法也可以) 将其进行封装。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
then(onFulfilled, onRejected) {
let promise = new Promise((resolve, reject) => { // 返回一个 promise 实例
if (this.status === FULFILLED) {
setTimeout(() => {
try {
let x = onFulfilled(this.result)
// 在此处进行封装处理
resolvePromise(promise, x, resolve, reject);
} catch (e) {
reject(e)
}
}
}
// ... 后面代码进行同样的修改
})
return promise
}

这样我们就可以读取到 promise实例 了,下面我们来实现 resolvePromise() 函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function resolvePromise(promise, x, resolve, reject) {
if (promise === x) {
return reject(new TypeError('错误'))
}
// promise 兼容性
if ((typeof x === 'object' && x !== null) || typeof x === 'function') {
try {
let then = x.then // 通过defineProperty实现时,取值时可能会有异常
if (typeof then === 'function') {
then.call(x, y => {
resolve(y)
}, r => {
reject(r)
})
} else {
resolve(x)
}
} catch (e) {
reject(e)
}
} else {
// 普通值
resolve(x)
}
}

注:在工作中,我们可能会调用别人封装的Promise,里面可能会有问题。所以我们还需要进行一步处理,也就是在代码里面加个*锁*,确保代码的严谨性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function resolvePromise(promise, x, resolve, reject) {
// ...
if ((typeof x === 'object' && x !== null) || typeof x === 'function') {
let called = false; // 定义一个参数
try {
let then = x.then // 通过defineProperty实现时,取值时可能会有异常
if (typeof then === 'function') {
then.call(x, y => {
// 在这里进行异常判断
if (called) return
called = true
resolve(y)
}, r => {
if (called) return
called = true
reject(r)
})
} else {
resolve(x)
}
} catch (e) {
if (called) return
called = true
reject(e)
}
} else {
// ...
}
}

这样我们就实现了 Promise 的链式调用。

特殊情况处理

嵌套Promise

可能还会出现这种情况,我们在 .then 方法的 resolve 中传入一个 Promise实例 ,这种情况我们要如何处理呢?

如下情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let promise = new Promise((resolve, reject) => {
resolve(1)
}).then(() => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(new Promise((resolve, reject) => {
setTimeout(() => {
resolve(200)
}, 1000);
}))
}, 1000);
})
})

promise.then(result => {
console.log(result);
}, err => {
console.log(err);
})

针对上述特殊情况,我们需要继续对之前的resolvePromise() 函数 进行改造。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function resolvePromise(promise, x, resolve, reject) {
// ...
if ((typeof x === 'object' && x !== null) || typeof x === 'function') {
// ...
try {
// ...
if (typeof then === 'function') {
then.call(x, y => {
// ...
// 一直解析,直到不是 Promise 为止
resolvePromise(promise, y, resolve, reject)
}, r => {
// ...
})
} else {
resolve(x)
}
} catch (e) {
// ...
}
} else {
// ...
}
}

关键点就是在于递归调用,直到其值为普通值为止。

参数穿透

我们在调用 .then方法 时,还会出现下面这种情况

1
2
3
4
5
6
7
8
new Promise((resolve, reject) => {
resolve(100)
// reject('err')
}).then().then().then().then(result => {
console.log(result); // 100
}, err => {
console.log(err); // 如果传入,则输出 err
})

不传入参数的情况下,结果会一直进行传递,直到输出为止。

这种参数穿透的情况,我们也需要在代码上进行改造。

1
2
3
4
5
6
then(onFulfilled, onRejected) {
// 对 onFulfilled 进行处理
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : v => v;
onRejected = typeof onRejected === 'function' ? onRejected : err => {throw err}; // 抛出的情况下才会输出错误结果,所以要用 throw
// ...
}

Promise测试

我们可以对自己封装的Promise进行测试,需要用到测试包 promises-aplus-tests

在Promise实例目录下执行如下代码

1
2
npm install promises-aplus-tests -g
promises-aplus-tests ./promise.js

他会自动检测我们封装的Promise是否符合 Promise A+ 规范。

在我们封装的 Promise 文件下添加 延迟对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Promise {
// ... 自己封装的 Promise
}

// 需要进行测试用的代码
Promise.deferred = function () {
let dfd = {};
dfd.promise = new Promise((resolve, reject) => {
dfd.resolve = resolve
dfd.reject = reject
})
return dfd
}

module.exports = Promise

注:catchall 等都不属于Promise规范中包含的方法

检测完后,我们可以看到其输出结果,根据结果我们可以清楚自己封装的 Promise 是否可以正常运行。

至此,我们就封装好了一个Promise

延迟对象

用来帮我们减少一次套用,应用并不算广泛。有点类似于代理。

我们可以对最一开始我们自己的 readFile读取操作 进行封装。

1
2
3
4
5
6
7
8
function readFile(path, encoding) {
let dfd = Promise.deferred();
fs.readFile(path, encoding, (err, data) => {
if (err) return dfd.reject(err)
dfd.resolve(data)
})
return dfd.promise;
}

本篇文章由莫小尚创作,文章中如有任何问题和纰漏,欢迎您的指正与交流。
您也可以关注我的 个人站点博客园掘金,我会在文章产出后同步上传到这些平台上。
最后感谢您的支持!

请打赏并支持一下作者吧~

欢迎关注我的微信公众号