前端异步方案之Promise(附实现代码)

一、基本原理

一个Promise对象可以理解为这样一个状态机,它(通常)接收一个异步任务作为输入,然后去执行这个异步任务,根据异步任务的执行结果来改变自身的状态,并保留这个执行结果 。

这个状态机总共有三种状态:pending(异步任务执行中)、fullfilled(执行成功)和rejected(执行失败)。在异步任务执行完之前,状态机处于pending状态;一旦任务执行成功,状态就会转为fullfilled;如果任务执行失败,则状态转为rejected。一旦发生了状态转变,状态机就会冻结在该状态,在任何情况下都不会再次改变状态。此时的状态机处于resolved(已完成)状态,它表示状态机已经执行完了异步任务。

我们可以通过Promise对象上的原型方法then为状态机注册回调函数,注册进来的回调函数会在状态发生相应变化时被调用。它接收两个函数作为参数,第一个是状态为fullfilled(成功)时要调用的函数,第二个是状态为rejected(失败)时需要执行的函数。我们可以多次调用then方法为promise注册多个回调函数,他们以数组的形式保存在promise内,并在状态机状态变化时依次被调用。举个例子:

//这里传入的function就是我们封装的一个异步任务
var promise = new Promise(function(resolve, reject){
  $.ajax({
    url: url,
    method: "GET",
    success: function(data){
      resolve(data);    //将状态机的状态变为fullfilled(成功)
    },
    error: function(err){
      reject(err);     //将状态机的状态变为rejected(失败)
    }
  })
});

promise.then(function(){...}, function(){...}); //两者分别在异步任务执行成功和失败时被调用

//我们又为promise注册了第二个fullfilled回调函数,它也会在状态转为fullfilled时执行
promise.then(function(){...}) 

在使用new Promise生成一个promise对象时,我们传入了一个函数作为参数,它封装了一个ajax异步任务。js引擎在调用这个函数时会为我们提供两个定义在Promise内部的方法resolve和reject(由于只是形参,你可以用任何合法的变量名来接收这两个参数),前者用于通知状态机将状态由pending变为fullfilled(成功),后者用于通知状态机将状态由pending变为rejected(失败)。我们在调用这两个方法时传入的值,将作为状态机执行异步任务的结果保存在状态机内,并在执行回调函数时作为参数传入回调函数。

前面说到,状态机的状态一旦发生了变化,就会被冻结。因此如果你调用了resolve将状态机的状态变为成功,那么就不能再将其变为失败,反之亦然。由于状态机被冻结,js引擎将不会执行resolve或reject语句后面的代码(因为它们已经无法再影响到状态机)。

说到这里,我们就可以理解一下Promise为什么被称为Promise。Promise中文释义为“承诺”,那么promise对象向我们“承诺”了什么呢?

它承诺的是它的状态永远只取决于异步任务的执行结果,你无法在状态机的外部通过任何方式改变状态机的状态。而且promise会为你保留这个异步任务的执行结果,你可以在任何时候向它注册回调函数,它都会根据当前状态为你执行它们,哪怕这个异步任务早已经执行完毕。如:

var promise = new Promise(function(resolve, reject){
  setTimeout(function(){
    resolve(1)
  }, 100);  //100毫秒后将状态变为fullfilled
}).then(function(data){ //这里是在异步任务还未执行完时就注册到promise对象上的回调
  console.log(data);
})

//我们在5秒后又为promise注册了一个回调,显然此时异步任务已经执行完了,
//但我们注册的回调函数依然会被执行
setTimeout(function(){   
  promise.then(function(){...})  //这里的回调也会被执行
}, 5000)

注意:传入Promise构造函数的函数(也就是我们封装的异步任务)会立即被调用,因此是同步执行的。而我们为该对象注册的回调函数将被存储在微任务队列,他们会在同步任务执行完毕后被执行,但是执行的优先级高于宏任务(如setTimeout任务),这点在Promise的实现中可以找到答案。

二、为什么要使用Promise?

要回答这个问题,我们可以先思考一下以前我们是如何按顺序执行多个异步任务的(当上个异步任务的结果会对下一个异步任务产生影响时,你必须保证它们按顺序执行,否则后面的异步任务将无法启动)。

