三、ECMAScript 6 语法简介(5)

本章概要

  • Promise
    • 基本用法
    • 创建已处理的 Promise
    • 响应多个 Promise
  • async 函数
    • 基本用法
    • await 和并行任务执行
    • 使用 async 函数重写 Promise 链
    • 错误处理

3.12 Promise

JavaScript 引擎是基于单线程时间循环的概念构建的,他采用任务队列的方式,将要执行的代码块放到队列中,当 JavaScript 引擎中的一段代码执行结束,时间循环会指定队列中的下一个任务来执行。
事件循环是 JavaScript 引擎中的一段代码,负责监控代码执行并管理任务队列。
JavaScript 执行异步调用的传统方式是事件和回调函数,随着应用的复杂化,事件和回调函数无法完全满足开发者要实现的需求,为此,ES6 给出了 Promise 这一更加强大的异步编程方案。

3.12.1 基本用法

一个 Promise 可以通过 Promise 构造函数创建,这个构造函数只接受一个参数:包含初始化 Promise 代码的执行器(executor)函数,在该函数内包含需要异步执行的代码。
执行器函数接收两个参数,分别是 resolve() 函数 和 reject() 函数,这两个函数有 JavaScript 引擎提供,不需要用户自己编写。异步操作结束成功时调用 resolve() 函数,失败时调用 reject() 函数。如下:

const promise = new Promise(function(resolve, reject) {
	// 开启异步操作
  setTimeout(function(){
  	try{
  		let c = 6 / 2 ;
  		// 执行成功调用resolve函数
  		resolve(c);
  	}catch(ex){
  		// 执行失败调用reject函数
  		reject(ex);
  	}
  }, 1000);
});

在执行器函数内包含了异步调用,在 1s 后执行两个数的除法运算,如果成功,则用相除的结果作为参数调用 resolve() 函数,失败则调用 reject() 函数。
每个 Promise 都会经历一个短暂的生命周期:先是处于进行中(pending)的状态,此时操作尚未完成,所以它也是未处理的(unsettled),,一旦异步操作执行结束,Promise 则变为已处理的 (settled)状态。操作结束后,根据异步操作执行成功与否,可以进行以下两个状态之一:
(1)fulfilled:Promise 异步操作成功完成
(2)rejected:由于程序错误或其它原因,Promise 异步操作未能成功完成,即已失败。
一旦 Promise 状态改变,就不会再变,任何时候都可以得到这个结果。Promise 对象的状态改变只有两种可能:从 pending 变为 fulfilled ,或者从 pending 变为 rejected。
在 Promise 状态改变后,怎么根据不同状态做相应的处理呢?
Promise 对象有一个 then() 方法,它接受两个参数:第一个是当 Promise 的状态变为 fulfilled 时要调用的函数,与异步操作相关的附加数据通过调用 resolve() 函数传递给这个完成函数;第二个是当 Promise 的状态变为 rejected 时要调用的函数,所有与失败相关的附加数据通过调用 reject() 函数传递给这个拒绝函数。
如下:

const promise = new Promise(function(resolve, reject) {
	// 开启异步操作
  setTimeout(function(){
  	try{
  		let c = 6 / 2 ;
  		// 执行成功调用resolve函数
  		resolve(c);
  	}catch(ex){
  		// 执行失败调用reject函数
  		reject(ex);
  	}
  }, 1000);
});
promise.then(function(value){
	// 完成
	console.log(value);  // 3
},function(err){
	// 拒绝
	console.error(erro.message);
})

then 方法的两个参数都是可选的。例如,只在执行失败后进行处理,可以给 then() 方法的第一个参数传递 null,如下:

promise.then(null,function(err){
	// 拒绝
	console.error(err.message);
})

Promise 对象还有一个 catch() 方法,用于在执行失败后进行处理,等价于上述只给 then() 方法传入拒绝处理函数的代码。如下:

prommise.catch(function (err){
	console.error(err.message);
})

通常是将 then() 方法和 catch() 方法一起使用来对异步操作的结果进行处理,这样能更清楚地指明操作结果是成功还是失败。如下:

promise.then(function(value){
	// 完成
	console.log(value);  // 3
}).catch(function (err){
	// 拒绝
	console.error(err.message);
});

上述代码使用箭头函数会更加简洁。如下:

promise.then(value => console.log(value)).catch(err => console.error(err.message));

提示:如果调用 resolve() 函数或 reject() 函数带有参数,那么他们的参数会被传递给 then() 或 catch() 方法的回调函数。
Promise 支持方法链的调用形式,如上述代码所示。每次调用 then() 或 catch() 方法时实际上会创建并返回另一个 Promise ,因此可以将 Promise 串联调用。在串联调用时,只有在前一个 Promise 完成或被拒绝时,第二个才会被调用。如下:

const promise = new Promise((resolve, reject) => {
	// 调用setTimeout模拟异步操作
	setTimeout( ()=> {
		let intArray = new Array(20);
		for(let i=0; i<20; i++){
			intArray[i] = parseInt(Math.random() * 20, 10);
		}
		// 成功后调用resolve
		resolve(intArray);
	},1000);
	// 该代码会立即执行
	console.log("开始生成一个随机数的数组")
});
	
