【重学JS异步系列】原来Promise不过如此

2,606 阅读14分钟

本文概要

Promise是工作中非常常用,也是面试经常问到的知识点,本文从以下几方面重学 Promise,对它有更深入的理解,帮助我们更轻松的面对工作和面试。

  1. Promise/A+ 规范解读
  2. es6的 Promsie 介绍
  3. 实现一个符合 Promsie/A+ 规范的 Promsie 和es6中新增的一些重要方法
  4. 总结

Promise规范

Promise 出现之前,js中的异步是一直依靠回调函数来实现的,当多个异步操作有依赖关系关系的时候,就会出现多层嵌套,这样多个异步的操作形成了强耦合,牵一发而动全身,很难维护,这就是"回调函数地狱"。

Promise 规范最早是由社区提出和实现的,CommonJS组织先后提出了Promise/APromise/BPromise/D 规范,后来一个叫 Promise/A+ 的组织,基于 Promise/A 规范进行修改和补充,制定了 Promise/A+ 规范。ES6中 Promise 的实现就是基于这个规范来实现的。

Promise/A+ 规范分为术语和要求两部分,下面我们看一下核心的规范内容(文章结尾参考文章中,给出了 Promise/A+ 原文地址,有兴趣可以参考)

  1. 术语

    promise 一个包含了兼容规范的then方法的对象或函数。
    thenable 一个包含了then方法的对象或函数。
    value 一个任何Javascript值。
    exception 是由throw表达式抛出来的值。
    reason 是一个用于描述Promise被拒绝原因的值。

  2. 要求

    2.1 Promise 状态 Promise 必须是下面3个状态之一:pending, fulfilledrejected
    状态只能由 pending 转到 fulfilledrejected 状态,且状态不能再改变。
    状态由 pending 转到 fulfilled ,必须有一个值,由pending转到rejected,必须有一个reason(原因)。

    2.2 then方法

    Promise 必须提供一个then方法来获取其值或原因。then方法接受两个参数:

    promise.then(onFulfilled, onRejected)
    
    1. onFulfilledonRejected 都是可选的: 如果 onFulfilledonRejected 不是函数,直接忽略

    2. 如果 onFulfilled 是函数, 它必须在状态变为 fulfilled 后调用,且不能调用多次,promisevalue 为其第一个参数。
      如果 onRejected 是函数, 它必须在状态变为 rejected 后调用,且不能调用多次,promisereason 为其第一个参数。

    3. 对于一个 promise,它的 then 方法可以调用多次。
      当状态 fulfilled 后,所有 onFulfilled 都必须按照其注册顺序执行。 当状态 rejected 后,所有 OnRejected 都必须按照其注册顺序执行。

es6的promise

下面我们参照 promise 规范,一起看下es6对promise的实现。

ES6 中,Promise 对象是一个构造函数,用来生产 Promise 实例。 Promise 构造函数接受一个函数作为参数,该函数的两个参数分别是 resolvereject
resolve:状态从 pending 变为resolved时调用,并将异步操作的结果作为参数;
reject: 状态从从 pending 变为 rejected 时调用,并将异步操作报出的错误作为参数。

const promise = new Promise(function(resolve, reject) {
  // ... some code

  if (/* 异步操作成功 */){
    resolve(value);
  } else {
    reject(error);
  }
});

then方法

then 方法定义在 Promise.prototype 上,Promise 实例生成以后,可以用 then 方法分别指定 resolved 状态和 rejected 状态的回调函数,其中 rejected 状态的回调函数(第二个参数)是可选的。

promise.then(function(value) {
  // success
}, function(error) {
  // failure
});

then 方法会返回的是一个新的 Promise 实例,因此可以采用链式写法。

function timeout(ms) {
  return new Promise((resolve, reject) => {
    setTimeout(resolve, ms, 1);
  });
}

timeout(5000)
.then((value) => {
  console.log(value);
  return 2
})
.then((value) => {
	console.log(value)
})
.then((value) => {
	console.log(value)
})

// 1
// 2
// undefined

上面代码采用了then的链式写法,此时会按照次序依次调用回调函数。第一个回调函数完成以后,会将返回值2作为参数,传入第二个回调函数,所以第二个回调函数会打印2,由于第2个函数没有返回值,所以第3个回调会打印 undefined

catch方法

catch 方法也定义在 Promise.prototype 上。是.then(null, rejection).then(undefined, rejection)的别名,用来指定发生错误时的回调函数。

$.post("/path1")
.then(() => {
    return $.post("/path2")
})
.then(() => {
    return $.post("/path3")
})
.catch((err) => {
    console.log(err)
})

上面的代码中,如果异步操作发生错误,状态变为 rejected 时,就会触发 catch 方法中的回调函数,另外,如果代码运行出现错误也会执行 catch 方法。
Promise 对象的错误总是会被下一个 catch 语句捕获,也就是说上面代码中的3个 Promise 中的任何一个出现错误,都能够被 catch 捕获。

catch 方法并不是 Promise/A+ 规范中规定的,下面我们要说的一些方法也都不是 Promise/A+ 中规定的,而是es6中新增的。

在使用时,使用 catch 方法,而不是then方法的第二个参数,来处理异常情况是更好的选择。 第一,catch 可以捕获上面所有then方法中的执行错误。 第二,使用 catch 方法更接近于同步的写法。

resolve()

将现有对象转为 promise 对象

reject()

Promise.reject(reason)方法也会返回一个新的 promise 实例,该实例的状态为 rejected

finally方法

ES2018 标准引入,用于指定不管 Promise 对象最后状态如何,都会执行的操作。

finally方法不依赖于 Promise 的执行结果,其回调函数不接受任何参数。

promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});

上面代码,不管 promise 的状态怎么变化,最终都会执行 finally 方法中的回调。

function timeout(ms) {
  return new Promise((resolve, reject) => {
    setTimeout(resolve, ms, 'done');
  });
}

timeout(100).then((value) => {
  console.log(value);
});

promise.all

Promise.all() 方法可以将多个 Promise 实例包装成一个新的 Promise 实例。

const p = Promise.all([p1, p2, p3]);

上面代码中p1,p2,p3都为promise实例。包装后的promise实例的状态由p1,p2,p3决定,有两种情况。

  1. 只有p1、p2、p3的状态全都变成 fulfilled,p的状态才会变成 fulfilled,此时传递给p回调函数的参数将是p1、p2、p3的返回值组成一个数组。

  2. 只要p1、p2、p3之中任意一个被 rejected,p的状态就变成 rejected,此时传递给p的回调函数的参数是第一个 reject 的实例的返回值。

Promise.race

Promise.race()方法也是将多个 Promise 实例包装成一个新的 Promise 实例。

const p = Promise.race([p1, p2, p3]);

上面代码中,只要p1、p2、p3中任一个实例状态发生变化,p的状态就会改变。第一状态变化的 Promise 实例的返回值,就传递给p的回调函数的参数。

Promise.allSettled

ES2020 标准引入,同样接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例。

const p = Promise.allSettled([p1, p2, p3]);

上面代码中,只有等到所有p1,p2,p3实例状态都发生改变,无论状态是fulfilled还是rejected,实例p的状态才会发生变化。

Promise.any

该方法目前是一个第三阶段的提案。 该方法同样接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例。

const p = Promise.any([p1, p2, p3]);

只要参数p1,p2,p3中任意一个变成 fulfilled 状态,包装后的实例p就会变成 fulfilled 状态;如果所有参数实例都变成 rejected 状态,包装实例就会变成 rejected 状态。

手写Promise

下面一步步自己实现一个 Promsie,目的并不是完全实现一个和es6一样功能的 Promise,而是通过手写一个 Promise,更深入的了解 Promise 的原理。
下面我们先实现一个符合 Promise/A+ 规范的 Promsie

工具函数定义

首先定义 promise 的状态常量和和一些工具函数

const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'

// 工具函数,判断是否为函数
const isFunction = fn => Object.prototype.toString.call(fn) === '[object Function]'
// 工具函数,判断状态是否为pending
const isPending = status => status === PENDING
// 工具函数,判断是否是promise实例
const isPromise = target => target instanceof Promise

经过上面我们知道,Promise 类接收一个函数作为参数,这个函数又有两个参数 resolvereject,这两个参数也是方法。

Promise类创建

// 创建Promise类
class Promise {
	constractor(executor){

		// 要求Promise的参数必须为函数,如果不是函数就抛出错误
		if(!isFunction(executor)){
			throw new Error('Promise must accept a function as a parameter')
		}
		
		this._status = PENDING // 初始化状态值为pending
		this._value = undefined // 给value设置默认值
		this._reason = undefined  // 给reason设置默认值
       // 执行executor方法,把参数resole,reject传递进来
            try {
                executor(this._resolve.bind(this), this._reject.bind(this))
            }				
            catch (err) { // executor执行出现异常调用rejected方法
                this._reject(err)
            }
	},