不知道你有没有写过下面这种令人头皮发麻的代码:

$.ajax({
    url: url,
    method: "GET",
    success: function(data){
    
      //在第一个ajax请求成功后,继续发送第二个ajax请求
      $.ajax({
    	url: url,
    	method: "GET",
    	data: data,
    	success: function(data){
    	
    	    //第二个ajax请求成功后,继续发送第三个ajax请求
      		$.ajax({
    			url: url,
    			method: "GET",
    			data: data,
    			success: function(data){
    			
      				......   //这里可能还有更多的ajax请求需要发送
      				
    			},
    			error: function(err){
      				console.log(err);
    			}
  			})
    	},
    	error: function(err){
      		console.log(err);
    	}
  	  })
    },
    error: function(err){
      console.log(err);
    }
  })

类似的回调函数嵌套在前端称为“回调地狱”。过深的嵌套会导致代码非常难以阅读,难以阅读的代码从另一个角度来说就是难以维护,而难以维护的代码就是垃圾代码。如果写成下面的形式,代码看上去就会清爽很多:

var promise = new Promise(function(resolve, reject){
  $.ajax({
    url: url,
    method: "GET",
    success: function(data){
      resolve(data);    //将状态机的状态变为fullfilled(成功)
    },
    error: function(err){
      reject(err);     //将状态机的状态变为rejected(失败)
    }
  })
}).catch(function(err){
  console.log(err)
}).then(function(data){
  return new Promise(function(resolve, reject){
    $.ajax({
      url: url,
      method: "GET",
      data: data,
      success: function(data){
        resolve(data);    //将状态机的状态变为fullfilled(成功)
      },
      error: function(err){
        reject(err);     //将状态机的状态变为rejected(失败)
      }
    })
  })
}).catch(function(err){
  console.log(err)
}).then(function(data){
  return new Promise(function(resolve, reject){
    $.ajax({
      url: url,
      method: "GET",
      data: data,
      success: function(data){
        resolve(data);    //将状态机的状态变为fullfilled(成功)
      },
      error: function(err){
        reject(err);     //将状态机的状态变为rejected(失败)
      }
    })
  })
}).catch(function(err){
  console.log(err);
})

我们可以很容易理解上面的代码在做什么:先发送一个ajax请求,成功后执行then内的回调函数,并把上个ajax的返回值传进去,依次类推。由于这里的每个then方法都返回了新的promise对象,因此你需要在每个then后面都附带一个catch,依次处理每个promise抛出的异常。该代码同样实现了三个ajax任务的顺序执行,但由于采用了链式结构,代码变得非常易读。

如果你不太理解为什么Promise可以使用链式语法,没关系,我们会在后面手动实现Promise时详细介绍这一点。

除此之外,如果不使用Promise,你必须在创建一个异步任务时就指定回调函数,已经执行或执行完毕的异步任务不会接收新的回调函数。而Promise为异步任务提供了更好的封装,它可以保留异步任务的结果状态,以便你可以在任何时候为异步任务注册回调。

Promise因对异步任务的良好封装,而被纳入了ES6语言规范。

三、Promise的使用

上面已经介绍了Promise最简单的用法,就是创建promise对象时传入一个异步任务,然后注册成功和失败回调。当异步任务执行完毕,状态机会根据自身状态调用相应的回调函数。下面我们来看一下Promise的更多常见用法。

1. 使用catch代替then的第二个参数

我们上面说到,then方法可以接收两个函数,分别是成功和失败时需要调用的回调函数,但是在实际使用Promise时我们却很少这样写。我们一般只会为then传入一个成功的函数,然后使用链式语法继续添加一个catch方法来处理失败的情况。举个例子:

//我们一般不会这样写
var promise = new Promise(function(resolve, reject){
  ... //执行一个异步任务
}).then(function(data){/*成功回调*/},  function(err){/*失败回调*/})

//我们更推荐这样写
var promise = new Promise(function(resolve, reject){
  ... //执行一个异步任务
}).then(function(data){/*成功回调*/})
  .catch(function(err){/*失败回调*/})

显然使用catch更加如何符合链式语法。而catch本质上只是then方法的一个变形,它等同于下面的实现:

Promise.prototype.catch = function(onRejected){
  return this.then(null, onRejected)
}

