【ES6基础知识】promise和await/async

时间:2020-9-5 作者:admin

写此篇文章的理由:

  1. 异步是高频应用场景,应该是无处不在
  2. 平常积累的知识比较零散,没有形成整套体系,所以专题化并付诸实践,掌握模棱两可的知识点
  3. 建立起思维、身体、情绪之间关系
  4. 每天充充电,生活质量才改变

看此文的收货:

  1. 懂得Promise和await/async的不容易注意到的知识点
  2. 经常用到的使用场景

进阶请看我的另一篇promise和await async总结

我觉得这2篇文章知识点有点重复,因为边总结边思考肯定有交集,所以我觉得文章质量:没有最好,只有更好。

有新的知识点后面继续补上,也欢迎指正。

此处说下心中一点小心思:输出高吸引力的文章,希望利人也利己,希望大家给个👍,不花钱哦!

一、Promise

写正文前,拿一个外网可以请求到的接口为例

https://api.github.com/users/github

function get(url, params, config = {}) {    
  return new Promise((resolve, reject) => {
    axios.get(url, {
      params,
      ...config,
    }).then(res => {
      resolve(res.data);
    }).catch(err => {
      reject(err.data);
    })    
  });
}

后文请求得到的值用  【"真实请求到的值"】代替

1. 基本用法

1.1 p1的状态作为p2

const p1 = new Promise(function (resolve, reject) {
  setTimeout(() => reject(new Error('fail')), 3000)
})

const p2 = new Promise(function (resolve, reject) {
  setTimeout(() => resolve(p1), 1000)
})

p2
  .then(result => console.log(result))
  .catch(error => console.log(error))
// Error: fail

p1是一个 Promise,3 秒之后变为rejected。p2的状态在 1 秒之后改变,resolve方法返回的是p1。由于p2返回的是另一个 Promise,导致p2自己的状态无效了,由p1的状态决定p2的状态。所以,后面的then语句都变成针对后者(p1)。又过了 2 秒,p1变为rejected,导致触发catch方法指定的回调函数。

1.2 resolve后面的函数还会执行

new Promise((resolve, reject) => {
  resolve(1);
  console.log(2);
}).then(r => {
  console.log(r);
});
// 2
// 1

1.3 resolve加上return,后面的不会再执行

new Promise((resolve, reject) => {
  return resolve(1);
  // 后面的语句不会执行
  console.log(2);
})

一般来说,调用resolve或reject以后,Promise 的使命就完成了,后继操作应该放到then方法里面,而不应该直接写在resolve或reject的后面。所以,最好在它们前面加上return语句,这样就不会有意外。

then方法返回的是一个新的Promise实例,因此可以采用链式方法。
第一个回调函数完成以后,会将返回结果作为参数,传入第二个回调函数。

返回promise得到的值

get('https://api.github.com/users/github').then(res => {
  console.log('first:',res) //"first:真实请求到的值"
  return get('https://api.github.com/users/github');
}).then((res)=>{
  console.log('second:',res) //"second:真实请求到的值"
})

直接返回一个值

//"first:真实请求到的值"

get('https://api.github.com/users/github').then(res => {
  console.log('first:',res) //"first:真实请求到的值"
  return 123;
}).then((res)=>{
  console.log('second:',res) //"second:123"
})

2. Promise.prototype.then()

then方法是定义在原型对象Promise.prototype上的。作用是为 Promise 实例添加状态改变时的回调函数。

3. Promise.prototype.catch()

3.1 Promise.prototype.catch()方法是.then(null, rejection)或.then(undefined, rejection)的别名,用于指定发生错误时的回调函数。

get('https://api.github.com/users/github').then(res => {
  throw new Error('error');
}).catch((res)=>{
  console.log('second:',res)  //second: Error: error
})

get('https://api.github.com/users/github').then(res => {
  throw new Error('error');
}).then(null,err => {
  console.log('err:',err)  //err: Error: error

})

以下两种写法是等价的

// 写法一
const promise = new Promise(function(resolve, reject) {
  try {
    throw new Error('test');  // throw方式
  } catch(e) {
    reject(e);
  }
});
promise.catch(function(error) {
  console.log(error);
});

// 写法二
const promise = new Promise(function(resolve, reject) {
  reject(new Error('test'));   // reject方式
});
promise.catch(function(error) {
  console.log(error);
});

3.2 如果 Promise 状态已经变成resolved,再抛出错误是无效的

const promise = new Promise(function(resolve, reject) {
  resolve('ok');
  throw new Error('test');  //无效的
});
promise
  .then(function(value) { console.log(value) })
  .catch(function(error) { console.log(error) });
// ok

3.3 Promise 对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止。也就是说,错误总是会被下一个catch语句捕获

get('https://api.github.com/users/github').then(res => {
  console.log('first:',res) //"first:真实请求到的值"
  return get('https://api.github.com/users/github');
}).then((res)=>{
  console.log('second:',res) //"second:真实请求到的值"
})

上面代码中,一共有三个 Promise 对象:一个由get()产生,两个由then()产生。它们之中任何一个抛出的错误,都会被最后一个catch()捕获。

3.4 最好不要在then()方法里面定义 Reject 状态的回调函数,总是使用catch方法。

promise
  .then(function(data) {
    // success
  }, function(err) {
    // error
  });

// good
promise
  .then(function(data) { //cb
    // success
  })
  .catch(function(err) {
    // error
  });

理由是:第二种写法可以捕获前面then方法执行中的错误,也更接近同步的写法(try/catch)

3.5 Promise 内部的错误不会影响到 Promise 外部的代码的执行官

通俗的说法就是“Promise 会吃掉错误”

const someAsyncThing = function() {
  return new Promise(function(resolve, reject) {
    // 下面一行会报错,因为x没有声明
    resolve(x + 2);  //运行到此行抛出错误,但是不会退出进程、终止脚本执行
  });
};

someAsyncThing().then(function() {
  console.log('everything is great');
});

setTimeout(() => { console.log(123) }, 2000);  //依旧会执行
// Uncaught (in promise) ReferenceError: x is not defined
// 123

通过上面例子,所以用catch比较好

跟传统的try/catch代码块不同的是,如果没有使用catch()方法指定错误处理的回调函数,Promise 对象抛出的错误不会传递到外层代码,即不会有任何反应。

3.6 Promise 在下一轮“事件循环”再抛出错误,能捕获

const promise = new Promise(function (resolve, reject) {
  resolve('ok');
  setTimeout(function () { throw new Error('test') }, 0)  
  //在下一轮“事件循环”再抛出错误,会执行,错误会冒泡到外层,能捕获
});
promise.then(function (value) { console.log(value) });
// ok
// Uncaught Error: test

3.7 抛出错误在同一轮“事件循环”

页面出错,没有catch,没有输出错误信息(也就是没有捕获),因为resolved,再抛出错误是无效的

const promise = new Promise(function (resolve, reject) {
  resolve('ok');
  throw new Error('test') 
});
promise.then(function (value) { console.log(value) });
//ok

这样也没有捕获

const promise = new Promise(function (resolve, reject) {
  resolve('ok');
  throw new Error('test') 
});
promise
.then( (value)=> { console.log(value) })
.catch(e => {console.log('err:',e)});
//ok

如果没有resolve能够捕获

const promise = new Promise(function (resolve, reject) {
  throw new Error('test') 
});
promise
.then( (value)=> { console.log(value) })
.catch(e => {console.log('err:',e)});
//err: Error: test

3.8 一般总是建议,Promise 对象后面要跟catch()方法,可处理 Promise 内部发生的错误

catch()方法返回的还是一个 Promise 对象,因此后面还可以接着调用then()方法

const someAsyncThing = function() {
  return new Promise(function(resolve, reject) {
    // 下面一行会报错,因为x没有声明
    resolve(x + 2);
  });
};

someAsyncThing()
.catch(function(error) {
  console.log('oh no', error);
})
.then(function() {
  console.log('carry on');
});
// oh no [ReferenceError: x is not defined]
// carry on