	// 定义executor的参数resolve的执行函数
	_resolve(value){
		const {_status: status} = this
		if (isPending(status)) { // 状态只能由pending变成其他两种状态,且只能改变一次
			this._value = value // 将resove方法传递进来的参数保存下来
			this._status = FULFILLED // 状态更新为fufilled
		}
	},

	// 定义executor的参数reject的执行函数
	_reject(reason){
		const {_status: status} = this
		if(isPending(status)){ // 状态只能由pending变成其他两种状态,且只能改变一次
			this._reason = reason
			this._status = REJECTED

		}
	}
}

then方法实现

在实现 then 方法之前,建议读者再仔细阅读上面 Promise/A+ 规范的 then 部分,有助于更好的理解这部分代码。

then 方法接受两个参数,onFulfilledonRejected。并且 then 方法必须返回一个 promise 对象,可以被同一个 promise 对象调用多次,当 promise 成功状态时,所有 onFulfilled 需按照其注册顺序依次回调 当 promise 失败状态时,所有 onRejected 需按照其注册顺序依次回调。

首先在 constractor 增加两个数组,fulfilledQueuesrejectedQueues,用来维护每次 then 注册时的回调函数。

this._fulfilledQueues = [] // 添加成功回调函数队列
this._rejectedQueues = [] // 添加失败回调函数队列
// then 方法实现
then(onFulfilled, onRejected) {
    // 定义then方法返回的promise
    return new Promise((resolve, reject) => {
        // 成功时执行的函数
        let fulfilledHandle = (value) => {
        try {
            // 如果onFulfilled不是函数,忽略掉
            if (isFunction(onFulfilled)) {
                const result = onFulfilled(value)
                // 如果当前回调函数返回值为promise实例,需要等待其状态变化后再执行下一个回调
                if (isPromise(result)) {
                    result.then(resolve, reject)
                } else {
                // 如果当前回调函数返回值不是promise实例,直接执行下一个then的回调,并将结果作为参数
                    resolve(result)
                }
            }

        } catch (err) {
            // 如果新的promise执行出错,新的promise状态直接变为rejected,调用reject函数,并将error作为参数
            	reject(err)
            }

        }

        // 失败时执行的函数,实现上同成功时执行的函数
        let rejectedHandle = (reason) => {
            try {
            	// 如果onFulfilled不是函数,忽略掉
                if (isFunction(onRejected)) {
                	const result = onRejected(reason)
                    if (isPromise(result)) {
                        result.then(resolve, reject)
                    } else {
                        reject(reason)
                    }
                }
            } catch (err) {
                reject(err)
            }
        }

    const { _value: value, _status: status, _reason: reason, _fulfilledQueues: fulfilledQueues, _rejectedQueues: rejectedQueues } = this

    switch (status) {

        // 状态为pending时,将then方法回调函数加入执行队列等待执行
        case PENDING:
        fulfilledQueues.push(fulfilledHandle)
        rejectedQueues.push(rejectedHandle)
        break

        // 状态为fulfilled时,直接执行onFulfilled方法
        case FULFILLED:
        fulfilledHandle(value)
        break

        // 状态为rejected时,直接执行onRejected方法
        case REJECTED:
        rejectedHandle(reason)
        break
    }
})
}

实现then的链式调用

为了实现 then 方法的链式调用,我们需要修改 constractor 下的 _resolve_reject 方法,让他们依次执行成功/失败任务队列中的函数。

 _resolve(value) {
        const { _status: status } = this
    	const resolveFun = () => {
    	if (isPending(status)) { // 状态只能由pending变成其他两种状态,且只能改变一次
            this._value = value // 将resove方法传递进来的参数保存下来
            this._status = FULFILLED // 状态更新为fufilled
            let cb
            while (cb = this._fulfilledQueues.shift()) {
                cb(value)
            }
       	}
    	}
    	// 通过setTimeout 0 模拟同步的promise情况
    	setTimeout(resolveFun, 0)
    }

    _reject(reason) {
        const { _status: status } = this
        const rejectFun = () => {
        if (isPending(status)) { // 状态只能由pending变成其他两种状态,且只能改变一次
            this._reason = reason
            this._status = REJECTED
            let cb
            while (cb = this._rejectedQueues.shift()) {
                cb(reason)
            }
        }
        }
        setTimeout(rejectFun, 0)
    }

