Promise对象

261 阅读10分钟

首先先理解以下几个概念的概念:

同步:一次只能执行一次任务,这个任务执行完之后才能执行下一个,它会阻塞其他任务。

异步:可以一起执行多个任务。

回调地狱:回调函数的层层嵌套,导致代码层次过多,不好理解和维护。

一、Promise的含义

先简单了解以下Promise的含义和特性:

  • Promise是异步编程的一种解决方案,比传统的解决方案(回调函数和事件)更加合理和强大;

  • 所谓的Promise,简单来说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果;

  • 从语法上来说,Promise是一个对象,从它可以获取异步操作的消息;

  • Promise对象代表的是一个异步操作,有三种状态: pending(等待状态)、 fulfilled(成功状态)、 rejected(失败状态)

Promise对象的特点:

1. 对象的状态不收外界的影响;

只有异步操作的结果才能决定当前是哪一种状态,任何其他操作都无法改变这个状态。

2. 一旦状态改变就不会再变;

Promise对象状态改变只有两种可能:

  • 从pending变为fulfilled
  • 从pengding变为rejected

只要这两种状态发现了,状态就凝固了,不会再变了,会一直保持这个结果,这时称为resolved(已定型)

3. 每个Promise实例都有一个then方法,一个Promise可以.then多个;

4. 每次执行Promise的时候,都会返还一个新的Promise实例;

有了Promise对象,就可以将异步操作以同步操作的流程表达出来,解决了异步层层嵌套的函数(简称回调地狱)问题。

说到 Promise,我们首先想到的最核心的功能就是异步链式调用。

Promise对象的缺点:

  1. 无法取消Promise。一旦新建它就会立即执行,无法中途取消;

  2. 如果不设置回调函数,Promise内部抛出错误,不会反应到外部;

  3. 当处于Pending(进行中)状态时,无法得知目前进展到哪一个节点(刚开始还是即将完成);

使用Promise对象的优点:

  1. 可以解决异步嵌套问题(回调地狱);
  2. 可以解决多个异步并发问题;

二、基本用法

注意,为了行文方便,本章后面的resolved统一只指fulfilled状态,不包含rejected状态。

1、创造一个Promise实例

ES6规定,Promise对象时一个构造函数,使用new来生成Promise实例。

let promise = new Promise(() => {//executor执行器,特点是立即执行

})

这个Promise实例是一个类,允许接受一个函数作为参数,这个函数叫做executor执行器,特点是立即执行。如下会立即打印:

let promise = new Promise(() => {//executor执行器,作用立即执行
    console.log("立即执行");//Promise有三种状态,此处默认Pending状态
}) 
console.log("222");

/*控制台打印*/

//立即执行
//222

2、实例接受两个参数resolve,rejected,使用then

函数的可以接受两个参数resolve(代表成功状态)和rejected(代表失败状态),而且每个Promise实例都有一个

then方法。(注意:resolve和rejected必须配合then一起使用,不然会打印空值)

then分别放置了两个函数:

  • onfulfilled 实例成功要执行的逻辑
  • onrejected 实例失败要执行的逻辑
let promise = new Promise((resolve,rejected) => {
  resolve('成功');//如果这里调用的是resolve,那边then就会走成功的逻辑;
}).then(data => {//成功
  console.log(data);
},err => {//失败
  console.log(err);
})

/*控制台打印*/
//成功


let promise = new Promise((resolve,rejected) => {
  rejected('失败');//如果这里调用的是rejected,那边then就会走失败的逻辑;
}).then(data => {//成功
  console.log(data);
},err => {//失败
  console.log(err);
})

// 失败


//上边的代码也可以这样写:
let promise = new Promise((resolve, rejected) => {
    resolve('成功');
})
promise.then(data => {
    console.log(data);
}, err => {
    console.log(err);
})

因为Promise实例的状态一旦状态改变就不会再变,所以调用resolve之后再调用rejected,后一步是不会生效的。反之亦然。

let promise = new Promise((resolve,rejected) => {
  resolve('成功');
  rejected('失败');//这一步不会执行
}).then(data => {//成功
  console.log(data);
},err => {//失败
  console.log(err);
})

//成功

如果Promise实例内部报错,就会变成失败状态,不会执行后边的方法:

let promise = new Promise((resolve, rejected) => {//executor执行器,作用立即执行
    throw new Error('内部抛出错误');//内部抛出错误,就会变成失败状态,then就会走失败的逻辑
    resolve('成功');//这一步依然不会执行
}).then(data => {//成功
    console.log(data);
}, err => {//失败
    console.log(err);
})

//Error: 内部抛出错误
// ...

三、Promise实例的原理

1、手写一个基础版的Promise

新建一个promise.js,并新建一个Promise类,并导出:

//promise.js:
class Promise {

};
module.exports = Promise;//导出

将前面的代码引入promise.js:

//callBack.js:
let Promise = require('./promise');
//引入自定义的Promise,相当于下面用的Promise就是自定义的Promise

let promise = new Promise((resolve, rejected) => {
    resolve('成功');
}).then(data => {
    console.log(data);
}, err => {
    console.log(err);
})