没错误跳过catch,直接执行后面的then()方法。此时,如果是then()方法里面报错,就与前面的catch()无关了

Promise.resolve()
.catch(function(error) {
  console.log('oh no', error);
})
.then(function() {
  console.log('carry on');
});
// carry on

3.9 catch还能再抛出错误

const someAsyncThing = function() {
  return new Promise(function(resolve, reject) {
    // 下面一行会报错,因为x没有声明
    resolve(x + 2);
  });
};

someAsyncThing().then(function() {
  return someOtherAsyncThing();
}).catch(function(error) {
  console.log('oh no', error);
  // 下面一行会报错,因为 y 没有声明
  y + 2;
}).catch(function(error) {
  console.log('carry on', error);
});
// oh no [ReferenceError: x is not defined]
// carry on [ReferenceError: y is not defined]

第二个catch()方法是用来捕获前一个catch()方法抛出的错误

4. Promise.prototype.finally()

不管promise最后的状态,在执行完then或catch指定的回调函数以后,都会执行finally方法指定的回调函数

// resolve 的值是 undefined
Promise.resolve(2).then(() => {}, () => {})

// resolve 的值是 2
Promise.resolve(2).finally(() => {})

// reject 的值是 undefined
Promise.reject(3).then(() => {}, () => {})

// reject 的值是 3
Promise.reject(3).finally(() => {})

5. Promise.all()

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

5.1 参数可以不是数组,但必须具有 Iterator 接口,且返回的每个成员都是 Promise 实例

5.2 参数中的所有状态都变成fulfilled,p就变成fulfilled,返回值组成数组

5.3 参数中的有一个被rejected,p就变成rejected

5.4 参数如果不是promise实例,会先调用Promise.resolve方法,将参数转为 Promise 实例

5.5 后面最好也跟着catch

5.6 如果作为参数的 Promise 实例,自己定义了catch方法,那么它一旦被rejected,并不会触发Promise.all()的catch方法。

const p1 = new Promise((resolve, reject) => {
  resolve('hello');
})
.then(result => result)
.catch(e => e);

const p2 = new Promise((resolve, reject) => {
  throw new Error('报错了');
})
.then(result => result)
.catch(e => e);

Promise.all([p1, p2])
.then(result => console.log(result))
.catch(e => console.log(e));
// ["hello", Error: 报错了]

只会报第一个错误

const p1 = new Promise((resolve, reject) => {
  throw new Error('报错了1');
})
.then(result => result);

const p2 = new Promise((resolve, reject) => {
  throw new Error('报错了2');
})
.then(result => result);

Promise.all([p1, p2])
.then(result => console.log(result))
.catch(e => console.log(e));
//Error: 报错了1

6. Promise.race()

const p = Promise.race([p1, p2, p3]);

6.1 参数规则同Promise.all()

6.2 参数之中只要有一个实例率先改变状态,p的状态就跟着改变

6.3 应用场景:指定时间内没获取到结果,就报错

const p = Promise.race([
  get('https://api.github.com/users/github'),
  new Promise(function (resolve, reject) {
    setTimeout(() => reject(new Error('request timeout')), 5000)
  })
]);

p
.then(console.log)
.catch(console.error)
//5m内没得到结果,就报错

7. Promise.allSettled()

7.1 只有等到所有参数实例都返回结果,不管是fulfilled还是rejected,包装实例才会结束。

7.2 结果是数组,元素是对象,具体如下代码

const resolved = Promise.resolve(42);
const rejected = Promise.reject(-1);

const allSettledPromise = Promise.allSettled([resolved, rejected]);

allSettledPromise.then(function (results) {
  console.log(results);
});
// [
//    { status: 'fulfilled', value: 42 },
//    { status: 'rejected', reason: -1 }
// ],找出

可以通过status

7.3 应用场景:不关心异步操作的结果,只关心这些操作有没有结束

8. Promise.any()

8.1 只要参数实例有一个变成fulfilled状态,包装实例就会变成fulfilled状态