这样我们就实现了一个符合 promsie/A+ 规范的 Promise

下面再来实现es6的 Promise 的一些方法

finally方法

finally(callback){
  let P = this.constructor;
  return this.then(
    value  => P.resolve(callback()).then(() => value),
    reason => P.resolve(callback()).then(() => { throw reason })
  );

}

Promise.resolve和Promise.reject

static resolve (value) {
  if (value instanceof Promise) return value
  return new Promise(resolve => resolve(value))
}

static reject (value) {
  return new Promise((resolve ,reject) => reject(value))
}

Promise.all

Promise.allPromise 类的静态方法。

static all(list){
	return new Promise((resolve, reject) => {
		let count = 0 // promise状态结束的个数,初始化为0
		let resValues = [] // 状态转为fufilled的value值
		const listLength = list.length
		for (let i = 0; i < list.length; i++) {
			// 如果不是不是Promise,调用Promise.resolve方法
			const p = isPromise(list[i]) ? list[i] : this.resolve(list[i])
			p.then((res)=>{
				resValues.push(res)
				count++
				// 所有状态都变为fufilled,直接resolve,返回resValues
				if(count === listLength){
					resolve(resValues)
				}
			}, (e)=> {
				// 如果有一个reject,直接reject
				reject(e)
			})
		}
	})
}

Promsie.race

static race(list){
	return new Promise((resolve, reject) => {
    	for(let i = 0; i < list.length; i++){
    		const p = isPromise(list[i]) ? list[i] : this.resolve(list[i])
    		// 只要有一个状态转变,新的promsie状态就转变
    		p.then((res) => {
    			resolve(res)
    		}, (e) => {
    			reject(e)
    		})
    	}
	})
}

下面是刚刚实现的 promsie 的完成代码:

// 定义promise的状态常量 
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'

// 工具函数,判断是否为函数
const isFunction = fn => Object.prototype.toString.call(fn) === '[object Function]'
// 工具函数,判断状态是否为pending
const isPending = status => status === PENDING
// 工具函数,判断是否是promise实例
const isPromise = target => target instanceof Promise


// 已经知道,promise 类接收一个函数作为参数,这个函数又有两个参数resolve,reject,这两个参数也是方法。
class Promise {
    constructor(executor) {

        // 要求Promise的参数必须为函数,如果不是函数就抛出错误
        if (!isFunction(executor)) {
            throw new Error('Promise must accept a function as a parameter')
        }

        this._status = PENDING // 初始化状态值为pending
        this._value = undefined // 给value设置默认值
        this._reason = undefined  // 给reason设置默认值
        this._fulfilledQueues = [] // 添加成功回调函数队列
        this._rejectedQueues = [] // 添加失败回调函数队列

        // 执行executor方法,把参数resole,reject传递进来
        try {
            executor(this._resolve.bind(this), this._reject.bind(this))
        }				catch (err) { // executor执行出现异常调用rejected方法
            this._reject(err)
        }
    }

    // 为了实现 then 方法的链式调用,我们需要修改constractor下的 _resolve 和 _reject 方法,让他们依次执行成功/失败任务队列中的函数。
    _resolve(value) {
        const { _status: status } = this
    	const resolveFun = () => {
    	if (isPending(status)) { // 状态只能由pending变成其他两种状态,且只能改变一次
            this._value = value // 将resove方法传递进来的参数保存下来
            this._status = FULFILLED // 状态更新为fufilled
            let cb
            while (cb = this._fulfilledQueues.shift()) {
                cb(value)
            }
       	}
    	}
    	// 通过setTimeout 0 模拟同步的promise情况
    	setTimeout(resolveFun, 0)
     
    }

    _reject(reason) {
        const { _status: status } = this
        const rejectFun = () => {
        if (isPending(status)) { // 状态只能由pending变成其他两种状态,且只能改变一次
            this._reason = reason
            this._status = REJECTED
            let cb
            while (cb = this._rejectedQueues.shift()) {
                cb(reason)
            }
        }
        }
        setTimeout(rejectFun, 0)

    }

