持续对红宝书进行输出(七)

112 阅读14分钟

1. 期约与异步函数

  1. 内容
    1. 异步编程
    2. 期约
    3. 异步函数

1.1 异步编程

  1. 在JavaScript是单线程事件循环模式中。异步行为是为了优化因计算量大而时间长的操作。
  2. 同步行为对应内存中顺序执行的处理器指令。每条指令都会严格按照它们出现的顺序来执行。在程序执行的每一步,都可以推断出程序的状态。
  3. 异步行为类似于系统中断,即当前进程外部的实体可以触发代码执行。
  4. 以往的异步编程模式
    1. 早期的JavaScript中,只支持定义回调函数来表明异步操作完成。串联多个异步操作函数,会造成回调地狱的问题。
    2. 异步返回值:给异步操作提供一个回调,这个回调中包含要使用异步返回值的代码
    3. 失败处理:异步操作失败处理在回调模型中也要考虑,因此也有成功回调和失败回调

// setTimeout的参数
/**
 *  setTimeout(function, milliseconds, param1, param2, ...)
 *      1. function       必需。要调用一个代码串,也可以是一个函数。
 *      2. milliseconds   可选。执行或调用function 需要等待的时间,以毫秒计。默认为 0
 *      3. params1,param2 可选。传给执行函数的其他参数(IE9 及其更早版本不支持该参数)。
 * 
 */

function double(val){
    setTimeout(() => {
        setTimeout(console.log,0,val*2 );
    }, 1000);
}
double(3) // 6

/**
 *  解析:setTimeout可以定义一个在指定时间之后会被调度执行的回调函数。
 *  1000毫秒之后,JavaScript运行时会把回调函数推到自己的消息队列上去等待执行。
 *  推到队列之后,回调什么时候出队列被执行对JavaScript代码就完全不可见了。
 *  还有一点,double()函数在setTimeout成功调度异步操作之后会立即退出。
 */
 // 异步操作的返回值 提供一个回调函数,使用异步返回值代码
function double2(val,callback){
    setTimeout(() => {
        callback(val * 3)
    }, 1000);
}

double2(3, (x)=>{
    console.log(`返回值: ${x}`)
})
// 1s后输出 返回值 9

// 失败处理
function double(value,success,fail){
    setTimeout(() => {
        try {
            if(typeof value !== 'number'){
                throw 'args not number'
            }
            success(2 * value)
        } catch (error) {
            fail(error)            
        }
    }, 1000);
}

const successCallBack = (x) => console.log(`Success: ${x}`)
const failCallBack = (e) => console.log(`Fail: ${e}`); 
// 传入成功和失败的回调
double(2,successCallBack,failCallBack) // Success: 4
double('abc',successCallBack,failCallBack) // Fail: args not number

// 这样的模式已经不可取了,因为必须在初始化异步操作时定义回调。异步函数的返回值只在短时间内存在
// 只有预备好将这个短时间内存在的值作为参数的回调才能接收到它。

1.2 期约

  1. ECMAScript6新增的引用类型Promise,可以通过new操作符来实例化。创建新期约时需要传入执行器函数作为参数。
  2. 期约状态:
    1. 待定 (pending)
    2. 兑现 (fulfilled),也称为 解决状态 resolved
    3. 拒绝 (rejected)
  3. 期约的状态是不可逆的,只要从待定状态转换为兑现或拒绝,期约的状态就不会改变了。
  4. 期约的状态是私有的,不能直接通过JavaScript检测到。这主要是为了避免根据读到的期约状态,以同步的方式处理期约对象。另外,期约的状态也不能被外部JavaScript代码修改。期约故意将异步行为封装起来,从而隔离外部的同步代码

1.2.1 通过执行函数控制期约状态

  1. 由于期约的状态是私有的,所以只能在内部进行操作。执行器函数主要有两项职责:初始化期约的异步行为和控制状态的最终转换
    1. 控制期约状态的转换是通过调用它的两个函数参数实现的。 resolve() reject()
let p1 = new Promise((resolve, reject) => resolve());
setTimeout(console.log, 1000, p1);

// 查看执行顺序
new Promise(() => setTimeout(console.log, 0, "executor"));
setTimeout(console.log, 0, "promise init");
// executor
// promise init

// 添加setTimeout可以推迟切换状态
let p2 = new Promise((resolve, reject) => setTimeout(resolve, 1000));

// 在console.log打印期约的时候,还不会执行超时回调
setTimeout(console.log, 0, p2);

  1. Promise.resolve() 也可以实例化一个解决的期约。
    1. 使用这个静态方法,实际上可以把任何值都转换为一个期约。
    2. 如果传入的参数是一个期约,那它的行为就类似于一个空包装。
    3. 静态方法能够包装任何非期约值,包括错误对象,并将其转换为解决的期约。因此可能会有不符合预期的行为。
let p1 = new Promise((resolve, reject) => {
  resolve();
});
let p2 = Promise.resolve();

// 使用这个静态方法 Promise.resolve() 可以把任何值都转换为一个期约
setTimeout(console.log, 0, Promise.resolve(3));

// 对于这个静态方法,如果传入的参数本身是一个期约,那么它的行为就类似于一个空包装。
let p3 = Promise.resolve(4);
setTimeout(console.log, 0, p3 === Promise.resolve(p3));
// 返回结果为 true

// 静态方法能够包装任何非期约值,包括错误对象,并将其转换为解决的期约。因此可能会有不符合预期的行为。
let p4 = Promise.resolve(new Error("foo"));
setTimeout(console.log, 0, p4);

// 输出结果: Promise<resolved> Error: foo
  1. Promise.reject() 也可以实例化一个拒绝的期约并抛出一个异步错误。这个错误不能通过try/catch捕获,而只能通过拒绝处理程序捕获
    1. 如果给它传入一个期约对象,则这个期约会成为它返回的拒绝期约的理由。
let p = Promise.reject(3);
setTimeout(console.log, 0, p);
// 输出结果: rejected: 3

// 如果给它传入一个期约对象,则这个期约会成为它返回的拒绝期约的理由。

setTimeout(console.log, 0, Promise.reject(Promise.resolve()));
// 输出结果: Promise<rejected>: Promise {<fulfilled>}

1.2.2 期约的实例方法

  1. 实现Thenable接口:在ECMAScript暴露的异步结构中,任何对象都有一个then()方法。这个方法被认为实现了Thenable接口。
  2. Promise.prototype.then() 是为期约实例添加处理程序的主要方法。这个then()方法接收最多两个参数:onResolved处理程序和onRejected处理程序。这两个参数都是可选的,如果提供的话,则会在期约分别进入"兑现"和"拒绝"状态时执行。
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body></body>
</html>
<script>
  function onResolved(id) {
    setTimeout(console.log, 0, id, "resolved");
  }
  function onRejected(id) {
    setTimeout(console.log, 0, id, "rejected");
  }

  let p1 = new Promise((resolve, reject) => {
    setTimeout(resolve, 3000);
  });
  let p2 = new Promise((resolve, reject) => {
    setTimeout(reject, 3000);
  });

  p1.then(
    () => {
      onResolved("p1");
    },
    () => {
      onRejected("p2");
    }
  );
  p2.then(
    function () {
      onResolved("p2");
    },
    function () {
      onRejected("p2");
    }
  );
  // 3秒后输出 p1 'resolved'  p2 'rejected'