8.2 如果所有参数实例都变成rejected状态,包装实例就会变成rejected状态

8.3 跟Promise.race()方法很像,只有一点不同,就是不会因为某个 Promise 变成rejected状态而结束

捕捉错误可以用

9. Promise.resolve()

9.1 可以将现有对象转为 Promise 对象

Promise.resolve('foo')
// 等价于
new Promise(resolve => resolve('foo'))

9.2 参数分为四种情况

(1) 参数是一个 Promise 实例

Promise.resolve将不做任何修改、原封不动地返回这个实例。

(2) 参数是一个thenable对象

let thenable = {
  then: function(resolve, reject) {
    resolve(42);
  }
};

let p1 = Promise.resolve(thenable);
p1.then(function(value) {
  console.log(value);  // 42
});

thenable对象的then方法执行后,对象p1的状态就变为resolved,从而立即执行最后那个then方法指定的回调函数,输出 42

(3) 参数不是具有then方法的对象,或根本就不是对象

返回一个新的 Promise 对象,状态为resolved。

const p = Promise.resolve('Hello');

p.then(function (s){
  console.log(s)
});
// Hello

(4) 不带有任何参数

const p = Promise.resolve();

p.then(function () {
  // ...
});

10. Promise.reject()

10.1 返回一个新的 Promise 实例,该实例的状态为rejected

const p = Promise.reject('出错了');
// 等同于
const p = new Promise((resolve, reject) => reject('出错了'))

p.then(null, function (s) {
  console.log(s)
});
// 出错了

10.2 传啥参数,啥参数作为reject的理由

注意:与Promise.resolve方法不一致的是:Promise.reject()方法的参数,会原封不动地作为reject的理由,变成后续方法的参数。

const thenable = {
  then(resolve, reject) {
    reject('出错了');
  }
};

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

Promise.reject方法的参数是一个thenable对象,执行以后,后面catch方法的参数不是reject抛出的“出错了”这个字符串,而是thenable对象。

11. 实例

  • 图片加载
function loadImageAsync(url) {
  return new Promise(function(resolve, reject) {
    const image = new Image();

    image.onload = function() {
      resolve(image);
    };

    image.onerror = function() {
      reject(new Error('Could not load image at ' + url));
    };

    image.src = url;
  });
}
  • 异步请求
function get(url, params, config = {}) {    
  return new Promise((resolve, reject) => {
    axios.get(url, {
      params,
      ...config,
    }).then(res => {
      resolve(res.data);
    }).catch(err => {
      reject(err.data);
    })    
  });
}
  • 封装有正反按钮的组件,比如messageBox
    确定:将promise断定为resolve状态
    取消:将promise断定为reject状态
buttonAction() {
   this.resolve();
}

二、await/async

1. 特点

  • 内置执行器
  • async函数返回的是 Promise 对象,Promise可以作为await命令的参数
  • 比起星号和yield,更好的语义
  • await命令后面,可以是 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时会自动转成立即 resolved 的 Promise 对象)
  • await必须写在async函数中
  • async函数内部return语句返回的值,会成为then方法回调函数的参数。
async function f() {
  return 'hello world';
}

f().then(v => console.log(v))
// "hello world"
  • 任何一个await语句后面的 Promise 对象变为reject状态,那么整个async函数都会中断执行。
async function f() {
  await Promise.reject('出错了');
  await Promise.resolve('hello world'); // 不会执行
}

怎么能不中断呢?

方法一:把第一个await放在try…catch结构里面

async function f() {
  try {  
    await Promise.reject('出错了'); 
  } catch(e) {
  }
  return await Promise.resolve('hello world');
}

f()
.then(v => console.log(v))
// hello world

方法二:await后面的 Promise 对象再跟一个catch方法,这样可以处理前面可能出现的错误

async function f() {
  await Promise.reject('出错了')
    .catch(e => console.log(e));
  return await Promise.resolve('hello world');
}

f()
.then(v => console.log(v))
// 出错了
// hello world

2. 函数写法

