Promise对象

Promise 的含义

Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。它由社区最早提出和实现,ES6将其写进了语言标准,统一了用法,原生提供了Promise对象。

所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。

Promise对象有以下两个特点。

(1)对象的状态不受外界影响。Promise对象代表一个异步操作,有三种状态:Pending(进行中)、Resolved(已完成,又称 Fulfilled)和Rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是Promise这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变。

(2)一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能:从Pending变为Resolved和从Pending变为Rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果。如果改变已经发生了,你再对Promise对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。

有了Promise对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise对象提供统一的接口,使得控制异步操作更加容易。

Promise也有一些缺点。首先,无法取消Promise,一旦新建它就会立即执行,无法中途取消。其次,如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。第三,当处于Pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。

如果某些事件不断地反复发生,一般来说,使用 stream 模式是比部署Promise更好的选择。

我们看一下异步和同步

当cpu面对一个长时间、其他设备处理,比如磁盘I/O,数据库查询、写入等等这些操作的时候,此时有两种模式:第一种是同步模式,死等这个设备处理完成,此时cpu被堵塞了;第二种就是异步模式,不等这个设备处理完成,而是先执行后面的语句,等这个设备处理完成之后执行回调函数。

比如下面的代码,先让cpu处理累加计算的逻辑,然后再命令硬盘异步读取文件,此时读取过程中没有堵塞进程,cpu提前执行后面的累加程序,等读取完毕之后再执行读取内部的回调函数

var fs = require("fs");
// 0-100的累加
for(var i = 0,sum = 0; i <= 100; i++) {
  sum += i;
}
console.log(sum);
// 读取文件
fs.readFile("./text.txt",(err,content)=>{
  console.log(content.toString())
})
// 0-100的累加
for(var i = 0,sum = 0; i <= 100; i++) {
  sum += i;
}
console.log(sum);

 

 

 如果我们将代码改为同步的

var fs = require("fs");
// 0-100的累加
for(var i = 0,sum = 0; i <= 100; i++) {
  sum += i;
}
console.log(sum);
// 同步读取文件
var content = fs.readFileSync("./text.txt")
console.log(content.toString())
// 0-100的累加
for(var i = 0,sum = 0; i <= 100; i++) {
  sum += i;
}
console.log(sum);

  

 

 

 此时发现,同步和异步的区别从语法上看,同步是在等号左侧接收,异步是在回调函数内部接收结果,除了处理语法层面上不同之外,时间上也有区别,同步代码的时间,会比异步代码的耗费时间要长

异步的语法缺点

第一个缺点是异步代码的语法上会让代码变得不美观

fs.readFile("./txt/01.txt",(err,content)=>{
  console.log(content.toString())
  fs.readFile("./txt/02.txt",(err,content)=>{
    console.log(content.toString());
    fs.readFile("./txt/03.txt",(err,content)=>{
      console.log(content.toString());
    })
  })
})

此时如果我们改成同步的,的确就美观了,此时我们也会发现cpu的效率变低了

console.log(fs.readFileSync("./txt/01.txt"));
console.log(fs.readFileSync("./txt/02.txt"));
console.log(fs.readFileSync("./txt/03.txt"));

第二个缺点是异步的语法造成了,代码的维护性和机构性不强

Promise的基本使用

var fs = require("fs")
function File(url){
  return new Promise((resolve,reject)=>{
    fs.readFile(url,(err,content)=>{
      if(err) {
        reject(err)
        return;
      }
      resolve(content.toString())
    })
  })
}
//处理我们的回调逻辑的部分
File("./text.txt").then((data)=>{
  console.log(data);
  return File("./text2.txt")
})
.then(data=>{
  console.log(data);
  return File("./text3.txt")
})
.then(data=>{
  console.log(data);
})

 

 

 

 Promise的封装

function File(url){
  return new Promise((resolve,reject)=>{
    fs.readFile(url,(err,content)=>{
      if(err) {
        reject(err)
        return;
      }
      resolve(content.toString())
    })
  })
}
  • File函数返回了一个Promise的对象,这个对象就是构造函数,是ES6新增的
  • Promise一定是某一个函数的返回值,Promise必须当做一个函数的返回值才有意义,上面代码中Promise就是File函数的返回值
  • Promise在被new的时候要求传入一个函数,这个函数就是Promise的回调函数,这个函数有两个形参,分别是resolve和reject;resolve表示成功之后做的事情,reject表示失败之后做的事情