根据callBack.js的Promise实例,自定义一个基础版的Promise,不考虑其他异常情况:

(1)创建公共的then方法

Promise实例有三种状态,不管是哪一种状态都会执行的then方法,所以then方法属于公共的属性。

//promise.js:
class Promise {
  then(){
  
  }
};
module.exports = Promise;//导出

每一个Promise实例都有自己的三个状态,所以将三种状态放到对应的构造函数(constructor)中。

在构造函数中可以拿到Promise实例的状态,因为这个三个状态会经常用到,所以我们可以把它们存成一个常量。

//promise.js:
const PENDING = "PENDING";//等待状态
const RESOLVED = "RESOLVED";//成功状态
const REJECTED = "REJECTED";//失败状态
class Promise {
  
  constructor(){//构造函数
    this.status = PENDING;//Promise实例的默认pending状态
  }
  
  then(){
  
  }
};
module.exports = Promise;//导出

我们在callBacks.js的new Promise实例时,传了一个函数(也就是一个执行器),并且这个函数是立即执行的,所以我们要给构造函数也传递一个executor执行器:

//promise.js:
const PENDING = "PENDING";//等待状态
const RESOLVED = "RESOLVED";//成功状态
const REJECTED = "REJECTED";//失败状态
class Promise {
  
  constructor(executor){//构造函数
    this.status = PENDING;//Promise实例的默认pending状态
    
    executor();//默认执行器会立即执行
  }
  
  then(){
  
  }
};
module.exports = Promise;//导出

callBacks.js的new Promise实例时,executor执行的时候传递了两个函数,并且属于当前的Promise实例,不需要再then中拿到,所以我们可以在constructor中声明两个函数:成功函数和失败函数。并且将这两个函数传递给executor执行器:

//promise.js:
const PENDING = "PENDING";//等待状态
const RESOLVED = "RESOLVED";//成功状态
const REJECTED = "REJECTED";//失败状态
class Promise {
  
  constructor(executor){//构造函数
    this.status = PENDING;//Promise实例的默认pending状态
    //成功函数
    let resolve = () => {
      
    }
    //失败函数
    let reject = () => {
    
    }
    
    
    executor(resolve,reject);//默认执行器会立即执行
  }
  
  then(){
  
  }
};
module.exports = Promise;//导出

成功函数和失败的函数都接受一个值,value和reason,因为在then也需要用到这两个值,所以我们需要定义一个变量。我们在执行完成功和失败函数之后,需要更新Promise的状态:

//promise.js:
const PENDING = "PENDING";//等待状态
const RESOLVED = "RESOLVED";//成功状态
const REJECTED = "REJECTED";//失败状态
class Promise {
  
  constructor(executor){//构造函数
    this.status = PENDING;//Promise实例的默认pending状态
    
    this.value = undefined;//成功的值
    this.reason = undefined;//失败的原因
    
    //成功函数
    let resolve = (value) => {
      this.value = value;
      this.status = RESOLVED;//Promise实例状态更新为成功状态:resolved状态
    }
    
    //失败函数
    let reject = (reason) => {
      this.reason = reason;
      this.status = REJECTED;//Promise实例状态更新为失败状态:rejected状态
    }
    
    
    executor(resolve,reject);//默认执行器会立即执行
  }
  
  then(){
  
  }
};
module.exports = Promise;//导出

因为Promise实例的状态一旦改变了就不能再变了,也就是说我们调用了resolve函数就不能再调用reject函数了,所以我们必须添加添加状态判断,只要状态时等待状态pending时,才执行函数:

//promise.js:
const PENDING = "PENDING";//等待状态
const RESOLVED = "RESOLVED";//成功状态
const REJECTED = "REJECTED";//失败状态
class Promise {
  
  constructor(executor){//构造函数
    this.status = PENDING;//Promise实例的默认pending状态
    
    this.value = undefined;//成功的值
    this.reason = undefined;//失败的原因
    
    //成功函数
    let resolve = (value) => {
      if(this.status === PENDING){//// 屏蔽调用,调了成功函数之后不能调失败函数
        this.value = value;
        this.status = RESOLVED;//调用成功之后,Promise状态变成resolved状态
      }
    }
    
    //失败函数
    let reject = (reason) => {
      if(this.status === PENDING){
        this.reason = reason;
        this.status = REJECTED;////调用失败之后,Promise状态变成rejected状态
      }
    }
    
    
    executor(resolve,reject);//默认执行器会立即执行
  }
  
  then(){
  
  }
};
module.exports = Promise;//导出

因为执行器执行的时候内部可能会报错,会抛出异常,这个时候添加try...catch进行逻辑处理,如果内部抛出异常的话相当于调用了reject失败函数:

img

接下来开始调用then( )方法,then可以放置了两个函数:

  • onfulfilled 实例成功要执行的逻辑
  • onrejected 实例失败要执行的逻辑

那么什么时候调用哪一个方法呢?这要根据Promise实例的状态来判断了:

img

Promise实例还有订阅发布功能:

未完待续,后续更新~~

promise.js完整代码:

//promise.js:
const PENDING = "PENDING";//等待状态
const RESOLVED = "RESOLVED";//成功状态
const REJECTED = "REJECTED";//失败状态
class Promise {
  
  constructor(executor){//构造函数
    this.status = PENDING;//Promise实例的默认pending状态
    
    this.value = undefined;//成功的值
    this.reason = undefined;//失败的原因
    
    this.onResolveCallBacks = [];//成功的回调的数组
    this.onRejectCallBacks = [];//失败的回调的数组
    
    //成功函数
    let resolve = (value) => {
      if(this.status === PENDING){//// 屏蔽调用,调了成功函数之后不能调失败函数
        this.value = value;
        this.status = RESOLVED;//调用成功之后,Promise状态变成resolved状态
         this.onResolveCallBacks.forEach(fn => fn())//发布
      }
    }
    
    //失败函数
    let reject = (reason) => {
      if(this.status === PENDING){
        this.reason = reason;
        this.status = REJECTED;////调用失败之后,Promise状态变成rejected状态
        this.onRejectCallBacks.forEach(fn => fn())//发布
      }
    }
    
    //执行器执行时,可能内部报错:
    try{
        executor(resolve,reject);//默认执行器会立即执行,接受resolve,reject作为参数
    }catch(error){
      reject(error);//如果执行器执行时发生错误,等价于调用了失败方法
    }
    
  }
  
  then(onfulfilled,onrejected){/// then 目前有两个参数:onfulfilled,onrejected
    //同步情况:成功之后执行逻辑
    if(this.status === RESOLVED){
       onfulfilled(this.value);
    }
    
    //同步情况:失败之后执行逻辑
    if(this.status === PENDING){
       onrejected(this.reason);
     }
    
     // 如果是异步就先订阅号
    if (this.status === PEDING) {
      this.onResolveCallBacks.push(() => {
        // todo
        onfulfilled(this.value)
      })

      this.onRejectCallBacks.push(() => {
        // todo
        onrejected(this.value)
      })
        }
  }
};
module.exports = Promise;//导出

五、常见的Promise面试题

1、实现函数sleep,先输出A,1秒之后输出B,有什么方案吗?

(1)通过Promise实现:

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

sleep(1000).then(() => {
  console.log('B');
});
        
//先输出A,延迟1秒之后输出B

//或
console.log('A');
const sleep = ((time)=>{
  return new Promise((resolve)=>{
    setTimeout(()=>{
      resolve();
    },time)
  })
})
sleep(1000).then(()=>{
  console.log('B');
})

(2)通过async/awiat进行实现:

const sleep = ((time)=>{
  return new Promise((resolve)=>{
    setTimeout(()=>{
      resolve();
    },time)
  })
})

async function sleepAsync(){
  await sleep(1000);
  console.log('B');
}

(3)从Generator配合yield进行实现

console.log("A");
const sleep = ((time)=>{
  return new Promise((resolive)=>{
    setTimeout(()=>{
      resolve();
    },time)
  })
})

function* sleepGenerator(time){
  yeild sleep(time);
}

sleepGenerator(1000).next().value.then(()=>{
  console.log("B");
})

相似的面试题:

经常有业务需求,要等几秒才进行下一步操作,也是使用以上的思路

2、红灯三秒亮一次,绿灯一秒亮一次,黄灯2秒亮一次;如何让三个灯不断交替重复亮灯?(用Promse实现+递归)

三个亮灯函数已经存在:

思路:

红灯三秒亮一次,绿灯一秒亮一次,黄灯2秒亮一次,意思就是3秒,执行一次 red 函数,2秒执行一次 green 函数,1秒执行一次 yellow 函数,不断交替重复亮灯,意思就是按照这个顺序一直执行这3个函数,这步可以就利用递归来实现。

function red() {
    console.log('red');
}
function green() {
    console.log('green');
}
function yellow() {
    console.log('yellow');
}

const sleep = ((time,fn)=>{
  return new Promise((resolve)=>{
    setTimeout(()=>{
      fn();//哪个灯亮
      resolve();
    },time)
  })
})

let step = (()=>{
  Promise.resolve().then(()=>{
    return sleep(3000,red);
  }).then(()=>{
    return sleep(2000,green);
  }).then(()=>{
    return sleep(1000,yellow);
  }).then(()=>{
    step();
  })
  
})

3、输出下面的执行结果:

Promise.resolve(1)
  .then(2)
  .then(Promise.resolve(3))
  .then(console.log)
  //输入1

Promise.resolve 方法的参数如果是一个原始值,或者是一个不具有 then 方法的对象,则 Promise.resolve 方法返回一个新的 Promise 对象,状态为resolved,Promise.resolve 方法的参数,会同时传给回调函数。

then 方法接受的参数是函数,而如果传递的并非是一个函数,它实际上会将其解释为 then(null),这就会导致前一个 Promise 的结果会穿透下面。

六、Promise的应用场景

...

学习写作中,持续补充更新,记录不好的地方望指出修改,共同进步~

参考资料:

ECMAScript 6 入门-Promise

关于Promise的面试题

手写Promise20行

剖析Promise内部结构,一步一步实现一个完整的、能通过所有Test case的Promise类

手写Promise