也就是说,当你向then方法的第一个参数传入null或undefined,它就是catch方法,因为此时的then只能处理失败的情况。这样做的一个很大的优势在于,当链式调用很长,而你并不在乎出错是哪个调用导致的,你就可以只在最后面写一个catch处理即可。如:

new Promise(function(resolve, reject){
  ...
}).then(function(){
  ...
}).then(function(){
  ...
}).then(function(){
  ...
}).catch(function(err){
  ... //在这里捕获错误
})

网上有人把这种情况归结为Promise的数据传递具有“穿透”性,错误对象会在链式语法中进行传递。不过我本人更倾向于另一种解释:这里的三个then和一个catch都是注册在同一个Promise对象上的(假设注册的then方法没有返回值,如果有,它可能会影响原Promise对象的data),它们会共享同一个promise状态机的状态,也就是说每个then其实都可以拿到相同的参数,所以这种现象是理所当然的。

从内部结构来看,前三个传入then的函数被添加到了promise对象的成功回调函数队列,第四个catch中的函数被添加到了失败回调函数队列,如下:

promise = {
  resolvedFunc: [func1, func2, func3],  //注册进来的成功回调函数
  rejectedFunc: [func]     //注册进来的失败回调函数
  ...  //其他属性
}

假如promise的状态变为成功,则resolvedFunc队列的函数依次被执行,并传入调用resolve方法时传入的异步任务的执行结果。假如promise的状态变为失败,则rejectedFunc队列的函数依次被执行,以reject方法抛出的错误为参数。

也就是说,虽然我们通过链式语法连续注册了四个回调函数,但他们最终会进入两个队列中,在实际调用时没有任何关系。每个回调函数在执行时所接收的数据对象都仅仅来自于promise实例,而不是由上一个函数“传递”下来的。

不过当你在then方法中重新创建并返回了一个新的Promise对象后,你后续注册的回调函数都会注册到这个新的promise对象上,你可以手动将上个promise抛出的异常传递给下一个promise。如:

new Promise(function(resolve, reject){
  ...
}).then(function(data){
  return new Promise(function(resolve, reject){
    ...
    //你可以在这里继续传递上一个promise的任务结果
  })
}).then(function(data){
  //现在你是在为上一个then中返回的新的promise对象注册回调函数,而不是最初的那个promise
})

2. 使用finally对成功和失败进行统一处理

假设你现在是在访问文件系统进行文件读取,那么无论读取成功或失败,你都应该关闭文件模块,这时候你就可以使用finally方法,它规定无论promise的状态无论变成成功还是失败,都要执行该回调函数。如:

new Promise(function(resolve, reject){
  fs.open();
  fs.readFile(filename, function(...){
    ...
  })
})
.then(function(data){...}).catch(function(err){...})
.finally(function(){
    fs.close();    //无论文件读取成功或失败,都关闭文件模块
})

3. Promise.all()

Promise.all接收若干个promise对象构成的数组作为参数,返回一个新的promise对象。如:

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

p.then(function(){...}).catch(function(){...})

这里的p1,p2,p3都是promise对象,p的状态符合以下情况:

  1. 当p1、p2、p3至少有一个状态为pending(也就是未完成)时,p的状态为pending。
  2. 当三者的状态全部为fullfilled(成功)时,p的状态为fullfilled。
  3. 当三者至少有一个状态为rejected(失败)时,p的状态为rejected。

简单来说,当三者没有全部完成时,p就未完成,当三者全部成功,p就为成功,只要有一个失败,p的状态就是失败。

当你需要在多个异步任务全部成功后执行某些操作,你就可以使用Promise.all()。假如现在要读取若干个json文件,然后在全部读取成功后提示用户,就可以这么写:

// 生成一个Promise对象的数组
const promises = [2, 3, 5, 7, 11, 13].map(function (id) {
  return getJSON('/post/' + id + ".json");
});

Promise.all(promises).then(function (posts) {
  //文件读取成功
  ...
}).catch(function(reason){
  //文件没有全部读取到
  ...
});

4. Promise.race()

race:中文释义为赛跑,在这里的含义为哪个执行得快,它就可以决定最终promise的状态。

