1.前言
Promise是ES6发布的异步编程的一种解决方案。比传统的解决方案--回调函数和事件,更加合理和强大。它由社区提出和实现,ES6将其写进了语言标准,统一了用法,原生提供了Promise对象。本文详细讲一下怎么手动实现一个Promise。阅读前请确保已经熟练使用Promise,要不会有些费劲。
2.实现
2.1 认识Promise
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<script>
const p = new Promise((resolve, reject) => {
console.log('new Promise')
// 这里执行异步操作,比如调接口。此处用setTimeout代替
setTimeout(() => {
resolve('Hello Promise')
}, 1000)
})
p.then(res => {
console.log('res========', res)
}, err => {
console.log('err============', err)
})
</script>
</head>
<body>
</body>
</html>
输出如下:
我们简单描述一下Pormsie
1. Promise是一个类,构造函数的参数是一个函数,这个函数的两个参数resolve和reject也是函数。
2. new 一个Promise的实例,在传入的函数里执行异步操作。该函数是同步任务会马上执行。成功则执行resolve函数,失败则执行reject函数。
3. Promise的实例有个then方法,then方法有两个参数。第一个是执行成功时的回调函数,函数参数是执行resolve函数时传入的值。第二个是执行失败时的回调函数,函数参数是执行reject函数时传入的值。then方法是异步调用的,在执行resolve或reject之后执行。
4. Promise的实例支持多次调用then方法,即链式调用。
2.2 从零开始实现Promsie
第一步首先需要一个类,为避免冲突起名为MyPromise。MyPromise的构造函数的参数是一个函数,创建MyPromise的实例时需要执行该函数。该函数有两个参数resolve和reject,这两个函数在MyPromise类中实现。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<script>
class MyPromise {
constructor(executor) {
// 执行传入的函数,为入参resolve和reject绑定this
console.log('执行传入的函数')
executor(this._resolve.bind(this), this._reject.bind(this))
}
// 实现resolve函数
_resolve() {}
// 实现reject函数
_reject() {}
}
const p = new MyPromise((resolve, reject) => {
setTimeout(() => {
resolve('Hello Promise')
}, 1000)
})
</script>
</head>
<body>
</body>
</html>
此时执行结果如下:
第二步实现Promise的状态。Promise对象有三种状态:pending(进行中)、fulfilled(已成功)、rejected(已失败),只有异步操作的结果可以决定当前是那种状态。Promise对象的状态改变只有两种情况:
1. pending变为fulfilled
2. pending变为reject
当异步操作成功时调用执行resolve函数,把异步操作的结果(Hello Promise)传给resolve,然后在then方法的回调函数中就可以取到此值。此时状态变为fulfilled。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<script>
class MyPromise {
static Pending = 'pending'
static Fulfiled = 'fulfiled'
static Reject = 'reject'
constructor(executor) {
this.status = MyPromise.Pending // Promise实例的状态,初始化为pending
this.result = undefined // result保存异步操作成功时的返回值
this.error = undefined // error保存异步操作失败时的返回值
// 执行传入的函数,为入参resolve和reject绑定this
console.log('执行传入的函数')
executor(this._resolve.bind(this), this._reject.bind(this))
}
// 实现resolve函数
_resolve(result) {
this.status = MyPromise.Fulfiled
this.result = result
}
// 实现reject函数
_reject(error) {
this.status = MyPromise.Reject
this.error = error
}
}
const p = new MyPromise((resolve, reject) => {
setTimeout(() => {
resolve('Hello Promise')
}, 1000)
})
</script>
</head>
<body>
</body>
</html>
第三步实现then方法。Promise的实例有一个then方法,接受两个回调函数为参数,第一个是成功时的回调函数,第二个是失败时的回调函数。
成功的回调函数在resolve调用之后调用,接受的参数是调用resolve时传的参数,即异步操作的结果(Hello Promise),失败的回调函数亦然。
也就是说传给then方法的回调函数不会马上执行,要等状态变化之后才会执行。就是说要把回调函数存起来,等状态变化后执行。
这类似于发布订阅模式,我们用then方法注册两个回调函数,什么时候执行呢?等resolve或reject执行时执行。下面是代码和运行结果。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<script>
class MyPromise {
static Pending = 'pending'
static Fulfiled = 'fulfiled'
static Reject = 'reject'
constructor(executor) {
this.status = MyPromise.Pending // Promise实例的状态,初始化为pending
this.result = undefined // result保存异步操作成功时的返回值
this.error = undefined // error保存异步操作失败时的返回值
this.callbacks = [] // 保存then方法传入的回调函数,在实例状态改变时调用
// 执行传入的函数,为入参resolve和reject绑定this
console.log('执行传入的函数')
executor(this._resolve.bind(this), this._reject.bind(this))
}
// 实现resolve函数
_resolve(result) {
// 调用_resolve时,表示异步操作成功。
// 此时置状态为fulfiled,把异步操作的返回结果(这里是字符串Hello Promise)赋给this.result
// 然后调用回调函数onSuccess
this.status = MyPromise.Fulfiled
this.result = result
this.callbacks.forEach((cb) => {
this._handle(cb)
})
}
// 实现reject函数
_reject(error) {
this.status = MyPromise.Reject
this.error = error
this.callbacks.forEach((cb) => {
this._handle(cb)
})
}
// then方法把传入的回调函数保存起来,当状态改变时调用。
then(onSuccess, onError) {
this.callbacks.push({ onSuccess, onError })
}
_handle(cb) {
const { onSuccess, onError } = cb
if (this.status = MyPromise.Fulfiled) {
onSuccess(this.result)
}
if (this.status === MyPromise.Reject) {
onError(this.error)
}
}
}
const p = new MyPromise((resolve, reject) => {
setTimeout(() => {
resolve('Hello Promise')
}, 1000)
})
p.then((res) => {
console.log('res===========', res)
}, (err) => {
console.log('err========', err)
})
</script>
</head>
<body>
</body>
</html>
执行结果:
到目前为止一个基本的Promise对象就实现了。
第四步是实现链式调用,这是比较难理解的地方。实例的then方法后面可以再跟then方法,前一个then方法的回调函数执行完成后,会将返回结果作为参数传入下一个then方法的回调函数。
其实链式调用无非就是返回一个类的实例。Promise对象采用的是返回一个新的Promise实例。
p.then((res) => {
console.log('res===========', res)
return res + 'aaa'
}, (err) => {
console.log('err========', err)
}).then((res1) => {
console.log('res1==========', res1)
}, (err1) => {
console.log('err1==========', err1)
})
调用then方法返回的Promise的result值来源于当前实例的成功回调函数(then的第一个参数)的执行结果,即onSuccess执行的结果。result只能在resolve方法中被赋值,因此我们也要把onSuccess执行成功的结果通过resolve的执行来传入到下一个Promise中去。
加入链式调用的处理:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<script>
class MyPromise {
static Pending = 'pending'
static Fulfiled = 'fulfiled'
static Reject = 'reject'
constructor(executor) {
this.status = MyPromise.Pending // Promise实例的状态,初始化为pending
this.result = undefined // result保存异步操作成功时的返回值
this.error = undefined // error保存异步操作失败时的返回值
this.callbacks = [] // 保存then方法传入的回调函数,在实例状态改变时调用
// 执行传入的函数,为入参resolve和reject绑定this
console.log('执行传入的函数')
executor(this._resolve.bind(this), this._reject.bind(this))
}
// 实现resolve函数
_resolve(result) {
// 调用_resolve时,表示异步操作成功。
// 此时置状态为fulfiled,把异步操作的返回结果(这里是字符串Hello Promise)赋给this.result
// 然后调用回调函数onSuccess
this.status = MyPromise.Fulfiled
this.result = result
this.callbacks.forEach((cb) => {
this._handle(cb)
})
}
// 实现reject函数
_reject(error) {
this.status = MyPromise.Reject
this.error = error
this.callbacks.forEach((cb) => {
this._handle(cb)
})
}
// then方法把传入的回调函数保存起来,当状态改变时调用。
then(onSuccess, onError) {
// 把下一个Promise实例的resolve方法也存进callbasks中去
// 是为了把当前onSuccess的执行结果通过resolve方法赋给下一个Promise实例的result
return new MyPromise((nextResolve, nextReject) => {
this._handle({
nextResolve,
nextReject,
onSuccess,
onError
})
})
}
_handle(cb) {
const {nextResolve, nextReject, onSuccess, onError } = cb
if (this.status === MyPromise.Pending) {
this.callbacks.push(cb)
}
if (this.status === MyPromise.Fulfiled) {
const nextResult = onSuccess ? onSuccess(this.result) : undefined
nextResolve(nextResult)
return
}
if (this.status === MyPromise.Reject) {
const nextError = onError ? onError(this.error) : undefined
nextReject(nextError)
return
}
}
}
const p = new MyPromise((resolve, reject) => {
setTimeout(() => {
resolve('Hello Promise')
}, 1000)
})
// 调用第一个then方法之后,会创建下一个Promise对象。
// 会把当前Promise对象的成功回调函数、失败回调函数、下一个Resolve方法、下一个Reject方法存起来。
// 然后在调用onSuccess之后,把其返回值作为nextResolve的参数,赋给下一个Promise对象的result1,
// 这样第二个then方法的成功回调函数就会取到result1,以此类推。
p.then((res) => {
console.log('res===========', res)
return res + 'aaa'
}, (err) => {
console.log('err========', err)
}).then((res1) => {
console.log('res1==========', res1)
}, (err1) => {
console.log('err1==========', err1)
})
</script>
</head>
<body>
</body>
</html>
运行结果如下:
第五步再深入一点。上面只说了then方法返回一个字符串(Hello Promise)的情况,如果then方法返回一个Promise实例呢?如下:
p.then((res) => {
console.log('res==========', res)
return new MyPromise((resolve, reject) => {
setTimeout(() => {
resolve('Test Promise')
}, 2000)
})
}, (err) => {
console.log('err========', err)
}).then((res1) => {
console.log('res1==========', res1)
}, (err1) => {
console.log('err1==========', err1)
})
此时第二个then方法会等待这个返回的Promise执行完毕后执行。代码处理如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<script>
class MyPromise {
static Pending = 'pending'
static Fulfiled = 'fulfiled'
static Reject = 'reject'
constructor(executor) {
this.status = MyPromise.Pending // Promise实例的状态,初始化为pending
this.result = undefined // result保存异步操作成功时的返回值
this.error = undefined // error保存异步操作失败时的返回值
this.callbacks = [] // 保存then方法传入的回调函数,在实例状态改变时调用
// 执行传入的函数,为入参resolve和reject绑定this
console.log('执行传入的函数')
executor(this._resolve.bind(this), this._reject.bind(this))
}
// 实现resolve函数
_resolve(result) {
if (result instanceof MyPromise) {
// 表示onSuccess返回结果是一个Promise实例,假定这个实例为a
// 我们是想获取这个叫a的Promise实例里面的result,但是a实例并没有暴露任何获取result的方法
// 但是a实例的then方法里有个成功的回调函数,这个回调函数里可以取到result。
result.then(
this._resolve.bind(this),
this._reject.bind(this)
)
return
}
this.status = MyPromise.Fulfiled
this.result = result
this.callbacks.forEach((cb) => {
this._handle(cb)
})
}
// 实现reject函数
_reject(error) {
if (error instanceof MyPromise) {
error.then(
this._resolve.bind(this),
this._reject.bind(this)
)
}
this.status = MyPromise.Reject
this.error = error
this.callbacks.forEach((cb) => {
this._handle(cb)
})
}
// then方法把传入的回调函数保存起来,当状态改变时调用。
then(onSuccess, onError) {
// 把下一个Promise实例的resolve方法也存进callbasks中去
// 是为了把当前onSuccess的执行结果通过resolve方法赋给下一个Promise实例的result
return new MyPromise((nextResolve, nextReject) => {
this._handle({
nextResolve,
nextReject,
onSuccess,
onError
})
})
}
_handle(cb) {
const {nextResolve, nextReject, onSuccess, onError } = cb
if (this.status === MyPromise.Pending) {
this.callbacks.push(cb)
}
if (this.status === MyPromise.Fulfiled) {
const nextResult = onSuccess ? onSuccess(this.result) : undefined
nextResolve(nextResult)
return
}
if (this.status === MyPromise.Reject) {
const nextError = onError ? onError(this.error) : undefined
nextReject(nextError)
return
}
}
}
const p = new MyPromise((resolve, reject) => {
setTimeout(() => {
resolve('Hello Promise')
}, 1000)
})
// 调用第一个then方法之后,会创建下一个Promise对象。
// 会把当前Promise对象的成功回调函数、失败回调函数、下一个Resolve方法、下一个Reject方法存起来。
// 然后在调用onSuccess之后,把其返回值作为nextResolve的参数,赋给下一个Promise对象的result1,
// 这样第二个then方法的成功回调函数就会取到result1,以此类推。
p.then((res) => {
console.log('res==========', res)
return new MyPromise((resolve, reject) => {
setTimeout(() => {
resolve('Test Promise')
}, 2000)
})
}, (err) => {
console.log('err========', err)
}).then((res1) => {
console.log('res1==========', res1)
}, (err1) => {
console.log('err1==========', err1)
})
</script>
</head>
<body>
</body>
</html>
执行结果如下: