Promise

2022-2-13 JavaScriptES6

Promise介绍与自定义封装、async/await介绍

# 异步编程

ES6新增了正式的 Promise 引用类型,支持优雅地定义和组织异步逻辑,是JS中进行异步编程的新解决方案(旧方案是使用回调地狱)。接下来几个版本增加了使用 async 和 await 关键字定义异步函数的机制。

回调函数并不是函数自调用,而是将函数作为参数。一般情况下,把函数作为参数的目的就是为了获取函数内部的异步操作结果。异步是因为js是单线程的,对一些耗时的异步操作放在后面执行可提高性能。

异步操作:

  • fs 文件操作(readFile/writeFile/readdir)
  • 数据库操作
  • setTimeout
  • ajax

# Promise介绍与基本使用

Promise 是一个构造函数,用来封装一个异步操作并可以获取成功/失败的结果值。Promise支持链式调用,可以解决回调地狱问题。

使用:

const p = new Promise((resolve, reject) => {
  // 异步操作
  if(...) {
    resolve();
  }else{
    reject();
  }
});
p.then(() => {
  //成功后执行
}, () => {
  //失败后执行
})
1
2
3
4
5
6
7
8
9
10
11
12
13

# Promise的状态

Promise实例对象中有一个表示状态的属性PromiseState。有三种状态:

  1. pending 待定
  2. resolved/fulfilled 成功
  3. rejected 失败

只有 pending->resolved 和 pending->rejected 两种状态变化的可能,且一个Promise对象只能改变一次,无论变为成功还是失败,都会有一个结果数据,成功的结果数据一般称为value,失败的一般称为reason。

# Promise对象的值

PromiseResult保存的是异步任务成功或失败的结果。

# Promise API

  1. Promise构造函数:Promise(executor){}
    • executor为执行器函数,即(resolve,reject)=>{}
    • executor会在Promise内部立即同步调用,不会进入任务队列,异步操作在执行器中执行
  2. Promise.prototype.then(onResolved,onRejected){}
    • then方法里面的代码是异步执行
    • onResolved函数:成功时的回调函数
    • onRejected函数:失败时的回调函数
  3. Promise.prototype.catch(onRejected){}
    • 只能指定失败的回调函数
  4. Promise.resolve(value=>{})
    • 如果value为非Promise类型的对象,则返回的结果为成功promise对象
    • 如果value为Promise 对象,则参数的结果决定了resolve的结果
  5. Promise.reject(reason=>{})
    • 将参数快速转换成一个失败的promise对象
  6. Promise.all(promises=>{})
    • promises是包含n个promise的数组
    • 返回一个新的promise(如果成功,则是所有promise对象结果组成的数组),只有所有的promise都成功才成功,只要有一个失败了就直接失败
  7. Promise.race(promises=>{})
    • 返回一个新的promise,第一个完成的promise的结果状态就是最终的结果状态

注:Promise.prototype.fun()表示该函数是Promise实例对象的方法,而Promise.fun()表示该函数是Promise函数对象的方法。

# Promise关键问题

  1. 如何改变promise的状态?
    • 调用resolvereject函数
    • 使用throw抛出错误
  2. 一个promise指定(用then或catch方法指定)多个成功or失败回调函数,都会调用吗,即能否执行多个回调?
    • 当promise改变为对应状态时都会调用
  3. 改变promise状态(resolve or reject)和指定回调函数(then or catch)谁先谁后?
    • 都有可能,正常情况下是先指定回调函数再改变状态
    • 如何先改状态再指定回调?
      1. 当执行器中的任务是同步任务时,直接调用resolve()/reject()
      2. 延迟更长时间才调用then()
    • 什么时候才能得到数据,即回调函数什么时候执行?
      1. 如果先指定的回调,那当状态发生改变时,回调函数就会调用,得到数据
      2. 如果先改变的状态,那当指定回调时,回调函数就会调用,得到数据
  4. Promise.prototype.then()返回的新promise的结果状态由什么决定?
    • 如果抛出异常,新promise变为rejected, reason为抛出的异常
    • 如果返回的是非promise的任意值,新promise变为resolved, value为返回的值
    • 如果返回的是另一个新promise,此promise的结果就会成为新promise的结果
  5. promise如何串连多个操作任务?
    • 通过then的链式调用串连多个操作任务,因为then返回的是一个promise
    • 如果有多个then链式调用,只有在上一个then执行后确定了状态才会执行下一个then中回调函数,即上一个then的回调函数执行后,下一个then的回调函数才会进入微队列
  6. promise异常穿透?
    • 当使用promise的then链式调用时,可以在最后指定失败的回调,可以用then或catch,用catch只要传一个参数
    • 前面任何操作出了异常,都会传到最后失败的回调中处理
  7. 中断promise链?
    • 在then的链式调用中,如果要中断链式调用,就必须返回一个pending状态的promise
    • return new Promise(() => {})
    • 此时状态没有改变,所以后续promise链不会再继续执行

# Promise自定义封装

# 封装promise构造函数

