前言
在了解Promise之前,我们先来了解一下什么是回调地狱,异步任务。
异步任务
异步任务对于初学者来说可能比较陌生,与异步任务相对的是同步任务。
众所周知JS是单线程的编译语言,这也导致了他有许多的弊端,必须等到前面的一个任务结束了才能开始下一个任务,如果前面一个任务需要耗时很久,而后面的任务很快就能完成,这就会导致效率降低。为了解决这一问题,出现了异步任务。
同步任务是在主线程上排队执行的,只有前一个任务执行完毕,才能执行下一个任务。
异步任务是那些放在一边,没有进入主线程,而是进入任务队列的任务。前一个任务是否完成,不影响下一个任务的执行。
function callback() {
console.log('Done');
}
console.log('before setTimeout()');
setTimeout(callback, 1000); // 1秒钟后调用callback函数
console.log('after setTimeout()');
按同步任务的逻辑来看,他的输出应该是before setTimeout(),Done,*after setTimeout()*这一执行顺序,可事实并非如此。
setTimeout()是一个异步函数,当代码执行到这一函数时,先将setTimeout()函数放入任务队列,当主线程中的同步任务全部执行完,也就是执行到after setTimeout()后,JS开始将异步任务推入主线程中开始执行。
回调地狱
在前端开发中,一开始为了处理异步请求的函数,我们经常在请求成功的函数内继续写函数。
function load() {
$.ajax({
url: 'xxx.com',
data: 'jsonp',
success: function(res) {
init(res, function(res) {
render(res, function(res) {
// 一层一层又一层
});
});
}
}
}
load();
当嵌套的层数越来越多的时候,出现回调函数层层嵌套的时候就会出现回调地狱。
如此一层又一层的嵌套函数,会导致我们的代码可读性极差,难以重构,后期项目的维护难度非常大,最终导致出现屎山一样的代码。为了避免出现这种现象,在ES6中,我们提出了Promise函数来避免在异步请求中出现这一问题。
Promise
Promise是ES6新增的引用类型,它主要有以下三种状态。
- 等待态(Pending):初始状态
- 执行态(Fulfilled):操作完成状态
- 拒绝态(Rejected):操作失败状态
状态只能由Pending变为Fulfilled或是从Pending变为Fulfiilled,当状态发生改变后,就不会再发生变化。
Pending变为Fulfilled会得到一个私有的value,Pending变为Rejected会得到一个私有的reason,当Promise达到Fulfilled或是Rejected时,执行异步代码就会得到value或是reason。
Promise本身只是一个容器,真正实现异步的是它的两个回调resolved()和rejected()
1.1 基础使用
Promise的构造函数接收一个函数作为参数,这个传入的函数有两个参数resolve和*reject。
resolve在函数执行成功时调用,将Promise的状态由等待态变为成功,将异步操作的结果作为参数传递过去;
reject在异步操作失败时调用,将Promise的状态由等待态变为拒绝态,将异步操作报出的错误作为参数传递出去。
在函数创建完后,可以使用then方法分别指定成功和失败的回调函数。
let p = new Promise((resolve,reject) => {
resolve("通过了")
})
p.then((data) => {
console.log('success'+data);
}, (error) => {
console.log(error);
})
当执行了reject()函数后,不会再执行后续函数
let p = new Promise((resolve,reject) => {
reject('拒绝了')
resolve("通过了")
})
p.then((data) => {
console.log('success'+data);
}, (error) => {
console.log(error);
})
1.2 then方法
- then方法下一次的输出需要上一次的输入
- 如果一个promise执行完后,返回的还是一个promise,会把这个promise的执行结果,传递给下一次then中。
- 如果then中返回中返回的不是一个promise对象而是一个普通的值,则会将这个结果作为下一次then成功的结果。
- 如果当前的then失败了,会走向下一个then的失败。
- 如果返回的是undefined,不管当前是成功还是失败,都会走向下一次的成功。
- catch是错误没有处理的情况才会往下走
- then中如果不写方法则值会穿透,传递到下一个then中。
手写Promise
Promise的使用方法很简单,但为了搞懂Promise的真正过程,手写一个Promise可能更有效。
实现resolve,rejecte
在实现Promise,我们先来看一下Promise中的resolve和reject函数
let p1 = new Promise((resolve,reject) => {
resolve('成功')
reject('失败')
})
console.log('p1',p1);
let p2 = new Promise((resolve,reject) => {
reject('失败')
resolve('成功')
})
console.log('p2',p2);
let p3 = new Promise((resolve,reject) => {
throw('报错')
})
console.log('p3',p3);
这一段代码的输出如下
从输出我们可以看出
- 执行了resolve,Promise的状态就会变成fulfilled
- 执行了rejec,Promise的状态就会变成rejected
- Promise只以第一次为准,当第一次成功后就永久变成了fulfilled;当第一次失败后就永久变成了rejected
- Promise中有throw的话,就相当于执行了rejected
除了以上四点,我们还要知道,Promise一开始的状态是pending,并且在函数执行的过程中,我们需要将resolve和reject绑定this,这是为了防止使resolve和reject的this指向永远指向当前的Promise实例,防止因为函数执行环境的变化而变化。
现在我们可以简单的实现resolve和reject
class myPromise {
constructor(executor) {
// 初始化值
this.initValue()
// 初始化this指向
this.initBind()
executor(this.resolve,this.reject)
}
initBind() {
// 初始化this指向
this.resolve = this.resolve.bind(this)
this.reject = this.reject.bind(this)
}
initValue() {
// 初试化值
this.PromiseResult = null //终值
this.PromiseState = 'pending' //状态
}
resolve(value) {
// 如果执行resolve,则将状态变为fulfilled
this.PromiseState = 'fulfilled'
this.PromiseResult = value
}
reject(reason) {
// 如果指向reject,则将状态变为rejected
this.PromiseState = 'rejected'
this.PromiseResult = reason
}
}
通过实例来测试一下代码
const p1 = new myPromise((resolve,reject) => {
resolve('成功')
})
console.log('p1',p1);
const p2 = new myPromise((resolve,reject) => {
reject('失败')
})
console.log('p2',p2);
输出如下
到这里,我们可以简单实现resolve和reject函数,但现在仍有很大的缺陷。当我们把测试代码改成如下。
const p1 = new myPromise((resolve,reject) => {
resolve('成功')
reject('失败')
})
按上面的逻辑,p1输出的状态应该为fulfilled,输出的值应该为成功,可事实如此吗?
从输出我们可以看出,这段代码中Promise的状态改变后是可以再变的,显然是有问题的。
在Promise中,它的状态主要有三个。
- pending: 初始状态,等待中。
- fulfilled: 成功状态。
- rejected: 失败状态。
将resolve和reject的代码改进如下:
resolve(value) {
// 判断状态,当状态改变后就不可变
if(this.PromiseState !== 'pending') return
// 如果执行resolve,则将状态变为fulfilled
this.PromiseState = 'fulfilled'
this.PromiseResult = value
}
reject(reason) {
if(this.PromiseState !== 'pending') return
// 如果指向reject,则将状态变为rejected
this.PromiseState = 'rejected'
this.PromiseResult = reason
}
测试一下
const p1 = new myPromise((resolve,reject) => {
resolve('成功')
reject('失败')
})
得到输出如下
到这里我们基本上实现了resolve和reject,离实现基本的Promise只差throw了
实现throw
Promise中有throw的话,就相当于执行了reject。此时就要使用try/catch。我们可以在构造函数中添加如下代码。
constructor(executor) {
// 初始化值
this.initValue()
// 初始化this指向
this.initBind()
try {
executor(this.resolve, this.reject)
} catch (error) {
this.reject(error)
}
}
测试一下
const p1 = new myPromise((resolve,reject) => {
throw('失败')
})
console.log('p1',p1);
输出如下
至此,我们已经基本上实现了Promise的基本功能了。
then
我们使用Promise时,更多的是因为它的then方法,这个方法可以防止出现回调地狱。接下来我们来实现then方法。
我们平时使用then如下
// 马上输出成功
const p1 = new Promise((resolve, reject) => {
resolve('成功')
}).then(res => console.log(res), err => console.log(err))
// 1秒后输出成功
const p2 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('成功')
}, 1000);
}).then(res => console.log(res), err => console.log(err))
// 链式调用 输出 2
const p3 = new Promise((resolve, reject) => {
resolve(1)
}).then(res => 2 * res, err => console.log(err)).then(res => console.log(res), err => console.log(err))
从上面我们可以看出
- then接收两个回调,一个是成功的回调,一个是失败的回调。
- 当Promise状态为fulfilled时,执行成功回调;当状态为rejected时,执行失败回调。
- 当resolve和rejecte在定时器里,则当定时器结束时再执行then。
- then支持链式调用,下一次then执行受上一次then返回值的影响
通过上面几点,我们就可以来尝试实现then
then(onFulfilled,onRejected) {
// 接收两个回调函数onFulfilled和onRejected
// 参数校验,确保传入的参数一定是函数
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : val => val
onRejected = typeof onRejected === 'function' ? onRejected : reason => {throw reason}
if (this.PromiseState === 'fulfilled') {
// 如果当前为成功状态,则执行第一个回调
onFulfilled(this.PromiseResult)
} else if (this.PromiseState === 'rejected') {
// 如果当前状态为失败状态,则执行第二个回调
onRejected(this.PromiseResult)
}
}
测试一下
const p1 = new myPromise((resolve,reject) => {
resolve('成功')
}).then(res => console.log(res),err=>console.log(err))
得到输出如下
到此,我们已经基本上实现了then函数的功能,但当我们遇到计数器时,如何等到计时器结束后才去执行then里面的回调。
这一问题,一开始我也不懂,后面看了大神的讲解,我们不能确保1秒以后才执行then函数,但我们可以保证1秒以后再去then里面的回调。这句话听起来确实很蒙蔽,大神的讲解如下。
我们可以在1秒内,先把then里面的两个回调给保存下来,等到1秒后,执行了resolve或reject,再去判断状态,并且去判断要去执行刚刚保存的两个回调中的哪一个。
当Promise的状态为pending时,就代表定时器还没跑完,当计时器跑完,Promise的状态一定是fulfilled或rejected。
在JS中用来保存数据的要么是对象,要么是数组,这里如果用对象的话,当执行一个then就创建一个对象的话,then执行太多的话创建的对象会变得非常多,占用非常多的内存。所以我们尝试用数组来保存数据。
总体代码如下:
class myPromise {
constructor(executor) {
// 初始化值
this.initValue()
// 初始化this指向
this.initBind()
// 保存成功的回调
this.onFulfilledCallbacks = []
// 保存失败的回调
this.onRejectedCallbacks = []
try {
executor(this.resolve, this.reject)
} catch (error) {
this.reject(error)
}
}
initBind() {
// 初始化this指向
this.resolve = this.resolve.bind(this)
this.reject = this.reject.bind(this)
}
initValue() {
// 初试化值
this.PromiseResult = null //终值
this.PromiseState = 'pending' //状态
}
resolve(value) {
// 判断状态,当状态改变后就不可变
if (this.PromiseState !== 'pending') return
// 如果执行resolve,则将状态变为fulfilled
this.PromiseState = 'fulfilled'
this.PromiseResult = value
// 执行保存成功的回调
while(this.onFulfilledCallbacks.length) {
this.onFulfilledCallbacks.shift()(this.PromiseResult)
}
}
reject(reason) {
if (this.PromiseState !== 'pending') return
// 如果指向reject,则将状态变为rejected
this.PromiseState = 'rejected'
this.PromiseResult = reason
// 执行保存失败的回调
while(this.onRejectedCallbacks.length) {
this.onRejectedCallbacks.shift()(this.PromiseResult)
}
}
then(onFulfilled,onRejected) {
// 接收两个回调函数onFulfilled和onRejected
// 参数校验,确保传入的参数一定是函数
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : val => val
onRejected = typeof onRejected === 'function' ? onRejected : reason => {throw reason}
if (this.PromiseState === 'fulfilled') {
// 如果当前为成功状态,则执行第一个回调
onFulfilled(this.PromiseResult)
} else if (this.PromiseState === 'rejected') {
// 如果当前状态为失败状态,则执行第二个回调
onRejected(this.PromiseResult)
} else if (this.PromiseState === 'pending') {
// 如果状态为待定状态,则暂时保存两个回调
this.onFulfilledCallbacks.push(onFulfilled.bind(this))
this.onRejectedCallbacks.push(onRejected.bind(this))
}
}
}
测试一下
const p1 = new myPromise((resolve,reject) => {
setTimeout(() => {
resolve('成功')
}, 1000);
}).then(res => console.log(res),err=>console.log(err))
输出如下
在1秒后,浏览器输出成功。
then链式调用
离最后实现Promise,现在只剩下then的链式调用。
then的链式调用受上一个then返回值的影响。
then的链式调用如下:
const p1 = new Promise((resolve, reject) => {
resolve(10)
}).then(res => 2 * res, err => console.log(err)).then(res => console.log(res), err => console.log(err))
输出如下
从上面的例子我们可以看出
- then方法本身会返回一个新的Promise对象。
- 如果返回值是一个Promise对象,返回值为成功,则新Promise的状态就是成功。
- 如果返回值是一个Promise对象,返回值为失败,则新Promise的状态就是失败。
- 如果返回值是一个非Promise对象,新Promise对象就是成功,值为返回值。
then实现链式调用最重要的是,then执行后返回一个Promise对象
then代码改进如下:
then(onFulfilled, onRejected) {
// 接收两个回调函数onFulfilled和onRejected
// 参数校验,确保传入的参数一定是函数
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : val => val
onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason }
var thenPromise = new myPromise((resolve, reject) => {
const resolvePromise = cb => {
try {
const x = cb(this.PromiseResult)
if (x === thenPromise) {
// 不能返回自身
throw new Error('不能返回自身')
}
if (x instanceof myPromise) {
// 如果返回值是Promise对象,返回值为成功,新的Promise就是成功
// 如果返回值是Promise对象,返回值为失败,新的Promise就是失败
x.then(resolve, reject)
} else {
// 非Promise对象直接成功
resolve(x)
}
} catch (err) {
reject(err)
throw new Error(err)
}
}
if (this.PromiseState === 'fulfilled') {
// 如果当前为成功状态,则执行第一个回调
// onFulfilled(this.PromiseResult)
resolvePromise(onFulfilled)
} else if (this.PromiseState === 'rejected') {
// 如果当前状态为失败状态,则执行第二个回调
onRejected(this.PromiseResult)
} else if (this.PromiseState === 'pending') {
// 如果状态为待定状态,则暂时保存两个回调
this.onFulfilledCallbacks.push(onFulfilled.bind(this))
this.onRejectedCallbacks.push(onRejected.bind(this))
}
})
return thenPromise
}
测试一下
const p1 = new myPromise((resolve, reject) => {
resolve(100)
}).then(res => res * 100, err => console.log(err)).then(res => console.log(res), err => console.log(err))
输出如下:
总结
至此,我们简单的实现了Promise,由于我本人比较菜,整篇文章基本上都是看别的大神的文章总结出来的。