下面代码中then的部分表示的就是读取成功之后的操作,catch表示的是读取失败的操作

File("./text.txt")
.then((data)=>{
  console.log(data)
}).catch(err=>{
  console.log("读取错误",err);
})

下面的catch一旦遇到了错误就会阻止整个程序的执行,比如读取两个文件,第一个文件读取失败了

File("./text1.txt").then((data)=>{
  console.log(data);
  return rFile("./text.txt")
}).then((data)=>{
  console.log(123)
}).catch(err=>{
  console.log("读取错误",err);
})

上面代码中,发现由于没有text1.txt,所以会走catch,此时你会发现第二个then中123也没有输出

 

 如果想要输出第二个then中的data可以使用单个then报错的方式,第二个参数

File("./text1.txt").then((data)=>{
    console.log(data);
    return rFile("./text2.txt")
  },(err)=>{
    console.log("读取错误text1",err);
  }).then((data)=>{
    console.log(data)
  })

 

 上面的代码中虽然text1.txt没有抛出错误了,但是没有阻止下一个then的执行,输出了then中的data为underfined。

注意:

  • Promise函数中,内部的语句一定是异步语句,异步语句的成功将通过resolve(成功的数据)传出去,这个数据将成为后面调用的then函数中的data返回值,异步语句的失败将通过reject(失败的数据)传出去,这个数据将成为后面调用的catch函数中的err返回值,失败的语句也可以是then的第二个参数
  • Promise的实例拥有then的能力,then里面接收一个参数,data就是创建promise的时候的resolve;then进行连续打点,then方法返回的是一个新的Promise实例(注意,不是原来那个Promise实例)。因此可以采用链式写法,即then方法后面再调用另一个then方法。
File("./text.txt").then((data)=>{
    console.log(data);
    return File("./text2.txt")
  }).then((data)=>{
    console.log(data)
    return File("./text3.txt")
  }).then(data=>{
    console.log(data)
  })

 非Node环境使用Promise

我们看一下浏览器调用Ajax读取文件

  <script>
        $("#button").click(function(){
          $.get("./text.txt",function(data){
            console.log(data);
            $.get("./text2.txt",function(data){
              console.log(data);
              $.get("./text3.txt",function(data){
                console.log(data);
              })
            })
          })
          console.log("输出了");
        })
      </script>

 

  发现请求也是异步的,并且也有回调黑洞的问题

可以使用Promise去解决这个问题

function readFile(url){
    return new Promise((resolve,reject)=>{
      $.get(url,(data)=>{
        resolve(data)
      })
    })
  }
$("#button").click(function(){
    readFile("./text.txt").then(data=>{
      console.log(data);
      return readFile("./text2.txt")
    }).then(data=>{
      console.log(data);
      return readFile("./text3.txt")
    }).then(data=>{
      console.log(data);
    }).catch(err=>{
      console.log("读取错误",err);
    })
    console.log("输出了")
  })

 

 Promise本质上就是一个“语法糖”

什么是语法糖?语法糖(Syntactic sugar),也译为糖衣语法,指计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。通常来说使用语法糖能够增加程序的可读性,从而减少程序代码出错的机会

 所以Promise其实就是改变了异步的语法,本质没有改变异步,而是将异步嵌套改为then的连续打点写法

Promise.resolve方法

Promise可以.then调用是因为当前的实例是一个promise对象,promise对象是怎么形成的?我们可以用promise.resolve方法去创建一个对象

var str = Promise.resolve("hello");
console.log(str);

 

发现上面的代码将"hello"封装成了一个带有promise对象的壳子

此时这个str就可以.then去使用

var str = Promise.resolve("hello");
str.then(data=>{
    console.log(data)
})

 

 如果resolve方法的参数是一个对象内部有then方法

Promise.resolve方法会将这个对象转为Promise对象,然后就立即执行str2对象的then方法

let str2 = {
    then: function(resolve, reject) {
      resolve("hello");
    }
  };
  
  let str = Promise.resolve(str2);
  str.then(function(value) {
    console.log(value); 
  });
  

 

 Promise.all方法

Promise.all方法用于将多个Promise实例,包装成一个新的Promise实例

var fs = require("fs");
function File(url){
  return new Promise((resolve,reject)=>{
    fs.readFile(url,(err,content)=>{
      if(err){
        reject(err)
        return;
      }
      resolve(content.toString())
    })
  })
}