与上面的Promise.all类似,race方法也会返回一个Promise对象,但它的状态取决于最先完成的那个Promise的状态。比如我们现在要取一个文件,但是规定了超时时间为5秒(超过5秒就判定为超时),我们可以使用下面的语法:

const p = Promise.race([
  fetch('/resource-that-may-take-a-while'),
  new Promise(function (resolve, reject) {
    setTimeout(() => reject(new Error('request timeout')), 5000)
  })
]);

p.then(console.log)
.catch(console.error);

我们为Promise.race传入了两个promise,前者用于获取一个资源,后者会在5秒后将状态变为失败。假如fetch先执行完(由于第二个promise会在5秒后改变状态,因此fetch先执行完的含义就是,该http请求在5秒内得到响应),它就可以率先改变最终的promise的状态,使其与自身状态保持一致,否则setTimeout定时任务会把状态变为失败。

5. 其他方法

这里指Promise.allSettled()、Promise.any()、Promise.resolve()、Promise.reject()。由于篇幅有限,并且这些方法较新或者用的不多,我们只做简单的介绍,详细请参考阮一峰 Promise入门

Promise.allSettled返回一个新的Promise对象,记为p,也接收一个promise对象数组作为参数。当所有的promise状态都变为resolved(已完成,不管成功或失败)时,p的状态变为fullfilled。否则p的状态将一直是pending。注意,Promise.allSettled返回的promise没有失败状态,它永远只会从未完成转变为成功。

Promise.any同上,返回一个新的Promise对象,记为p,也接收一个promise对象数组作为参数。只要有一个promise的状态变为fullfilled(成功),p的状态就是fullfilled。只有所有的promise都是rejected(失败)时,p的状态才会变成rejected。这一点类似于数组方法中的some。

Promise.resolve可以将一个普通对象(或基本数据类型)转化为一个promise对象,如果该对象具有then方法,那么它将作为异步任务传递给新创建的promise对象,否则,js引擎会直接创建一个promise对象,状态为resolved,并且值为传入的那个参数。这里不再详述。

Promise.reject可以创建一个状态为rejected的promise对象。同样的,如果传入的对象具有then方法,它将作为异步任务用于promise的创建,即:

const thenable = {       //这是一个具有then方法的对象
  then(resolve, reject) {
    reject('出错了');
  }
};

Promise.reject(thenable)       
.catch(e => {
  console.log(e === thenable)
})

//上面等同于该写法,resolve方法也是同样的原理
new Promise(thennable.then)
.catch(e => {
  console.log(e === thenable)
})

四、手动实现一个Promise

看了上面的介绍,你可能还是不了解Promise的内部原理究竟是怎么样的。没关系,我们可以一步步手动实现一个Promise类(本代码参考自github项目xieranmaya/Promise3,感兴趣的可以阅读原作者的介绍)。

首先,根据我们调用Promise的方式可以知道,Promise应该是一个构造函数,并且接受一个函数作为参数。

//这里的exector就是我们传入new Promise的函数,它封装了我们的异步任务
function Promise(executor){
  ...
}

根据前文介绍,我们知道一个promise内至少应该维护四个变量:状态、异步任务的结果、成功的回调队列、失败的回调队列。

function Promise(executor){
  var self = this
  self.status = 'pending' // Promise当前的状态
  self.data = undefined  // Promise的值
  self.onResolvedCallback = [] //成功时的回调
  self.onRejectedCallback = [] //失败时的回调
  
  ...
}

接下来我们来定义resolve和reject这两个内部函数,我们需要把它们作为参数传给传入的executor函数。它们的任务有三个:

  1. 改变promise的状态
  2. 保存异步任务的结果
  3. 执行对应回调队列中的回调函数
