Promise 从入⻔到精通

一. 为什么需要 promise ?

Javascript 是一⻔单线程语言,所以早期我们解决异步的场景时,大部分情况都是通过回调函数来进
行。
例如在浏览器中发送 ajax 请求,就是常⻅的一个异步场景,发送请求后,一段时间服务端响应之后我们 才能拿到结果。如果我们希望在异步结束之后执行某个操作,就只能通过回调函数这样的方式进行操 作。

var dynamicFunc = function(cb) { setTimeout(function() {
	cb();
	}, 1000);
}
dynamicFunc(function() {console.log(123)});
  • 上面这个例子的 dynamicFunc 就是一个异步的函数
  • 里面执行的 setTimeout 会在 1s 之后调用传入的 cb 函数
  • 照上面的调用方式,最终 1s 之后,会打印 123 这个结果
    同样的,如果后续还有内容需要在异步函数结束时输出的话,就需要多个异步函数进行嵌套,非常不利 于后续的维护。
setTimeout(function() { 
	console.log(123); 
	setTimeout(function() {
		console.log(321);
		// ...
	}, 2000); 
}, 1000);

为了能使回调函数以更优雅的方式进行调用,在 ES6 中 js 产生了一个名为 promise 的新规范,他让异步操作的变得近乎「同步化」

二. Promise 基础

在支持 ES6 的高级浏览器环境中,我们通过 Promise() 即可构造一个 promise 实例。
这个构造函数接受一个函数,分别接受两个参数,resolve 和 reject,代表着我们需要改变当前实例的状 态到 已完成 或是 已拒绝

function promise1() {
	return new Promise(function(resolve, reject) {
		// 定义异步的内容 
  	setTimeout(function() {
			console.log('1s 后输出');
			// 输出完成后,调用函数传入的 resolve 函数,将该 promise 实例标记为已完成,当前 promise 串行继续执行
			resolve(); 
    }, 1000);
	});
}
function promise2() {
  return new Promise(function(resolve) {
		setTimeout(function() { 
			console.log('2s 后输出'); resolve();
		}, 2000); 
	});
}

上面的两个 promise 实例,串联起来即可写为:
promise1().then(function() { return promise2(); });

也可以简写为 promise1().then(promise2);

浏览器中执行之后,即可看到,1s 之后出现 1s 后输出 字样,再经过 2s 出现 2s 后输出 字样。在这个例子中我们能看到。当前 promise 如果状态变为已完成(执行了 resolve 方法),那么就会去执行 then 方法中的下一个 promise 函数。

同样的,如果我们的 promise 变为已拒绝状态(执行了 reject 方法),那么就会进入后续的异常处理函数中。

function promise3() {
	return new Promise(function(resolve, reject) {
		var random = Math.random() * 10; // 随机一个 1 - 10 的数字 
    setTimeout(function() {
      if (random >= 5) { 
        resolve(random);
      } else {
        reject(random);
      }
		}, 1000);
	}); 
}

var onResolve = function(val) { 
  console.log('已完成:输出的数字是', val);
};

var onReject = function(val) { 
  console.log('已拒绝:输出的数字是', val);
}

// promise 的 then 也可以接受两个函数,第一个参数为 resolve 后执行,第二个函数为 reject 后执行
promise3().then(onResolve, onReject);

// 也可以通过 .catch 方法拦截状态变为已拒绝时的 
promise promise3().catch(onReject).then(onResolve);

// 也可以通过 try catch 进行拦截状态变为已拒绝的 promise 
try {
	promise3().then(onResolve); 
} catch (e) {
  onReject(e);
}
  • 这个例子使用了三种方式拦截最终变为「已拒绝」状态的 promise,分别是
    • 使用 then 的第二个参数
    • 使用 .catch 方法捕获前方 promise 抛出的异常
    • 使用 try catch 拦截代码块中 promise 抛出的异常

同时我们还可以发现,在改变 promise 状态时调用 resolve 和 reject 函数的时候,也可以给下一步 then 中执行的函数传递参数。这个例子中我们把随机生成的数字传给了 resolve 和 reject 函数,我们也就能在 then 中执行函数的时候拿到这个值。

总结一下本小节的内容:

  1. promise 会有三种状态,「进行中」「已完成」「已拒绝」进行中状态可以更改为已完成 或 已拒绝,已经更改过状态后无法继续更改(例如从已完成改为已拒绝)。

  2. ES6 中的 Promise 构造函数,我们构造之后需要传入一个函数,他接受两个函数参数,执行第一个参数之后就会改变当前 promise 为「已完成」状态,执行第二个参数之后就会变为「已拒绝」 状态。

  3. 通过 .then 方法,即可在上一个 promise 达到已完成时继续执行下一个函数或 promise。同时通过 resolve 或 reject 时传入参数,即可给下一个函数或 promise 传入初始值。

  4. 已拒绝的 promise,后续可以通过 .catch 方法或是 .then 方法的第二个参数或是 try catch 进行捕 获。

三. 如何封装异步操作为 promise

我们可以将任何接受回调的函数封装为一个 promise,下面举几个简单的例子来说明。

// 原函数
function dynamicFunc(cb) {
	setTimeout(function() { 
    console.log('1s 后显示'); 
    cb();
	}, 1000); 
}

var callback = function() { 
  console.log('在异步结束后 log');
}

// 用传入回调函数的方式执行 
dynamicFunc(callback);

上面的例子就是最传统的,使用传入回调函数的方式在异步结束后执行函数。我们可以通过封装 promise 的方式,将这个异步函数变为 promise。如下:

function dynamicFuncAsync() {
  return new Promise(function(resolve) {
		setTimeout(function() { 
      console.log('1s 后显示'); 
      resolve();
		});
  });
}

var callback = function() { 
  console.log('在异步结束后 log');
}

dynamicFuncAsync().then(function() { callback() });

再举一个例子,发送 ajax 请求也可以进行封装:

function ajax(url, success, fail) {
	var client = new XMLHttpRequest(); 
  client.open("GET", url); 
  client.onreadystatechange = function() {
		if (this.readyState !== 4) { 
      return;
		}
    
		if (this.status === 200) {
			success(this.response); } else {
			fail(new Error(this.statusText)); 
    }
	};
	client.send(); 
};

ajax('/ajax.json', function() {console.log('成功')}, function() {console.log('失败')});

我们可以看到,调用 ajax 方法时需要传入 success 和 fail 的回调函数进行调用。我们可以不传入回调函 数,通过封装 promise 的方式,在原来的执行回调函数的地方更改当前 promise 的状态,就可以通过链式调用。

function ajaxAsync(url) {
	return new Promise(function(resolve, reject){
		var client = new XMLHttpRequest(); 
    client.open("GET", url); 
    client.onreadystatechange = function() {
			if (this.readyState !== 4) { 
        return;
			}
			if (this.status === 200) {
				resolve(this.response);
   		} else {
				reject(new Error(this.statusText));
			} 
    };
		client.send(); 
  });
};

ajax('/ajax.json')
	.catch(function() {
		console.log('失败'); 
	})
	.then(function() { 
  	console.log('成功');
	})

总结一下当前小节:

  1. 我们可以轻松的把任何一个函数或者是异步函数改为 promise,尤其是异步函数,改为 promise 之 后即可进行链式调用,增强可读性。

  2. 将带有回调函数的异步改为 promise 也很简单,只需要在内部实例化 promise 之后,在原来执行 回调函数的地方执行对应的更改 promise 状态的函数即可。

四. promise 规范解读

任何符合 promise 规范的对象或函数都可以成为 promise,promise A plus 规范地址: https://promisesaplus.com/

上面我们熟悉了整体 promise 的用法,我们知道了如何去创建一个 promise,如何去使用它,后面我们 也熟悉了如何去改造回调函数到 promise。本小节我们详细过一遍 promise A+ 规范,从规范层面明白 promise 使用过程中的细节。

4.1 术语

Promise: promise 是一个拥有 then 方法的对象或函数,其行为符合本规范