//声明构造函数
function Promise(executor) {
  //添加属性
  this.PromiseState = 'pending';
  this.PromiseResult = null;
  //保存多个回调
  this.callbacks = [];
  //保存实例对象的this值
  const self = this;
  
  //resolve 函数
  function resolve(data) {
    //保证状态只能更改一次
    if(self.PromiseState !== 'pending') return;
    
    //这里的this指向window
    //1. 修改对象的状态 (PromiseState)
    self.PromiseState = 'fulfilled';    
    //2. 修改对象结果值(PromiseResult)
    self.PromiseResult = data;
    
    //调用成功的回调函数
    setTimeout(() => {
      self.callbacks.forEach(item => {
        item.onResolved(data);
      });
    })
  }
  
  //reject 函数
  function reject(data) {
    //保证状态只能更改一次
    if(self.PromiseState !== 'pending') return;
    
    //1. 修改对象的状态 (PromiseState)
    self.PromiseState = 'rejected';    
    //2. 修改对象结果值(PromiseResult)
    self.PromiseResult = data;
    
    //调用失败的回调函数
    setTimeout(() => {
      self.callbacks.forEach(item => {
        item.onRejected(data);
      });
    })
  }
  
  try {
    //同步调用【执行器函数】
  	executor(resolve, reject);
  }catch(e) {
    //修改 promise 对象状态为失败
    reject(e);
  }
  
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56

# 封装then和catch方法

//添加then方法
Promise.prototype.then = function(onResolved, onRejected) {
  const self = this;
  //判读失败的回调函数有没有传,因为默认用户可以不传这个参数
  if(typeof onRejected !== 'function') {
    //添加默认值,是异常穿透的原理
    onRejected = reason => {
      throw reason;
    }
  }
  //如果成功的回调函数没有传
  if(typeof onResolved !== 'function') {
    onResolved = value => value;
  }
  
  //返回promise
  return new Promise((resolve, reject) => {
    //封装函数
    function callback(type) {
      //获取回调函数的执行结果(链式调用需要)
      let result = type(self.PromiseResult);
      //判断result类型
      if(result instanceof Promise) {
        result.then(v => {
          resolve(v);
        }, r => {
          reject(r);
        })
      }else{
        //如果返回的不是promise类型,就将状态改为成功
        resolve(result);
      }
    }
    
    //如果promise中的是同步操作
    if(this.PromiseState === 'fulfilled') {
      setTimeout(() => {
      	callback(onResolved);
      })
    }
    if(this.PromiseState === 'rejected') {
      setTimeout(() => {
      	callback(onRejected);
      })
    }

    //如果promise中的是异步操作
    //判断pending状态
    /* 为什么要判断 pending 状态?
      如果promise中是异步操作,那么异步操作会进入等待队列,接着执行下面的then方法
      promise的状态改变是在异步操作完成后执行的,也就是说执行到then方法时,状态还没发生改变,还是pending状态
      这时候状态没有改变,但后面会变成什么状态是不确定的,所以要将回调函数保存起来,等状态发生改变时再调用
    */
    if(this.PromiseState === 'pending') {
      this.callbacks.push({
        onResolved: () => {callback(onResolved);},
        onRejected: () => {callback(onRejected);}
      });
    }
  })
}

//添加catch方法
Promise.prototype.catch = function(onRejected) {
  return this.then(undefined, onRejected);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66

# promise函数对象方法封装

//添加resolve方法
Promise.resolve = function(value) {
  //返回promise对象
  return new Promise((resolve, reject) => {
    if(value instanceof Promise) {
        value.then(v => {
          resolve(v);
        }, r => {
          reject(r);
        })
      }else{
        resolve(value);
      }
  });
}

//添加reject方法
Promise.reject = function(reason) {
  return new Promise((resolve, reject) => {
    reject(reason);
  });
}

//添加all方法
Promise.all = function(promises) {
  //返回结果为promise对象
  return new Promise((resolve, reject) => {
    let count = 0let arr = [];
    for(let i=0; i<promises.length; i++) {
      promises[i].then(v => {
        count++;
        arr[i] = v;
        if(count === promises.length){
          resolve(arr);
        }
      }, r => {
        reject(r);
      });
    }
  })
}

//添加race方法
Promise.race = function(promises) {
  return new Promise((resolve, reject) => {
    for(let i=0; i<promises.length; i++) {
      promises[i].then(v => {
        //这里直接调用就可以,因为状态只可以改变一次,所以第一次改变的那个就是最后的结果
        resolve(v);
      }, r => {
        reject(r);
      });
    }
  })
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56

# class版的promise封装

class Promise{
  constructor(executor) {...}
  
  //属于实例对象的方法
  then(onResolved, onRejected) {...}
  catch(onRejected) {...}
  
  //属于类的静态方法
  static resolve(value) {...}
  static reject(reason) {...}
  static all(promises) {...}
  static race(promises) {...}
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# async 与 await

# async函数

  • 函数返回值为promise对象,成功或失败规则与Promise.resolve一致
  • promise对象的结果由async函数执行的返回值决定
async function main() {...}
1

# await表达式

  • await右侧的表达式一般为promise对象,但也可以为其他值
  • 如果表达式是promise对象, await返回的是promise成功的值
  • 如果表达式是其它值,直接将此值作为await的返回值

注:

  1. await必须写在async函数中,但async函数中可以没有await
  2. 如果await的promise失败了,就会抛出异常,需要通过try...catch捕获处理
async function main(){
  let p = new Promise((resolve,reject) => {
    resolve("ok");
  });
  let res = await p;
  console.log(res);// ok
  
  let res1 = await 20;
  console.log(res1);// 20
}
1
2
3
4
5
6
7
8
9
10

# 小栗子

function sendAjax(url) {
  return new Promise((resolve, reject) => {
    //发送ajax请求
    ...
  })
}
btn.addEventListener('click', async function() {
   let data = await sendAjax('https://...');
   
})
1
2
3
4
5
6
7
8
9
10