最近这段时间因为疫情的缘由,在家实在闷得慌,因此看了下 js 的一些基础知识,从前不是很了解的 Promise 忽然豁然开朗了很多,因而就赶忙趁热打铁写下来(这就是温故而知新的感受吗,哈哈哈😁)。git
确实是待久了,🌸樱花🌸都开了。github
要想知道某个东西怎么写的,就要先学会用,因此读者大大们若是还有没用过的话,赶忙去学下再来看吧,由于本文的目标是手写一个 Promise 以及 catch、finally、all、race、resolve 等附加方法,下面是最简单的用法展现👇:数组
let p = new Promise((resolve, reject) => { // 这里面的代码是同步执行的
resolve(1)
}).then(res => { // 这里面的代码是异步的
console.log('成功', res)
}, err => {
console.log('错误', err)
})
// 成功 1
复制代码
事实上 Promise 是有个 Promises/A+ 规范的(这东西至关于一个需求文档),这里我会先罗列几个必知必会的点,毕竟要手写嘛✍,肚子里得有点墨水:promise
下面的描述我也没写的规范那么正经,由于我有时候总以为那样不利于理解,一堆英文名字就把你给愣住了😯。并发
首先咱们把前面提到的基础用法简化下:框架
let p = new Promise(fn).then(fn1, fn2)
复制代码
这样一来上面的结构就明了许多,既然须要 new,那么它首先是个构造函数,接收一个函数参数 fn,而后实例有个 then 方法,接收两个参数 fn1 和 fn2,也都是函数。此外由知识前置里面的内容可知 Promise 里面自身得维护一个状态,因此咱们能够先写出一个大致框架:dom
class MyPromise {
constructor(fn) {
this.status = 'pending' // 保存状态
this.successValue = null // 保存成功的值
this.failValue = null // 保存失败的值
fn() // 由于 Promise 里面的代码是同步执行的,因此直接进来须要直接调用
}
then(successFn, failFn) {}
}
复制代码
咱们知道在执行 fn 的时候其实还有两个参数,就是 fn(resolve, reject),因此咱们须要完善它,在 MyPromise 里面定义 resolve 和 reject 这两个函数,当外界调用 resolve 和 reject 时就是改变 MyPromise 里面的状态和值,并触发相应的回调函数,就像下面这样:异步
constructor(fn) {
this.status = 'pending' // 保存状态
this.successValue = null // 保存成功的值
this.failValue = null // 保存失败的值
let resolve = (successValue) => { // 这个 successValue 是外部调用传进来的值
this.status = 'success'
this.successValue = successValue
}
let reject = (failValue) => { // 这个 failValue 是外部调用传进来的值
this.status = 'fail'
this.failValue = failValue
}
fn(resolve, reject)
}
复制代码
constructor 里面的东西大概写完了,接下来咱们简要写下 then 方法,then 方法里面不是有两个函数参数吗,根据 status 的状态执行其中一个便可,就像下面这样:函数
then(successFn, failFn) {
if (this.status === 'success') {
successFn(this.successValue)
} else if (this.status === 'fail') {
failFn(this.failValue)
}
}
复制代码
ok,咱们来测试一下:测试
let p = new MyPromise((resolve, reject) => {
console.log('1')
resolve(100)
}).then(res => {
console.log('2')
console.log('成功', res)
}, err => {
console.log('错误', err)
})
console.log('3')
// 1
// 2
// 成功 100
// 3
复制代码
不错不错,能够成功打印出 100,可是有个问题,console.log
的顺序应该是 一、三、2,由于 then 里面的内容是异步执行的,因此咱们须要 setTimeout 来简单模拟下,把 then 操做延后,就像下面这样:
then(successFn, failFn) {
if (this.status === 'success') {
setTimeout(() => {
successFn(this.successValue)
})
} else if (this.status === 'fail') {
setTimeout(() => {
failFn(this.failValue)
})
}
}
复制代码
不错不错,看起来好像能够了,可是若是我这样写呢:
let p = new MyPromise((resolve, reject) => { // 连续调用
resolve(100)
reject(-1)
}).then(res => {
console.log('成功', res)
}, err => {
console.log('错误', err)
})
// 错误 -1
复制代码
上面结果输出 -1 固然是错的,由于连续调用 resolve 或 reject 是无效的,Promise 只容许被改变一次,因此咱们须要加个限制条件:
let resolve = (successValue) => {
if (this.status !== 'pending') return // 状态已经改变过就不往下执行了
this.status = 'success'
this.successValue = successValue
}
let reject = (failValue) => {
if (this.status !== 'pending') return // 状态已经改变过就不往下执行了
this.status = 'fail'
this.failValue = failValue
}
复制代码
固然还没完,一个新的问题诞生了,若是我这样写呢:
let p = new MyPromise((resolve, reject) => {
setTimeout(() => {
resolve(100)
}, 2000);
}).then(res => {
console.log(res)
}, err => {
console.log(err)
})
复制代码
没有输出任何东西,这是由于当我调用 then 的时候,MyPromise 里面的东西还没执行完,状态未被改变,因此 then 里面的成功和失败都不会调用,所以咱们须要在 then 中加上一种状况,就是若是 MyPromise 里面还没执行完就先把 then 中的 fn1 和 fn2 放进数组中存起来,等到 MyPromise 里面执行完再把数组拿出来遍历执行(固然也要放进 setTimeout 中),就像下面这样(接下来都用图了😁):
写到这里,其实还有个大问题,前面说过了,then 是有返回的,而且是个新的 Promise,还支持链式调用,显然咱们的并不具有这样的功能,因此如今咱们须要完善 then,主要思想就是在 then 里面套一层 Promise,此外你要知道若是 then(fn1, fn2) 继续向下传递的话,传递的值是 fn1 或 fn2 的返回值,看下面这张图你可能会清晰点:
let p = new MyPromise((resolve, reject) => {
resolve(100)
}).then(res => {
console.log('成功', res)
// return 0
return new MyPromise((resolve2, reject2) => {
setTimeout(() => {
resolve2(0)
}, 1000)
})
}, err => {
console.log('错误', err)
}).then(res2 => {
console.log('成功2', res2)
}, err2 => {
console.log('错误2', err2)
})
// 成功 100
// 成功2 0 (1s后打印出来)
复制代码
写到这里,then 里面的东西已经写的差很少了,但其实仍是有问题的,咱们这里只是解决了一层 Promise 的嵌套,若是你多嵌套几个 Promise 就不行了,这个须要咱们把上面公共的部分提取出来而后递归调用,写起来不复杂,但会有点绕容易晕,此外咱们也没有对循环调用同一个 Promsie 作判断以及一些异常捕获,由于咱们理解到这里就差很少了👏。固然了,我会在文末附上完整的代码😬,里面也有详细的注释。
什么是穿透呢?让咱们来看下面的代码:
let p = new Promise((resolve, reject) => {
resolve(100)
}).then()
.then(1)
.then(res => {
console.log('成功', res)
}, err => {
console.log('错误', err)
})
// 成功 100
复制代码
简单来讲就是,若是 then 中的参数不是函数或为空,then 以前的值还可以继续向下传递,其实这个写起来很简单,就是在 then 里面的一开始判断下参数是否为函数,不是的话就包装成函数,并把以前的值看成返回值,具体操做以下:
return new Error('xxx')
,由于这不是抛出错误,是返回一个错误对象,等价于
return {}
,它是个正常的返回值,而不是错误,好好体会一下。
catch 这东西其实和 then 一毛同样,只不过不须要成功回调,promise.catch(fn) 至关于 promise.then(null, fn),也就是说若是 catch 后面若是还有 then 也是能够继续执行的,咱们直接看下面的代码就了解了:
这里咱们先简要看一下 Promise.resolve 的用法:
Promise.resolve(1).then(res => console.log(res))
Promise.resolve(
new Promise((resolve, reject) => resolve(2))
).then(res => console.log(res))
// 1
// 2
复制代码
首先 Promise.resolve 是个静态方法(就是只能用类来调用,比如 Math.random()
),它能够进行链式调用,因此它返回的也是个 Promise,只不过要注意的是 resolve 里面接收的参数能够是 Promise 和通常值(数字、字符串、对象等),若是是 Promise 则须要等这个参数 Promise 执行完再返回,让咱们看下下面的代码:
finally 的特色是不论正确与否,它总会执行,接收一个函数参数,而且返回 Promise,不过要注意的是它向下传递的值是上一次的值而不是 finally 中的值,具体以下:
仍是同样咱们先来看下具体用法:
MyPromise.all([new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1)
}, 1000);
}), 2, 3]).then(res => {
console.log(res)
})
// [1, 2, 3] (1s后打印)
复制代码
首先 all 方法接收一个数组参数,数组的每一项能够是 Promise,也能够是常量等有返回值的东西;其次使用 all 方法以后也能够用 then 进行链式调用,因此 all 方法返回的也是个 Promise;最后 all 是并发执行,其实就是写个循环,不过只有所有成功才算是成功,不然就算失败,直接看下面的代码:
race 其实和上面的 all 差很少,可是规则有点不同,它也是接收一个数组,只不过返回的就一项,最早返回成功就成功,最早失败就失败,代码也是和上面雷同,具体以下:
所谓最好的输入就是输出,一晃又是几个月没写文章了,因此特此沉淀,仍是心虚啊。但愿本篇文章可以对你有所帮助,不知道写的清不清楚😁。最后祝福你们百毒不侵,回见👋。
ps:Gituhub 代码地址