promise.then(value => {
	value.sort((a,b) => a-b);
	return value;
	}).then(value => console.log(value));

代码解释:

  • Promise 的执行函数内的代码会立即执行,因此无论 setTimeout(0 指定的回调函数执行成功与否,console.log() 语句都会执行。
  • 在 20 个随机数生成完毕后,调用 resolve(intArray) 函数,因此 then() 方法的完成处理函数被调用,对数组进行排序,之后返回 value ;接着下一个 then() 方法的完成处理函数开始调用,输出排序后的数组。
  • Promise 链式调用时,有一个重要特性就是可以为后续的 Promise 传递数据,只需要在完成处理函数中指定一个返回值(如上述代码中的 return value),就可以沿着 Promise 链继续传递数据。

上述代码在 Node.js 中的输出结果为:

开始生成一个随机数的数组
[0, 1, 2, 3, 4, 4, 5, 5, 7, 8, 9, 10, 11, 12, 13, 13, 14, 17, 19, 19]

在完成处理程序或拒绝处理程序中也可能会产生错误,使用 Promise 链式调用可以很好地捕获这些错误。如下:

const promise = new Promise((resolve, reject)=>{
	resolve("Hello Word");
});

promise.then((value) => {
	console.log(value);
	throw new Error("错误");
}).catch(err => console.error(err.message));

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

3.12.2 创建已处理的 Promise

如果要将一个现有的对象转换为 Promise 对象,可以调用 Promise.resolve() 方法,该方法接受一个参数并返回一个完成状态的 Promise ,之后在返回的 Promise 对象上调用 then() 方法来获取值。如下:

const promise = Promise.resolve("Hello Vue.js");
// 等价于
promise.then(value => console.log(value));  // Hello Vue.js

Promise.resolve() 方法的参数分为以下3种情况:
(1)如果参数是一个 Promise 实例,那么将直接返回这个 Promise ,不做任何改动
(2)如果参数是一个 thenable 对象(即具有 then() 方法的对象),那么会创建一个新的 Promise 对象,并立即执行 thenable 对象的 then() 方法,返回的 Promise 对象的最终状态由 then() 方法的执行决定。如下:

const thenable = {
    then(resolve, reject){
        resolve("Hello");
    }
}

const promise = Promise.resolve(thenable);  // 会执行thenable对象的then()方法
promise.then(value => console.log(value));  //Hello

(3)如果参数为空,或者是基本数据类型,或者是不带 then() 方法的对象,那么返回 Promise 对象状态为 fulfilled,并且将参数值传递给对应的 then() 方法。
通常来说,如果不知道一个值是否为 Promise 对象,使用 Promise.resolve(value) 方法返回一个 Promise 对象,这样就能将该 value 以 Promise 对象形式使用。
Promise.reject(reason) 方法也会返回一个新的 Promise 对象,并将给定的失败信息传递给相应的处理方法,返回的 Promise 对象的状态为 rejected 。如下:

const promise = Promise.reject('fail');
promise.catch(err => console.log(err));  // fail

3.12.3 响应多个 Promise

如果需要等待多个异步任务完成后,再执行下一步的操作,那么可以利用 Promise.all() 方法,该方法接受一个参数并返回一个新的 Promise 对象,参数是一个包含多个 Promise 的可迭代对象(如数组)。
返回的 Promise 对象在参数给出的所有 Promise 对象都成功的时候才会触发成功,一旦有任何一个 Promise 对象失败,则立即触发该 Promise 对象的失败。
这个新的 Promise 对象在触发成功状态后,会把所有 Promise 返回值的数组作为成功回调的返回值,顺序与可迭代对象中的 Promise 顺序保持一致;如果这个新的 Promise 对象出发了失败状态,它会把可迭代对象中第一个触发失败的 Promise 对象的错误信息作为它的失败错误信息。
Promise.all() 方法通常被用于处理多个 Promise 对象的状态集合。如下:

const promise1 = Promise.resolve(5);
const promise2 = 10;
const promise3 = new Promise((resolve, reject) => {
  setTimeout(resolve, 100, 'Hello');
});
const promise4 = new Promise((resolve, reject) => {
  throw new Error("错误");
})

Promise.all([promise1, promise2, promise3]).then(values => {
  console.log(values);
});

Promise.all([promise1, promise2, promise4])
.then(values => {
  console.log(values);
})
.catch(err => console.log(err.message));

如果 Promise.all() 方法的参数包含非 Promise 值,这些值将被忽略,但仍然会被放到返回的数组中(如果所有 Promise 都完成)。运行结果如下:

错误
[ 5, 10, 'Hello' ]

ES6 还提供了一个 Promise.race() 方法,同样也是传入多个 Promise 对象,但与 Promise.all() 方法的区别是,该方法是只要有任意一个 Promise 成功或失败,则返回的新的 Promise 对象就会用这个 Promise 对象的成功返回值或失败信息作为参数调用相应的回调函数。如下:

const promise1 = Promise.resolve(5);
const promise2 = new Promise((resolve, reject) => {
  resolve(10);
});
const promise3 = new Promise((resolve, reject) => {
  setTimeout(resolve, 100, 'Hello');
});

Promise.race([promise1, promise2, promise3]).then(value => {
  console.log(value); //5
});

3.13 async 函数

async 函数是在 ES2017 标准中引入的。
async 函数是使用 async 关键字声明的函数,async 函数是 AsyncFunction 构造函数的实例。在 async 函数内部可以使用 await 关键字,表示紧跟在后面的表达式需要等待结果。
async 和 await 关键字可以用一种更简洁的方式写出基于 Promise 的异步行为,而无需刻意地链式调用 Promise。

3.13.1 基本用法

async 函数会返回一个 Promise 对象,如果一个async 函数的返回值不是 Promise ,那么它会被隐式地包装到一个 Promise 中。如下:

async function helloAsync(){
    return "Hello";
}

// 等价于
//function helloAsync() {
//   return Promise.resolve("Hello");
//}

console.log(helloAsync())  // Promise { 'Hello' }
 
helloAsync().then(v=>{
   console.log(v);         // Hello
})

async 函数内部 return 语句返回的值,会成为 then() 方法回调函数的参数。
async 函数中可以有 await 表达式,async 函数执行时,如果遇到 await ,就会先暂停执行,等到触发的异步操作完成后,再恢复 async 函数的执行并返回解析值。如下:

function asyncOp(){
   return new Promise(resolve => {
       setTimeout(() => {
          console.log("延时任务");
          resolve();
       }, 1000);
   });
}
 
async function helloAsync(){
   await asyncOp();
   console.log("Hello");
 }
helloAsync();

如果 helloAsync() 函数执行时,在 await 处没有暂停,由于定时器设定的是在 1s 后才执行传入的匿名函数,那么在 Console 窗口应该先看到 Hello,然后才是“延时任务”。运行结果如下:

延时任务
Hello

async 函数的函数体可以看做是由 0 个或多个 await 表达式分割开来的,从第一行代码开始直到第一个 await 表达式(如果有的话)都是同步运行的。换句话说,一个不含 await 表达式的 async 函数是会同步运行的。然而,如果函数体内有一个 await 表达式,async 函数就一定会异步执行。
在 await 关键字后面,可以是 Promise 对象和原始类型的值。如果是原始类的值,会自动转成立即 resolve 的 Promise 对象。

3.13.2 await 和并行任务执行

在 async 函数中可以有很多个 await 任务,如果多个任务之间不要求顺序执行,那么可以在 await 后面节 Promise.all() 方法执行多个任务。如下:

const resolveAfter2Seconds = function() {
  console.log("starting slow promise");
  return new Promise(resolve => {
    setTimeout(function() {
      resolve("slow");
      console.log("slow promise is done");
    }, 2000);
  });
};

const resolveAfter1Second = function() {
  console.log("starting fast promise");
  return new Promise(resolve => {
    setTimeout(function() {
      resolve("fast");
      console.log("fast promise is done");
    }, 1000);
  });
};

const parallel = async function() {
  console.log("使用await Promise.all并行执行任务");
  // 并行启动两个任务,等待两个任务都完成
  await Promise.all([
      (async()=>console.log(await resolveAfter2Seconds()))(),
      (async()=>console.log(await resolveAfter1Second()))()
  ]);
}

parallel();

运行结果如下:

使用await Promise.all并行执行任务
starting slow promise
starting fast promise
fast promise is done
fast
slow promise is done
slow

3.13.3 使用 async 函数重写 Promise 链

返回 Promise 的 API 将产生一个 Promise 链,它将拆解成许多部分。部分代码如下:

function getProcessedData(url) {
  return downloadData(url)              // 返回一个 promise 对象
    .catch(e => {
      return downloadFallbackData(url)  // 返回一个 promise 对象
    })
    .then(v => {
      return processDataInWorker(v);    // 返回一个 promise 对象
    });
}

可以重写为单个 async 函数,如下:

async function getProcessedData(url) {
  let v;
  try {
    v = await downloadData(url); 
  } catch (e) {
    v = await downloadFallbackData(url);
  }
  return processDataInWorker(v);
}

3.13.4 错误处理

如果async 函数内部抛出错误,则会导致返回的 Promise 对象变为 reject 状态。抛出的错误对象会被 catch() 方法回调函数接收到。如下:

async function helloAsync() {
  await new Promise(function (resolve, reject) {
    throw new Error("错误");
  });
}

helloAsync().then(v => console.log(v))
            .catch(e => console.log(e.message));  // 错误

async 函数 helloAsync() 执行后,await 后面的 Promise 对象会抛出一个错误对象,导致 catch() 方法的回调函数被调用,他的参数就是抛出的错误对象。
防止出错的方法是将 await 放到 try/catch 语句中,如下:

async function helloAsync() {
  try {
    await new Promise(function (resolve, reject) {
      throw new Error("错误");
    });
  } catch(e) {
    // 错误处理
  }
  return await('hello');
}

如果有多个 await ,可以统一放到 try/catch 语句中。

你可能感兴趣的:(#,Vue.js,3.0,从入门到实战,ecmascript,javascript,Promise,async函数)