// 函数声明
async function foo() {}

// 函数表达式
const foo = async function () {};

// 对象的方法
let obj = { async foo() {} };
obj.foo().then(...)

// Class 的方法
class Storage {
  constructor() {
    this.cachePromise = caches.open('avatars');
  }

  async getAvatar(name) {
    const cache1 = await this.cachePromise;
    return name; //返回的值作为then中的参数
  }
}

const storage = new Storage();
storage.getAvatar('jake').then(…);

// 箭头函数
const foo = async () => {};

3. await后面的参数

(1)如果不是 Promise 对象,就直接返回对应的值

async function f() {
  // 等同于
  // return 123;
  return await 123;
}

f().then(v => console.log(v))
// 123

(2)await命令后面是一个thenable对象,会将其等同于 Promise 对象

class Sleep {
  constructor(timeout) {
    this.timeout = timeout;
  }
  then(resolve, reject) {  //有then对象
    const startTime = Date.now();
    setTimeout(
      () => resolve(Date.now() - startTime),
      this.timeout
    );
  }
}

(async () => {
  const sleepTime = await new Sleep(1000);//直接视为new Promise处理
  console.log(sleepTime);
})();
// 1000

4. 错误处理机制

4.1 async函数内部抛出错误,会导致返回的 Promise 对象变为reject状态。

async function f() {
  throw new Error('出错了');
  //await new Promise(function (resolve, reject) {
  //  throw new Error('出错了');
  //});
}

f().then(
  v => console.log(v),
  e => console.log(e)
)
// Error: 出错了

4.2 防止出错,使用try catch,这样也不会阻止下面运行

async function f() {
  try {
    await new Promise(function (resolve, reject) {
      throw new Error('出错了');
    });
  } catch(e) {
  }
  return await('hello world');
}

4.3 如果有多个await命令,可以统一放在一个try…catch结构中

为什么会有await/async
await和promise的区别

catch捕捉错误

结合for…of一起使用

注意:能并发执行的不要继发运行,所以这也是它的缺点

Promise的缺点:
解决回调地狱的问题,

文章不在于快速输出,而在于打磨。

5. 注意点

5.1 最好把await命令放在try…catch代码块中或者await后面跟着catch

async function myFunction() {
  try {
    await somethingThatReturnsAPromise();
  } catch (err) {
    console.log(err);
  }
}

// 另一种写法

async function myFunction() {
  await somethingThatReturnsAPromise()
  .catch(function (err) {
    console.log(err);
  });
}

5.2 多个await命令后面的异步操作,如果不存在继发关系,最好让它们同时触发,也就是可以不用await

5.3 await命令只能用在async函数之中,如果用在普通函数,就会报错

继发运行,可以用for…of

async function dbFuc(db) {
  let docs = [{}, {}, {}];

  for (let doc of docs) {
    await db.post(doc);
  }
}

最好不要用forEach,因为会并发

function dbFuc(db) { //这里不需要 async
  let docs = [{}, {}, {}];

  // 用forE爱吃,此写法是并发
  docs.forEach(async function (doc) {
    await db.post(doc); //await只能写在async函数之中
  });
}

继发运行的另一种写法,可以用reduce

async function dbFuc(db) {
  let docs = [{}, {}, {}];

  await docs.reduce(async (_, doc) => {
    await _;
    await db.post(doc);
  }, undefined);
}

如果确实希望多个请求并发执行,可以使用Promise.all方法

async function dbFuc(db) {
  let docs = [{}, {}, {}];
  let promises = docs.map((doc) => db.post(doc));

  let results = await Promise.all(promises);
  console.log(results);
}

// 或者使用下面的写法

async function dbFuc(db) {
  let docs = [{}, {}, {}];
  let promises = docs.map((doc) => db.post(doc));

  let results = [];
  for (let promise of promises) {
    results.push(await promise);
  }
  console.log(results);
}

5.4 async 函数可以保留运行堆栈

const a = async () => {
  await b();
  c();
};
b()运行的时候,a()是暂停执行,上下文环境都保存着。
一旦b()或c()报错,错误堆栈将包括a()。