var str1 = File("./text.txt")
var str2 = File("./text2.txt")
var str3 = File("./text3.txt")

Promise.all([str1,str2,str3]).then((data)=>{
  console.log(data);
}).catch(err=>{
  console.error(err);
})

 

 上面代码中,Promise.all方法接受一个数组作为参数,str1、str2、str3都是Promise对象的实例,如果不是,就会先调用到Promise.resolve方法,将参数转为Promise实例,再进一步处理

str的状态由str1、str2、str3决定,分成两种情况。

(1)只有str1、str2、str3的状态都变成成功状态,str的状态才会变成成功,此时str1、str2、str3的返回值组成一个数组,传递给str的回调函数。

(2)只要str1、str2、str3之中有一个为失败,str的状态就变成失败,此时第一个被reject的实例的返回值,会传递给promise.all的catch回调函数

Promise.race方法

Promise.race方法同样是将多个Promise实例,包装成一个新的Promise实例

var fs = require("fs");
function File(url){
  return new Promise((resolve,reject)=>{
    fs.readFile(url,(err,content)=>{
      if(err){
        reject(err)
        return;
      }
      resolve(content.toString())
    })
  })
}

var str1 = File("./text.txt")
var str2 = File("./text2.txt")
var str3 = File("./text3.txt")
var strAll=Promise.race([str1,str2,str3])
strAll.then(data=>{
    console.log(data);
}).catch(err=>{
    return err
})

 

 和promise.all方法不同的是只要str1、str2、str3之中有一个实例率先改变状态,strAll的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给strAll的回调函数;

两个非es6提供的方法

Promise.done方法

Promise对象的回调链,不管以then方法或catch方法结尾,要是最后一个方法抛出错误,都有可能无法捕捉到(因为Promise内部的错误不会冒泡到全局)。因此,我们可以提供一个done方法,总是处于回调链的尾端,保证抛出任何可能出现的错误。

var fs = require("fs");
Promise.prototype.done = function(onFulfilled, onRejected){
  this.then(onFulfilled, onRejected)
    .catch(function (reason) {
      // 抛出一个全局错误
      setTimeout(() => { console.error("信息错误") }, 0);
    });
}
function File(url){
  return new Promise((resolve,reject)=>{
    fs.readFile(url,(err,content)=>{
      if(err){
        reject(err)
        return;
      }
      resolve(content.toString())
    })
  })
}

var str = File("./tex1t.txt")

var str2 = Promise.race([str])
str2.then(data=>{
  console.log(data);
}).done()

 

 Promise.finally方法

finally方法用于指定不管Promise对象最后状态如何,都会执行的操作。它与done方法的最大区别,它接受一个普通的回调函数作为参数,该函数不管怎样都必须执行。

读正确的文件:

  var fs = require("fs");
  Promise.prototype.finally = function (callback) {
    let S = this.constructor;
    return this.then(
      value  => S.resolve(callback()).then(() => value),
      reason => S.resolve(callback()).then(() => { throw reason })
    );
  };
  function File(url){
    return new Promise((resolve,reject)=>{
      fs.readFile(url,(err,content)=>{
        if(err){
          reject(err)
          return;
        }
        resolve(content.toString())
      })
    })
  }
  
  var str = File("./text.txt")
  
  var str2= Promise.race([str])
  str2.then(data=>{
    console.log(data);
  }).finally(()=>{
    console.log("最后执行");
  })
  

 

 读错误的文件:

  var fs = require("fs");
  Promise.prototype.finally = function (callback) {
    let S = this.constructor;
    return this.then(
      value  => S.resolve(callback()).then(() => value),
      reason => S.resolve(callback()).then(() => { throw reason })
    );
  };
  function File(url){
    return new Promise((resolve,reject)=>{
      fs.readFile(url,(err,content)=>{
        if(err){
          reject(err)
          return;
        }
        resolve(content.toString())
      })
    })
  }
  
  var str = File("./te1xt.txt")
  
  var str2= Promise.race([str])
  str2.then(data=>{
    console.log(data);
  }).finally(()=>{
    console.log("最后执行");
  })
  

此时会报错,但依旧会执行回调函数

 

posted @ 2021-10-25 17:03  keyeking  阅读(369)  评论(0编辑  收藏  举报