具有 then 方法(thenable): 是一个定义了 then 方法的对象或函数

值(value): 指任何 JavaScript 的合法值(包括 undefined , thenable 和 promise)

异常(exception): 是使用 throw 语句抛出的一个值

原因(reason): 表示一个 promise 的拒绝原因。

4.2 要求

1. promise 的状态

一个 Promise 的当前状态必须为以下三种状态中的一种: 等待态(Pending)已完成(Fulfilled)已拒绝(Rejected)

2.必须有一个 then 方法

一个 promise 必须提供一个 then 方法以访问其当前值和原因。

promise 的 then 方法接受两个参数: promise.then(onFulfilled, onRejected) 他们都是可选参数,同时他们都是函数,如果 onFulfilledonRejected 不是函数,则需要忽略他们。

  • 如果 onFulfilled 是一个函数

    • promise 执行结束后其必须被调用,其第一个参数为 promise 的结果
    • promise 执行结束前其不可被调用
    • 其调用次数不可超过一次
  • 如果 onRejected 是一个函数

    • promise 被拒绝执行后其必须被调用,其第一个参数为 promise 的原因
    • promise 被拒绝执行前其不可被调用
    • 其调用次数不可超过一次
  • 在执行上下文堆栈仅包含平台代码之前,不得调用 onFulfilledonRejected

  • onFulfilledonRejected 必须被作为普通函数调用(即非实例化调用,这样函数内部 this 非严格模式下指向 window)

  • then 方法可以被同一个 promise 调用多次

    • promise 成功执行时,所有 onFulfilled 需按照其注册顺序依次回调
    • promise 被拒绝执行时,所有的 onRejected 需按照其注册顺序依次回调
  • then 方法必须返回一个 promise 对象 promise2 = promise1.then(onFulfilled, onRejected);

    • 只要 onFulfilled 或者 onRejected 返回一个值 xpromise 2 都会进入 onFulfilled 状态
    • 如果 onFulfilled 或者 onRejected 抛出一个异常e ,则 promise2必须拒绝执行,并返回拒因 e
    • 如果 onFulfilled 不是函数且 promise1 状态变为已完成, promise2 必须成功执行并返回相 同的值
    • 如果 onRejected 不是函数且 promise1 状态变为已拒绝, promise2 必须执行拒绝回调并返回相同的据因
var promise1 = new Promise((resolve, reject) => {reject() }); 

promise1
	.then(null, function() { 
  	return 123;
	})
	.then(null, null) 
	.then(null, null) 
	.then(
  	() => {
			console.log('promise2 已完成');
		},
  	() => {
			console.log('promise2 已拒绝'); 
   });

五. Promise 构造函数上的静态方法

Promise.resolve

返回一个 promise 实例,并将它的状态设置为已完成,同时将他的结果作为传入 promise 实例的值

var promise = Promise.resolve(123);
promise 
  .then(function(val) {
		console.log('已完成', val); 
	});
// 已完成 123

同样的, Promise.resolve 的参数也可以处理对象,函数等内容,处理方式和上面规范中介绍的相同。

Promise.reject

返回一个 promise 实例,并将它的状态设置为已拒绝,同时也将他的结果作为原因传入 onRejected 函数

var promise = Promise.reject(123);
promise
	.then(null, function(val) {
	console.log('已拒绝', val); 
	});
// 已拒绝 123

Promise.all

返回一个 promise 实例,接受一个数组,里面含有多个 promise 实例,当所有 promise 实例都成为已完 成状态时,进入已完成状态,否则进入已拒绝状态。

var promise1 = function() {
	return new Promise(function(resolve) {
		setTimeout(function() { 
      console.log(1); 
      resolve();
		}, 1000)
  }); 
}
 
var promise2 = function() {
	return new Promise(function(resolve) {
		setTimeout(function() { 
      console.log(2); 
      resolve();
		}, 2000); 
  });
}

Promise.all([promise1(), promise2()]) 
  .then(function() {
		console.log('全部 promise 均已完成'); 
	});

