ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

Promise 从入⻔到精通

2021-09-19 23:58:55  阅读:161  来源: 互联网

标签:function 精通 resolve 函数 从入 promise Promise log


一. 为什么需要 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('已完成')
})

标签:function,精通,resolve,函数,从入,promise,Promise,log
来源: https://blog.csdn.net/baidu_24293811/article/details/120385473

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有