    // then 方法实现
    then(onFulfilled, onRejected) {
        // 定义then方法返回的promise
        return new Promise((resolve, reject) => {
            // 成功时执行的函数
            let fulfilledHandle = (value) => {
            try {
                // 如果onFulfilled不是函数,直接忽略
                if (isFunction(onFulfilled)) {
                    const result = onFulfilled(value)
                    // 如果当前回调函数返回值为promise实例,需要等待其状态变化后再执行下一个回调
                    if (isPromise(result)) {
                        result.then(resolve, reject)
                    } else {
                    // 如果当前回调函数返回值不是promise实例,直接执行下一个then的回调,并将结果作为参数
                        resolve(result)
                    }
                }

            } catch (err) {
                // 如果新的promise执行出错,新的promise状态直接变为rejected,调用reject函数,并将error作为参数
                	reject(err)
                }

            }

            // 失败时执行的函数,实现上同成功时执行的函数
            let rejectedHandle = (reason) => {
                try {
                	// 如果onRejected不是函数,直接忽略
                    if (isFunction(onFulfilled)) {
                    	const result = onRejected(reason)
                        if (isPromise(result)) {
                            result.then(resolve, reject)
                        } else {
                            reject(reason)
                        }
                    }
                } catch (err) {
                    reject(err)
                }
            }

        const { _value: value, _status: status, _reason: reason, _fulfilledQueues: fulfilledQueues, _rejectedQueues: rejectedQueues } = this

        switch (status) {

            // 状态为pending时,将then方法回调函数加入执行队列等待执行
            case PENDING:
            fulfilledQueues.push(fulfilledHandle)
            rejectedQueues.push(rejectedHandle)
            break

            // 状态为fulfilled时,直接执行onFulfilled方法
            case FULFILLED:
            fulfilledHandle(value)
            break

            // 状态为rejected时,直接执行onRejected方法
            case REJECTED:
            rejectedHandle(reason)
            break
        }

    })

    }

    finially(callback){
      let P = this.constructor;
	  return this.then(
	    value  => P.resolve(callback()).then(() => value),
	    reason => P.resolve(callback()).then(() => { throw reason })
	  );

    }

    static resolve (value) {
	  if (value instanceof Promise) return value
	  return new Promise(resolve => resolve(value))
	}

	static reject (value) {
	  return new Promise((resolve ,reject) => reject(value))
	}

	static all(list){
    	return new Promise((resolve, reject) => {
    		let count = 0 // promise状态结束的个数,初始化为0
    		let resValues = [] // 状态转为fufilled的value值
    		const listLength = list.length
    		for (let i = 0; i < list.length; i++) {
    			// 如果不是不是Promise,调用Promise.resolve方法
    			const p = isPromise(list[i]) ? list[i] : this.resolve(list[i])
    			p.then((res)=>{
    				resValues.push(res)
    				count++
    				// 所有状态都变为fufilled,直接resolve,返回resValues
    				if(count === listLength){
    					resolve(resValues)
    				}
    			}, (e)=> {
    				// 如果有一个reject,直接reject
    				reject(e)
    			})
    		}
    	})
    }

    static race(list){
    	return new Promise((resolve, reject) => {
        	for(let i = 0; i < list.length; i++){
        		const p = isPromise(list[i]) ? list[i] : this.resolve(list[i])
        		// 只要有一个状态转变,新的promsie状态就转变
        		p.then((res) => {
        			resolve(res)
        		}, (e) => {
        			reject(e)
        		})
        	}
    	})
    }
}

总结

对比回调函数而言,Promise 可以通过链式写法,将异步操作以同步操作的流程表达出来。
Promise 的一个更大的特点是,它为所有的异步操作提供了一个统一的 API,这让我们可以更统一的处理异步操作。

然而 Promsie 的本质还是回调函数,只是在写法上进行了改进,在 then 中执行,使其看起来更清楚而已。 带来的坏处也很明显,原有的代码都要使用promsie包装一层,一眼看去都是then,使原有的语义看起来不是十分清楚。

那么除了 Promise 有什么更好的写法吗?优缺点又是什么呢,欢迎关注重学js异步系列文章。

参考

参考
segmentfault.com/a/119000000…
promisesaplus.com/
segmentfault.com/a/119000000…
es6.ruanyifeng.com/#docs/promi…

欢迎关注我的公众号「前端小苑」,我会定期在上面更新原创文章。也可以加我微信 yu_shihu_ 共同交流技术。