注意,此时多个 promise 是同时进行的,也就是在上面这个例子中,等待 1s 打印 1 之后,再等待 1s 就 会打印 2 和「全部 promise 均已完成」。

Promise.race

返回一个 promise 实例,接受一个数组,里面含有多个 promise 实例,当有一个 promise 实例状态改变 时,就进入该状态且不可改变。这里所有的 promise 实例为竞争关系,只选择第一个进入改变状态的 promise 的值。

var promise1 = function() {
	return new Promise(function(resolve) {
		setTimeout(function() { 
      console.log(1); 
      resolve(1);
		}, 1000) 
  });
}

var promise2 = function() {
	return new Promise(function(resolve) {
		setTimeout(function() { 
      console.log(2); 
      resolve(2);
		}, 2000); 
  });
}

Promise.race([promise1(), promise2()]) 
  .then(function(val) {
		console.log('有一个 promise 状态已经改变', val); 
	});

六. 实现 Promise

以下代码非严谨遵守 Promise A+ 规范,仅为demo测试

class MyPromise{
  // 1.handleFn:(resolve,reject)=>{}
  // 2.搭建框架:
  // ①通过实例化调用的的方法:then... ,promise.then()
  // ②通过构造函数调用的方法:static resolve... ,Promise.resolve()
  constructor(handleFn){
    // 状态
    this.status = 'pending';
    // 值
    this.value = undefined;
    // fulfilled 已完成回调数组
    this.fulfilledList = [];
    // rejected 已拒绝回调数组
    this.rejectedList = [];

    // 执行 handleFn,4.triggerResolve,triggerReject
    handleFn(this.triggerResolve.bind(this), this.triggerReject.bind(this));
  }

  /**
   * 定义成功方法,作为Promise 传入的函数体的参数
   * 实现PromiseA+状态转换 定义成功参数
   */
  triggerResolve(val){
    // 因为需要先把 then 里面注册的回调函数获取到,才能知道在 resolve 里面该去执行哪些回调函数;所以通过 setTimeout(()=>,0)的形式,在下一个Event Loop事件循环内,再去执行回调
    // 这里的 triggerResolve 函数,虽然是在构造器内 handleFn 执行了,但是 setTimeout 是在下一个事件循环执行的,所以实际上是先执行的 then ,才是 setTimeout 内的代码
    setTimeout(() => {
      // 在这个里面通过定义 then 方法之后,可以拿到 then 里的回调函数【function () { console.log('resolve')】,从而通过 resolve 的时候,去执行对应的方法 
      // 如果这里的代码是同步执行的的,不是写在 setTimeout 内,那么此时的状态已经改变了
      // 实际执行的时候,还没完全的执行完,所以要判断下
      if(this.status !== 'pending') return;

      // 判断 resolve 方法传入的结果是不是 promise
      if(val instanceof MyPromise){
        // 是promise,则需要给它定义一个 promise, 它的状态就会改变当前所有的状态
        val.then(
          value => {},
          err => {}
        )
      }else{
        // resolve 方法传入的是普通值
        // 把当前 promise 的状态变为【已完成】
        this.status = 'fulfilled';
        // 同时把值记录下来
        this.value = val;
        // 然后再触发一下,之前在上一个事件循环里面记录的所有回调
        this.triggerFulfilled(val);
      }
    }, 0);
  }

  /**
   * 定义失败方法,作为Promise 传入的函数体的参数
   * 实现PromiseA+状态转换 定义失败参数
   */
  triggerReject(val){
    // 自己写的
    setTimeout(() => {
      if (this.status !== 'pending') return;
      if(val instanceof MyPromise){
        val.then(
          value => {},
          err => {}
        )
      } else{
        this.status = 'rejected';
        this.value = val;
        this.triggerRejected(val);
      }
    },0);
  }

  /**
   * 将所有已完成状态的回调执行
   */
  triggerFulfilled(val){
    // 拿到 this 上定义的所有已完成的回调,以 forEach 的形式执行一下 
    this.fulfilledList.forEach(item => item(val));
    this.fulfilledList = [];
  }