</script>
  1. Promise.prototype.then()方法返回一个新的期约实例
    1. 新期约实例基于onResolved处理程序的返回值构建。换句话说,该处理程序的返回值会通过Promise.resolve()包装来生成新期约。
    2. 如果没有提供这个处理程序,则Promise.resolve()就会包装上一个期约解决之后的值。
    3. 如果没有显式的返回语句,则Promise.resolve()会包装默认的返回值undefined.
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <script>
      let p1 = Promise.resolve("foo");
      let p2 = p1.then();
      setTimeout(console.log, 0, p2); // Promise {<fulfilled>: 'foo'}

      // 如果没有显式的返回语句,则Promise.resolve()会包装默认的返回值undefined
      let p3 = p1.then(() => undefined);
      let p4 = p1.then(() => {});
      let p5 = p1.then(() => Promise.resolve());
      setTimeout(console.log, 0, p3); // Promise {<fulfilled>: undefined}
      setTimeout(console.log, 0, p4); // Promise {<fulfilled>: undefined}
      setTimeout(console.log, 0, p5); // Promise {<fulfilled>: undefined}

      // 如果有显式的返回值,则Promise.resolve()会包装这个值
      let p6 = p1.then(() => "bar");
      let p7 = p1.then(() => Promise.resolve("bar"));
      setTimeout(console.log, 0, p6); // Promise {<fulfilled>: 'bar'}
      setTimeout(console.log, 0, p7); // Promise {<fulfilled>: 'bar'}

      // Promise.resolve()保留返回的期约
      let p8 = p1.then(() => new Promise(() => {}));
      let p9 = p1.then(() => Promise.reject());
      setTimeout(console.log, 0, p8); // Promise {<pending>}
      setTimeout(console.log, 0, p9); // Promise {<rejected>: undefined}

      // 抛出异常会返回拒绝的期约
      let p10 = p1.then(() => {
        throw "bar";
      });
      setTimeout(console.log, 0, p10); // Promise {<rejected>: 'bar'}

      // 返回错误值不会触发拒绝行为,而会把错误对象包装在一个解决的期约中
      let p11 = p1.then(() => Error("zxc"));
      setTimeout(console.log, 0, p11); // Promise {<fulfilled>: Error: zxc}
    </script>
  </body>
</html>
  1. onRejected处理程序也与之类似:onRejected处理程序返回的值也会被Promise.resolve()包装。因此,拒绝处理程序在捕获错误后不抛出异常是符合期约的行为,应该返回一个解决期约。
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <script>
      let p1 = Promise.reject("foo");
      // 调用then()时不传处理程序则原样向后传
      let p2 = p1.then();
      setTimeout(console.log, 0, p2); // Promise {<rejected>: 'foo'}
      // 这些都一样
      let p3 = p1.then(null, () => undefined);
      let p4 = p1.then(null, () => {});
      let p5 = p1.then(null, () => Promise.resolve());
      setTimeout(console.log, 0, p3); // Promise {<fulfilled>: undefined}
      setTimeout(console.log, 0, p4); // Promise {<fulfilled>: undefined}
      setTimeout(console.log, 0, p5); // Promise {<fulfilled>: undefined}

      // Promise.resolve() 保留返回的期约
      let p6 = p1.then(null, () => new Promise(() => {}));
      let p7 = p1.then(null, () => Promise.reject());
      setTimeout(console.log, 0, p6); // Promise {<pending>}
      setTimeout(console.log, 0, p7); // {<rejected>: undefined}

      let p8 = p1.then(null, () => {
        throw "bar";
      });
      let p9 = p1.then(null, () => Error("zxc"));
      setTimeout(console.log, 0, p8); // {<rejected>: 'bar'}
      setTimeout(console.log, 0, p9); //  {<fulfilled>: Error: zxc
    </script>
  </body>