const a = () => {
  b().then(() => c());
};
函数a内部运行了一个异步任务b()。
当b()运行的时候,函数a()不会中断,而是继续执行。
等到b()运行结束,可能a()早就运行结束了,b()所在的上下文环境已经消失了。
如果b()或c()报错,错误堆栈将不包括a()。

6. async 函数的实现原理

async 函数的实现原理,就是将 Generator 函数和自动执行器,包装在一个函数里。

async function fn(args) {
  // ...
}

// 等同于

function fn(args) {
  return spawn(function* () { //Generator 函数
    // ...
  });
}
//自动执行器
function spawn(genF) {
  return new Promise(function(resolve, reject) {
    const gen = genF();
    function step(nextF) {
      let next;
      try {
        next = nextF();
      } catch(e) {
        return reject(e);
      }
      if(next.done) {
        return resolve(next.value);
      }
      Promise.resolve(next.value).then(function(v) {
        step(function() { return gen.next(v); });
      }, function(e) {
        step(function() { return gen.throw(e); });
      });
    }
    step(function() { return gen.next(undefined); });
  });
}

7. 与其他异步处理方法的比较

假定某个 DOM 元素上面,部署了一系列的动画,前一个动画结束,才能开始后一个。如果当中有一个动画出错,就不再往下执行,返回上一个成功执行的动画的返回值。

Promise

function chainAnimationsPromise(elem, animations) {

  // 变量ret用来保存上一个动画的返回值
  let ret = null;

  // 新建一个空的Promise
  let p = Promise.resolve();

  // 使用then方法,添加所有动画
  for(let anim of animations) {
    p = p.then(function(val) {
      ret = val;
      return anim(elem); //返回的也是promise对象,注意这种写法思路
    });
  }

  // 返回一个部署了错误捕捉机制的Promise
  return p.catch(function(e) {
    /* 忽略错误,继续执行 */
  }).then(function() {
    return ret;
  });

}

Generator

function chainAnimationsGenerator(elem, animations) {

  return spawn(function*() {
    let ret = null;
    try {
      for(let anim of animations) {
        ret = yield anim(elem);
      }
    } catch(e) {
      /* 忽略错误,继续执行 */
    }
    return ret;
  });

}
写法复杂

async

async function chainAnimationsAsync(elem, animations) {
  let ret = null;
  //async最好加上try catch
  try {
    for(let anim of animations) {
      ret = await anim(elem);
    }
  } catch(e) {
    /* 忽略错误,继续执行 */
  }
  return ret;
}

async 函数的实现最简洁,最符合语义,几乎没有语义不相关的代码。它将 Generator 写法中的自动执行器,改在语言层面提供,不暴露给用户,因此代码量最少。

8. 实例

  • 按顺序完成异步操作

比如,依次远程读取一组 URL,然后按照读取的顺序输出结果

Promise联合reduce方式,继发发出远程请求,结果是按照顺序输出

function logInOrder(urls) {
  // 远程读取所有URL
  const textPromises = urls.map(url => {
    return fetch(url).then(response => response.text());
  });

  // 按次序输出
  textPromises.reduce((chain, textPromise) => {
    return chain.then(() => textPromise)
      .then(text => console.log(text));
  }, Promise.resolve());
}

await联合for…of方式,继发发出远程请求,结果是按照顺序输出

async function logInOrder(urls) {
  for (const url of urls) {
    const response = await fetch(url);
    console.log(await response.text());
  }
}

并发发出远程请求,结果是按照顺序输出

async function logInOrder(urls) {
  // 并发读取远程URL
  const textPromises = urls.map(async url => {
    const response = await fetch(url);
    return response.text();
  });

  // 按次序输出
  for (const textPromise of textPromises) {
    console.log(await textPromise);
  }
}

扩展阅读:

es6.ruanyifeng.com/#docs/promi…

es6.ruanyifeng.com/#docs/async…

声明:本文内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎进行举报,并提供相关证据,工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。