  /**
   * 将所有已拒绝状态的回调执行
   */
  triggerRejected(val){
    this.rejectedList.forEach(item => item(val));
    this.rejectedList = [];
  }

  /**
   * 3.在 then 里面将异步函数【回调函数】注册进去,再在调用 resolve【triggerResolve】 的时候执行了下
   * @param onFulfilled 注册的在异步执行的过程中,要去执行的状态改变时的回调函数
   * @param onRejected 注册的在异步执行的过程中,要去执行的状态改变时的回调函数
   * @returns 每一个then 都要返回一个 Promise
   */
  then(onFulfilled, onRejected){
    const {status,value} = this;

    // 每一个then 都要返回一个 Promise
    const promiseInstance = new MyPromise((onNextFulfilled, onNextRejected) => {

      /**
       * 为了链式调用下去
       * @param val triggerFulfilled(val)的时候,去执行的,当前变化的值的时候,改变的这个参数
       * @description 
       * ① 为了将 onFulfilled 与 onNextFulfilled 串联起来;当执行 onFulfilled 函数的时候,就知道怎么去执行 onNextRejected
       * ② 在记录回调函数的过程中,通过内部闭包函数的形式,把【上一个 then 里面注册的回调函数】与【 返回的新 promise 的 resolve函数】进行融合。最终要执行的回调函数的结果
       * ③ 在执行 onFinalFulfilled 过程中,可以把当前新创建的 promise【 new MyPromise((onNextFulfilled, onNextRejected)=>{})】的 resolve【onNextFulfilled】函数进行执行
       * ④ 当上一个注册的回调函数执行的过程中,我们就可以执行下一个 promise 的结果
       */
      function onFinalFulfilled(val){
        if(typeof onFulfilled !== 'function'){
          // 什么时机去执行下一个 promise 的结果?
          // 规范里写的:如果上一个 promise 返回的结果不是函数,就直接去把上一个 promise 返回的结果放到下一个 promise 里面去执行  
          onNextFulfilled(val);
        } else {
          // 先执行一下 上一步的promise里面的结果,就是res
          const res = onFulfilled(val);
          if(res === promiseInstance){
            throw new TypeError('不能是同一个promise')
          }
          // 判断返回的 是否为 promise
          if(res instanceof MyPromise){
            // 通过 promise.then 的方法,注册 onNextFulfilled 与 onNextRejected 这两个回调
            // 怎么去执行下一个 then 里面的结果?
            // 通过 res 的状态:
            // ① 如果 res是 resolve 状态,也就是已完成状态,那么就去执行下一个promise的resolve状态【onNextFulfilled】
            // ② 如果 res是 reject 状态,也就是已拒绝状态,那么就去执行下一个promise的reject状态【onNextRejected】
            res.then(onNextFulfilled, onNextRejected);
          } else {
            // 返回一个普通的值,通过调用下一个 promise 的结果
            onNextFulfilled(val);
          }
        }
      }

      function onFinalRejected(error){
        if(typeof onRejected !== 'function'){
          onNextRejected(error);
        }else{
          let res = null;
          try {
            res = onRejected(error);
          } catch (e) {
            // 如果在这一步 catch 到了问题,执行下一个 promise链的时候,把这一步暴露的问题传递出去;
            onNextRejected(e);
          }

          // 否则的话,这个还是一个 resolve 的状态
          if(res instanceof MyPromise){
            res.then(onNextFulfilled, onNextRejected);
          }else{
            onFulfilled(res);
          }
        }
      }

      // 根据不同的 promise状态,执行不同的方法
      switch (status) {
        case 'pending':{
          // 需要记录下当前 注册进来的这两个回调函数
          this.fulfilledList.push(onFinalFulfilled);
          this.rejectedList.push(onFinalRejected);
          break;
        }
        case 'fulfilled': {
          onFinalFulfilled(value);
          break;
        }

      }
    })

    return promiseInstance;
  }

  catch(onRejected){
    return this.then(null, onRejected)
  }
  