function Promise(executor) {
  var self = this
  self.status = 'pending' // Promise当前的状态
  self.data = undefined  // Promise的值
  self.onResolvedCallback = [] //成功时的回调
  self.onRejectedCallback = [] ////失败时的回调

  function resolve(value) {
    if (self.status === 'pending') {
      self.status = 'fullfilled';   // 在这里将promise的状态变为fullfilled
      self.data = value;   // 接收调用resolve时传入的异步任务的执行结果,保存在data中

	  //依次调用成功回调队列的回调函数
      for(var i = 0; i < self.onResolvedCallback.length; i++) {
        self.onResolvedCallback[i](value)
      }
  }

  function reject(reason) {
    if (self.status === 'pending') {
      self.status = 'rejected';    // 在这里将promise的状态变为rejected
      self.data = reason;   // 接收调用reject时传入的失败原因,保存在data中

	  //依次调用失败回调队列的回调函数
      for(var i = 0; i < self.onRejectedCallback.length; i++) {
        self.onRejectedCallback[i](reason)
      }
    }
  }

  //考虑到执行executor的过程中有可能出错,所以我们用try/catch块给包起来,
  //并且在出错后以catch到的值reject掉这个Promise
  try { 
    executor(resolve, reject) // 执行executor
  } catch(e) {
    reject(e)
  }
}

现在Promise的构造函数已经封装完毕了。它已经可以根据传入的异步任务的执行结果改变自身的状态,并一直维护这个状态了。从这里可以看到,我们在调用Promise的构造函数时立即执行了传入的executor,因此它是同步执行的。

但是到现在,我们还没有提供向成功和失败的回调函数队列添加回调函数的方法,如果不能为开发者执行回调函数,那么这个状态机将没有任何意义。所以我们现在要实现promise的then方法了。

在之前的讲解中我们看到,then方法是在promise实例对象上调用的,那么很显然,它应该是Promise原型上的方法。所以我们现在为Promise的原型对象定义一个then方法:

//then方法接受一个成功的回调和一个失败的回调
Promise.prototype.then = function(onResolved, onRejected){
  var self = this
  var promise2

  // 根据标准,如果then的参数不是function,则我们需要忽略它,此处以如下方式处理
  onResolved = typeof onResolved === 'function' ? onResolved : function(v) {}
  onRejected = typeof onRejected === 'function' ? onRejected : function(r) {}

  if (self.status === 'fullfilled') {
    return promise2 = new Promise(function(resolve, reject) {
	   try {
	    //维持新promise与之前promise对象的状态同步,这样在进行
	    //链式调用时,就如同在访问同一个promise对象
        var x = onResolved(self.data)   
        if (x instanceof Promise) { // 如果onResolved的返回值是一个Promise对象,直接取它的结果做为promise2的结果
          x.then(resolve, reject)
        }
        resolve(x) // 否则,以它的返回值做为promise2的结果
      } catch (e) {
        reject(e) // 如果出错,以捕获到的错误做为promise2的结果
      }
    })
  }

  if (self.status === 'rejected') {
    return promise2 = new Promise(function(resolve, reject) {
		try {   //同上
        var x = onRejected(self.data)
        if (x instanceof Promise) {
          x.then(resolve, reject)
        }
      } catch (e) {
        reject(e)
      }
    })
  }

  //如果当前promise还未执行完毕,则需要把注册的回调函数添加到Promise中对应的队列里
  //并像上面一样创建一个promise,以支持链式调用
  if (self.status === 'pending') {
    return promise2 = new Promise(function(resolve, reject) {
		self.onResolvedCallback.push(function(value) {
        try {
          var x = onResolved(self.data)
          if (x instanceof Promise) {
            x.then(resolve, reject)
          }
        } catch (e) {
          reject(e)
        }
      })

      self.onRejectedCallback.push(function(reason) {
        try {
          var x = onRejected(self.data)
          if (x instanceof Promise) {
            x.then(resolve, reject)
          }
        } catch (e) {
          reject(e)
        }
      })
    })
  }
}

我们看到,then方法的返回值也是一个Promise对象,这是Promise支持链式语法的关键。因为当你调用了then方法后得到的又是一个promise对象,那么你就可以在这个对象上继续调用then方法,也就形成了链式结构。

这里then方法根据当前状态的不同走了三个分支。如果当前状态为fullfilled,那么应该立即执行刚刚注册的成功的回调函数,传入异步任务的结果data。然后按照当前promise的状态创建一个新的promise对象并返回,以此支持链式调用。如果当前状态时rejected,过程类似,只是需要执行失败的回调函数。而如果当前状态为pending,说明异步任务还没有执行完,因此不能执行回调函数。这时需要把传入的成功和失败的回调函数添加到对应的回调队列,等待任务执行完毕时执行。

同时我们还可以实现catch方法,它是then方法的变形:

Promise.prototype.catch = function(onRejected) {
  return this.then(null, onRejected)
}

至此,我们就实现了一个很简单的Promise,它具有我们上面提到的大部分功能,并且很好地揭示了Promise的工作原理。完整的实现代码如下:

function Promise(executor) {
  var self = this
  self.status = 'pending' // Promise当前的状态
  self.data = undefined  // Promise的值
  self.onResolvedCallback = [] //成功时的回调
  self.onRejectedCallback = [] ////失败时的回调

  function resolve(value) {
    if (self.status === 'pending') {
      self.status = 'fullfilled';   // 在这里将promise的状态变为fullfilled
      self.data = value;   // 接收调用resolve时传入的异步任务的执行结果,保存在data中

	  //依次调用成功回调队列的回调函数
      for(var i = 0; i < self.onResolvedCallback.length; i++) {
        self.onResolvedCallback[i](value)
      }
  }

  function reject(reason) {
    if (self.status === 'pending') {
      self.status = 'rejected';    // 在这里将promise的状态变为rejected
      self.data = reason;   // 接收调用reject时传入的失败原因,保存在data中

	  //依次调用失败回调队列的回调函数
      for(var i = 0; i < self.onRejectedCallback.length; i++) {
        self.onRejectedCallback[i](reason)
      }
    }
  }

  //考虑到执行executor的过程中有可能出错,所以我们用try/catch块给包起来,
  //并且在出错后以catch到的值reject掉这个Promise
  try { 
    executor(resolve, reject) // 执行executor
  } catch(e) {
    reject(e)
  }
}

Promise.prototype.then = function(onResolved, onRejected){
  var self = this
  var promise2

  // 根据标准,如果then的参数不是function,则我们需要忽略它,此处以如下方式处理
  onResolved = typeof onResolved === 'function' ? onResolved : function(v) {}
  onRejected = typeof onRejected === 'function' ? onRejected : function(r) {}

  if (self.status === 'fullfilled') {
    return promise2 = new Promise(function(resolve, reject) {
	   try {
	    //维持新promise与之前promise对象的状态同步,这样在进行
	    //链式调用时,就如同在访问同一个promise对象
        var x = onResolved(self.data)   
        if (x instanceof Promise) { // 如果onResolved的返回值是一个Promise对象,直接取它的结果做为promise2的结果
          x.then(resolve, reject)
        }
        resolve(x) // 否则,以它的返回值做为promise2的结果
      } catch (e) {
        reject(e) // 如果出错,以捕获到的错误做为promise2的结果
      }
    })
  }

  if (self.status === 'rejected') {
    return promise2 = new Promise(function(resolve, reject) {
		try {   //同上
        var x = onRejected(self.data)
        if (x instanceof Promise) {
          x.then(resolve, reject)
        }
      } catch (e) {
        reject(e)
      }
    })
  }

  //如果当前promise还未执行完毕,则需要把注册的回调函数添加到Promise中对应的队列里
  //并像上面一样创建一个promise,以支持链式调用
  if (self.status === 'pending') {
    return promise2 = new Promise(function(resolve, reject) {
		self.onResolvedCallback.push(function(value) {
        try {
          var x = onResolved(self.data)
          if (x instanceof Promise) {
            x.then(resolve, reject)
          }
        } catch (e) {
          reject(e)
        }
      })

      self.onRejectedCallback.push(function(reason) {
        try {
          var x = onRejected(self.data)
          if (x instanceof Promise) {
            x.then(resolve, reject)
          }
        } catch (e) {
          reject(e)
        }
      })
    })
  }
}

Promise.prototype.catch = function(onRejected) {
  return this.then(null, onRejected)
}

总结

本文探讨了Promise的基本用法及一个简单版本的promise的实现,promise的实现代码参考了xieranmaya关于promise3的实现,如果感兴趣,请点击查看github链接。

es的异步解决方案不止promise这一个,其他的还有Generator函数、async函数等。从某种程度上来说,它们可能比promise更加优雅,只是由于浏览器的支持性问题还未得到大量普及。希望大家尽快学习和使用。

发布了37 篇原创文章 · 获赞 90 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/qq_41694291/article/details/103096130