</html>

  1. Promise.prototype.catch() 用于给期约添加拒绝处理程序。这个方法只接收一个参数,onRejected处理程序。其实,这个方法就是一个语法糖,调用它就相当于调用Promise.prototype.then(null,onRejected)
    <script>
      let p1 = Promise.reject();
      let onRejected = function (e) {
        setTimeout(console.log, 0, "rejected");
      };
      // 这两种添加拒绝处理程序的方式是一样的
      p1.then(null, onRejected); // rejected
      p1.catch(onRejected); // rejected
      // 返回一个新的期约实例
      // let p2 = Promise.prototype.catch()
      let p3 = new Promise(() => {});
      let p4 = p3.catch();
      setTimeout(console.log, 0, p3); // Promise {<pending>}
      setTimeout(console.log, 0, p4); // Promise {<pending>}
      setTimeout(console.log, 0, p3 == p4); // false
    </script>
  1. Promise.prototype.finally() 用于给期约添加onFinally处理程序,这个处理程序在期约转换为解决或拒绝状态时都会执行。主要用于添加清理代码。
    <script>
      let p1 = Promise.resolve();
      let p2 = Promise.reject();

      function onFinally(e) {
        setTimeout(console.log, 0, "finally");
      }
      p1.finally(onFinally); // finally
      p2.finally(onFinally); // finally

      // Promise.prototype.finally() 方法返回一个新的期约实例
      let p3 = new Promise(() => {});
      let p4 = p3.finally();
      setTimeout(console.log, 0, p4); // Promise {<pending>}
      
    </script>
  1. finally()新期约实例不同于then()或catch()方法返回的实例。在大多数情况下它将表现为父期约的传递。
    <script>
      let p1 = Promise.resolve('foo')

      let p2 = p1.finally(()=> undefined)
      let p3 = p1.finally(()=> Promise.resolve())
      let p4 = p1.finally(()=> {})
      let p5 = p1.finally(()=> 'bar')
      let p6 = p1.finally(()=> Error('zxc'))

      setTimeout(console.log,0,p2) // Promise {<fulfilled>: 'foo'}
      setTimeout(console.log,0,p3) // Promise {<fulfilled>: 'foo'}
      setTimeout(console.log,0,p4) // Promise {<fulfilled>: 'foo'}
      setTimeout(console.log,0,p5) // Promise {<fulfilled>: 'foo'}
      setTimeout(console.log,0,p6) // Promise {<fulfilled>: 'foo'}
    </script>

1.2.3 期约连锁与期约合成

  1. 期约连锁:把期约串联起来,因为每个期约实例的方法then() catch() finally()都会返回一个新的期约对象。
 <script>
      let p = new Promise((resolve, reject) => {
        console.log("first...");
        resolve();
      });
      p.then(() => {
        console.log("second...");
      })
        .then(() => {
          console.log("third...");
        })
        .then(() => {
          console.log("four...");
        });
      // 这样使用最终执行了一连串同步任务,作用不大

      // 改写这个例子,执行异步任务。让每个执行器都返回一个期约实例。
      let p1 = new Promise((resolve, reject) => {
        console.log("p1 executor");
        setTimeout(resolve, 1000);
      });
      p1.then(
        () =>
          new Promise((resolve, reject) => {
            console.log("p2 executor");
            setTimeout(resolve, 1000);
          })
      )
        .then(
          () =>
            new Promise((resolve, reject) => {
              console.log("p3 executor");
              setTimeout(resolve, 1000);
            })
        )
        .then(
          () =>
            new Promise((resolve, reject) => {
              console.log("p4 executor");
              setTimeout(resolve, 1000);
            })
        );
      // 每隔1s会按次数依次输出

      // 把生成期约的代码提取到一个工厂函数中,
      function delayedResolve(str) {
        return new Promise((resolve, reject) => {
          console.log(str);
          setTimeout(resolve, 1000);
        });
      }
      delayedResolve("p1 executor")
        .then(() => delayedResolve("p2 executor"))
        .then(() => delayedResolve("p3 executor"))
        .then(() => delayedResolve("p4 executor"));
      // 每隔1s会按次数依次输出
    </script>
  1. 因为then() catch() finally()都返回期约,所以串联这些方法很直观