  /**
   * MyPromise.resolve 实现
   */
  static resolve(value){
    // 如果返回的值是 promise,则直接将其返回
    if(value instanceof MyPromise) return value;
    // 如果不是,则返回一个 promise 实例,并将它的状态设置为已完成,同时将它的结果value 作为传入 promise 实例的值
    return new MyPromise(resolve => resolve(value));

  }

  /**
   * MyPromise.reject 实现
   */
  static reject(reason){
    if(reason instanceof MyPromise) return reason;
    return new MyPromise((resolve,reject) => reject(reason))
  }

  /**
   * MyPromise.all 实现
   */
  static all(list){
    return new MyPromise((resolve,reject) => {
      if(!Array.isArray(list)) {
        return reject(new Error('请传入数组'))
      }

      let couter = 0
      let values = [];
      // 此处 for of 是并行的;但在 async await 中for of 可能会出现异步迭代,依次执行
      // 此处还可以用 forEach,for...
      for (const [i, MyPromiseInstance] of list.entries()) {
        MyPromise.resolve(MyPromiseInstance)
        .then(
          res => {
            values[i] = res;
            couter++;
            if(couter === list.length) resolve(values);
          }, 
          err => {
            reject(err)
          }
        )
      }
    })
  }

  /**
   * MyPromise.race 实现
   */
  static race(list){
    return new MyPromise((resolve,reject) => {
       if(!Array.isArray(list)) {
        return reject(new Error('请传入数组'))
      }
      list.forEach((item,i) => {
        //  当某一个promise进入 resolve 状态之后,就进入then
        MyPromise.resolve(item)
        .then(
          res => {
            resolve(res)
          },
          err => {
            reject(err)
          }
        )
      })
    })
  }
}




// 实例化过程中 new Promise 执行顺序:
// ① 先实例化,执行 handleFunc,再进入 then 里面 ,把回调函数注册进来 
// ② 这个时候才会去到 triggerResolve 的 setTimeout()里面去
const promise1 = new MyPromise(function(resolve,reject) { 
  // 注意:进入 then的时候就是 pending状态!!!  
  // 由于对于 此处的 unction(resolve,reject){} 函数来说,一开始会去立刻执行的,但是它的 resolve函数还没有注册进来,所以在注册的时候它还是 pending状态
  // 注册完成之后,在下一个事件循环内执行 resolve 里的方法
  resolve()
  // reject()
})

promise1.then(function () { console.log('resolve') })

七. 补充

function sleep(time = 1) {
    return new Promise(resolve => setTimeout(function() {console.log('promise resolve'); resolve()}, time * 1000))
}

const promiseCreatorList = [
    sleep,
    sleep,
    sleep
]

/********************************************************************/
// 1. Promise.all 并行执行
console.log('Promise.all start', new Date().getTime())
Promise.all(
    promiseCreatorList.map(item => item())
).then(
    () => console.log('Promise.all end', new Date().getTime())
)

/********************************************************************/
// 2. for of async 迭代依次执行
async function main() {
    console.log('for of async start', new Date().getTime())
    async function forOfLoop() {
        for (const promiseInstance of promiseCreatorList) {
            await promiseInstance()
        }
    }
    await forOfLoop()
    console.log('for of async end', new Date().getTime())
}
main()

/********************************************************************/
// 3. 把并行的 Promise 实现串联的
const promiseChain = promiseCreatorList.reduce((memo, current)=>{
    if(memo.then){
        return memo.then(current)
    }
    return memo().then(current)

})
// 相当于 sleep().then(sleep).then(sleep )
promiseChain.then(function () {
    console.log('已完成')
})

/********************************************************************/
// 4. 通过 Promise 实现依次执行
const promiseChain2 = promiseCreatorList.reduce((memo, current)=>{
    return memo().then(current)
}, Promise.resolve())
// 相当于 Promise.resolve().then(sleep).then(sleep).then(sleep)
promiseChain2.then(function () {
    console.log('已完成')
})
上一篇:Promise结合setTimeout--promise练习题(2)


下一篇:将Vue项目打包成一个HTML文件后是如此丝滑