let p1 = new Promise((resolve,reject)=>{
    console.log('init promise)
    reject()
})
p1.catch(()=> console.log('reject handler'))
.then(()=> console.log('resolve handler'))
.finally(()=> console.log('finally handler'))
  1. Promise.all()和Promise.race() 将多个期约实例组合成一个期约的静态方法,而合成后期约的行为取决于内部期约的行为。
    1. Promise.all() 静态方法创建的期约会在一组期约全部解决之后再解决。这个静态方法接收一个可迭代对象,返回一个新期约。
    2. 合成的期约只会在每个包含的期约都解决之后才解决
    3. 如果至少有一个包含的期约待定,则合成的期约也会待定
    4. 如果有一个包含的期约拒绝,则合成的期约也会拒绝
    5. 如果合成的期约都成功解决,则合成期约的解决值就是所有包含期约解决值的数组,按照迭代器顺序。
    6. 如果有期约拒绝,则第一个拒绝的期约会将自己的理由作为合成期约的拒绝理由。之后再拒绝的期约不会影响最终期约的拒绝理由。
let p = Promise.all([Promise.resolve(), Promise.resolve()]);
      // 可迭代对象中的元素会通过Promise.resolve()转换为期约
      let p2 = Promise.all([3, 4]);
      // 空的可迭代对象等价于Promise.resolve()
      let p3 = Promise.all([]);
      // 合成的期约只会在每个包含的期约都解决之后才解决
      let p4 = Promise.all([
        Promise.resolve(),
        new Promise((resolve, reject) => setTimeout(resolve, 1000)),
      ]);
      setTimeout(console.log, 0, p4);
      // 1s 后输出
      p4.then(() => setTimeout(console.log, 0, "all resolved()..."));

      // 如果至少有一个包含的期约待定,则合成的期约也会待定
      let p5 = Promise.all([new Promise(() => {})]);
      // 如果至少有一个包含的期约拒绝,则合成的期约也会拒绝
      let p6 = Promise.all([
        Promise.resolve(),
        Promise.resolve(),
        Promise.reject(),
      ]);

      // 如果所有期约都成功解决,则合成期约的解决值就是所有包含期约解决值的数组。按照迭代顺序
      let p7 = Promise.all([
        Promise.resolve(3),
        Promise.resolve(),
        Promise.resolve(4),
      ]);
      p7.then((val) => setTimeout(console.log, 0, val)); // [3, undefined, 4]

      // 如果有期约拒绝,则第一个拒绝的期约会将自己的理由作为合成期约的拒绝理由。
      let p8 = Promise.all([
        Promise.resolve(),
        Promise.reject(3),
        Promise.reject(4),
      ]);
      p8.catch((reason) => setTimeout(console.log, 0, reason)); // 3
  1. Promise.race() 静态方法返回一个包装期约,是一组集合中最先解决或拒绝的期约的镜像。这个方法接收一个可迭代对象,返回一个新期约。
    1. 无论是解决还是拒绝,只要是第一个落定的期约,Promise.race()就会包装其解决值或拒绝理由并返回心期约。
 let p = Promise.race([Promise.resolve(), Promise.resolve()]);
      // 可迭代对象中的元素会通过Promise.resolve()转换为期约
      let p2 = Promise.race([3, 4]);
      // 空的可迭代对象等价于Promise.resolve()
      let p3 = Promise.race([]);
      // 解决先发生,超时后的解决被忽略
      let p4 = Promise.race([
        Promise.resolve(3),
        new Promise((resolve, reject) => setTimeout(resolve, 1000)),
      ]);
      setTimeout(console.log, 0, p4); // 3

      // 拒绝先发生,超时后的解决被忽略
      let p5 = Promise.race([
        new Promise((resolve, reject) => setTimeout(resolve, 1000)),
        Promise.reject(4),
      ]);
      setTimeout(console.log, 0, p5); // 4

      // 迭代顺序决定了落定顺序
      let p6 = Promise.race([
        Promise.resolve(3),
        Promise.reject(4),
        Promise.resolve(5),
      ]);
      setTimeout(console.log, 0, p6); // 3

1.2.4 期约扩展

  1. ES6期约实现是可靠的,但它也有不足之处了。如期约取消进度追踪.
      // 定义一个类,包装一个期约,把解决方法暴露给cancelFn函数。
      class CancelToken {
        constructor(cancelFn) {
          this.promise = new Promise((resolve, reject) => {
            cancelFn(() => {
              setTimeout(console.log, 0, "delay cancel...");
              resolve();
            });
          });
        }
      }
      const start = document.querySelector("#start");
      const end = document.querySelector("#close");

      // 控制什么情况下可以取消期约的函数
      function cancelDelayedResolve(delay) {
        setTimeout(console.log, 0, "set delay...");
        return new Promise((resolve, reject) => {
          const id = setTimeout(() => {
            setTimeout(console.log, 0, "delayed resolve...");
            resolve();
          }, delay);
          const cancelToken = new CancelToken((cancelCallBack) => {
            end.addEventListener("click", cancelCallBack);
          });
          cancelToken.promise.then(() => clearTimeout(id));
        });
      }
      start.addEventListener('click',()=>cancelDelayedResolve(2000))

1.3 异步期约

  1. 异步函数,也称为"async/await"语法关键字,以同步方式写的代码能够以异步执行。
        // 期约在超时之后会解决一个值
        let p = new Promise((resolve,reject)=> setTimeout(console.log,1000,3))
        // 如果程序中的其他代码要在这个值可用时访问它,则需要一个解决处理程序
        p.then(x => console.log(x))
        // 可以将处理程序定义为一个函数
        function handler(x){
            console.log(x);
        }
        let p1 = new Promise((resolve,reject)=> setTimeout(console.log,1000,3))
        p1.then(handler)
        // 这个改进也不太大,因为任何需要访问这个期约所产生的值,都需要以处理程序的形式来接收这个值.

1.3.1 异步函数

  1. async关键字用于声明异步函数,这个关键字可以用在函数声明、函数表达式、箭头函数和方法上。
  2. 使用 async关键字可以让函数有异步特性,但总体上其代码仍然是同步求值的。而在参数或闭包方面,异步函数仍然具有普通函数的正常行为。
  3. 异步函数如果使用return关键字返回了值,这个值会被Promise.resolve()包装成一个期约对象。如果没有return则返回undefined 异步函数始终返回期约对象。 在函数外部调用这个函数可以得到它返回的期约。
  4. 在期约处理程序中一样,在异步函数中抛出错误会返回拒绝的期约。拒绝期约的错误不会被异步函数捕获。
    // 异步函数声明方式
    async function foo(){}
    let bar = async function(){}
    let baz = async ()=>{}
    class User {
        async name(){}
    }

    // 异步函数仍然具有JavaScript函数的正常行为
    async function foo(){
        console.log(1);
    }
    foo()
    console.log(2);
    // 输出 1 2

    // 异步函数使用return 
    async function test(){
        console.log(1);
        return 3
    }
    // 给返回的期约添加一个解决处理程序
    test().then(console.log)
    console.log(2);
    // 输出结果 1 2 3

1.3.2 await

  1. 因为异步函数主要针对不会马上完成的任务,所以自然需要一种暂停和恢复执行的能力。使用await关键字可以暂停异步函数代码的执行,等待期约解决。
  2. await 关键字会暂停执行异步函数后面的代码,让出JavaScript运行时的执行线程。 await关键字尝试解包对象的值,然后将这个值传给表达式,在异步恢复异步函数的执行。 3.await 关键字的用法与JavaScript的一元操作一样,可以单独使用,也可以在表达式中使用。
  3. await关键字期待一个实现thenable接口的对象,但常规的值也可以。 等待会抛出错误的同步操作,会返回拒绝的期约。单独的Promise.reject()不会被异步函数捕获,而会抛出未捕获错误,不过,对拒绝的期约使用awiat则会释放错误值。
async function foo() {
          let p = new Promise((resolve, reject) =>
            setTimeout(console.log, 1000, 3)
          );
          console.log(await p);
        }
        foo();

        // 异步打印test1
        async function test1() {
          console.log(await Promise.resolve("test1"));
        }
        test1();
        // 异步打印test2
        async function test2() {
          return await Promise.resolve("test2");
        }
        test2().then(console.log);
        // 1s 后打印 test3
        async function test3() {
          await new Promise((resolve, reject) => setTimeout(resolve, 1000));
          console.log("test3");
        }
        test3();
      //   等待会抛出错误的同步操作,则返回拒绝的期约
      async function test4() {
        console.log(1);
        await (() => {
          throw 3;
        })();
      }
      test4().catch(console.log);
      console.log(2);

1.3.3 停止和恢复执行

  1. async/await中真正起作用的是await。JavaScript运行时在碰到await关键字时,会记录在哪里暂停执行。等到await右边的值可用给了,JavaScript运行时会向消息队列中推送一个任务,这个任务会恢复异步函数的执行。
    // await后面跟着一个立即可用的值,函数的其余部分也会被异步求值
    async function test1(){
        console.log(2);
        await null
        console.log(4);
    } 
    console.log(1);
    test1()
    console.log(3);
    // 输出 1 2 3 4
    /**
     *  分析执行步骤: 
     *      1. 打印 1
     *      2. 调用异步函数test1()
     *      3. 在test1()中打印 2
     *      4. 在test1中 await关键字暂停执行,为立即可用的值null向消息队列中添加一个任务
     *      5. test1() 退出
     *      6. 打印 3
     *      7. 同步线程中代码执行完毕
     *      8. JavaScript运行时从消息队列中取出任务,恢复异步函数执行
     *      9. 在test1()中恢复执行,await取得null值(这里并没有使用)
     *      10. 在test1()中打印 4
     *      11. test1()返回
     * 
    */
 // await后面跟着一个期约,则问题会稍微复杂一些,此时,为了执行异步函数,实际上会有两个任务被添加到消息队列并被异步求值
        async function test(){
            console.log(2);
            console.log(await Promise.resolve(8));
            console.log(9);
        }
        async function bar(){
            console.log(4);
            console.log(await 6);
            console.log(7);
        }
        console.log(1);
        test()
        console.log(3);
        bar()
        console.log(5);
        // 输出 1 2 3 4 5 6 7 8 9
        /**
         *  分析执行步骤: 
         *      1. 打印 1
         *      2. 调用异步函数test()
         *      3. 在test() 中打印 2
         *      4. 在test()中  await关键字暂停执行,为立即可用的值null向消息队列中添加一期约在落定之后执行的任务
         *      5. 期约立即落定,把给await提供值的任务添加到消息队列中
         *      6. test() 退出
         *      7. 打印 3
         *      8. 调用异步函数bar()
         *      9. 在bar()中 打印 4
         *      10. 在bar()中 await关键字暂停执行,为立即可用的值6向消息队列中添加一个任务
         *      11. bar() 退出
         *      12. 打印 5
         *      13. 顶级线程执行完毕
         *      14. JavaScript运行时从消息队列中取出解决await期约的处理程序,并将解决的值 8 提供给它
         *      15. JavaScript运行时向消息队列中添加一个恢复执行test()函数的任务
         *      16. (在bar())中 恢复执行 , await 取得值6
         *      17. (在bar())中 打印 6
         *      18. (在bar())中 打印 7
         *      19. bar()返回
         *      20. 异步任务完成,JavaScript从消息队列中取出恢复执行test()的任务及值8
         *      21. (在test())中 打印 8
         *      22. (在test())中 打印 9
         *      23. foo() 返回 
         * 
        */