2022年5月

一、快速理解
1、promise
promise 提供了一种异步处理的操作,避免了嵌套地狱的出现。

// 一个传统写法的嵌套地狱
function qiantaodiyu() {
// 模拟一个异步操作
const syncTask = (content, time, resolve) => {

setTimeout(() => {
  console.log(content, time);
  resolve && resolve(content);
}, time);

};

// 传统的异步嵌套
syncTask('执行 task1', 100, () => {

syncTask('执行 task2', 300, () => {
  syncTask('执行 task3', 100, () => {
    console.log(chalk.green('这是嵌套地狱最后一个回调的内容'))
  })
});

});
}

// 以上的写法改成 promise 后的处理
function promiseDemo() {
// 用 promise 编写一个异步操作
const promiseTask = (content, time) => {

return new Promise((resolve) => {
  setTimeout(() => {
    console.log(content, time);
    resolve && resolve(content);
  }, time);
});

};

// 用 promise 的 then 来避免异步嵌套
promiseTask('执行 task1', 100)

.then((result) => {
  console.log('task1 resolve result:', result);
  return promiseTask('执行 task2', 300);
})
.then((result) => {
  console.log('task2 resolve result:', result);
  return promiseTask('执行 task3', 300);
});

}

2、async 与 await
async 与 await 最重要的一点就是能够让 promise 异步操作变成同步的操作。这样所有异步任务就都可以用 promise 去编写,然后可以并行处理时就用 promise.all ,如果要串行处理时就用 async 与 await 处理。具体示例如下。

// 用 setTimeout 模拟一个异步操作
function timeoutPromise(content, time) {
return new Promise((resolve) => {

setTimeout(() => {
  console.log(content, time);
  resolve && resolve(content);
}, time);

});
}

// 要并行操作时就采用这种写法
/* 注意:promise.all 的 resolve 是所有异步的 resolve 都完成后才执行,回调结果是一个数组,
包含所有异步的回调的结果。而 reject 是第一个出错的时候就会执行,回调结果是第一个异步的错误信息。
因此,在出错的时候,其他的异步并不会停止,而是会继续执行。
*/
function parallelTask() {
const task1 = timeoutPromise('执行 task1', 100);
const task2 = timeoutPromise('执行 task2', 300);
const task3 = timeoutPromise('执行 task3', 100);
const task4 = timeoutPromise('执行 task4', 400);
const task5 = timeoutPromise('执行 task5', 100);

Promise.all([task1, task2, task3, task4, task5])

.then((values) => {
  console.log(
    chalk.green(`所有任务完成后的操作,最后返回的内容是:${values}`)
  );
})
.catch((err) => {
  console.log(chalk.red('这是出错时的处理', err));
});

}

// 要串行处理时就采用这种写法
async function seriesTask() {
try {

const task1 = await timeoutPromise('执行 task1', 100);
const task2 = await timeoutPromise('执行 task2', 300);
const task3 = await timeoutPromise('执行 task3', 100);
const task4 = await timeoutPromise('执行 task4', 400);
const task5 = await timeoutPromise('执行 task5', 100);

const values = [task1, task2, task3, task4, task5]

console.log(chalk.green(`这是串行任务完成后的结果,内容是:${values.toString()}`));

} catch (error) {

console.log(chalk.red('这是出错时的处理', error))

}
}

二、相关资料
建议按照以下的顺序进行了解:
1、使用 promise
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Using_promises
2、promise
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Promise#%E5%88%9B%E5%BB%BApromise
3、async 与 await
https://developer.mozilla.org/zh-CN/docs/Learn/JavaScript/Asynchronous/Async_await

三,以前的学习笔记

async 与 await

// 这个函数是一个异步的操作
function resolveAfter2Seconds() {
return new Promise(resolve => {

setTimeout(() => {
  resolve('resolved');
}, 2000);

});
}

// await 让上面的异步函数进入到同步的反馈机制中,只在 async 定义的函数下才有效
async function asyncCall() {
console.log('calling');
const result = await resolveAfter2Seconds();
console.log(result);
// expected output: "resolved"
}

asyncCall();

  • 特别注意的点:async 与 await 只在当前函数里有效,比如以下两个例子:
    例子一,async 未定义

/**

  • 注意这里的 fetch 是一个 promise 来的,所以这里的
  • await 保证了后面的内容可以在 fetch 执行完后再处理。
  • 而此时 response 获取到的是 fetch 执行后的返回值
    */

async function postData(url = '', data = {}) {
const response = await fetch(url, {

method: 'POST', 
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),

});

console.log("response", response);

if (response.ok) {

return response.json();

}
}

/**

  • 由于 query 这里并没有定义 async 和 await ,所以即使 postData 自身有这个定义,
  • 在 query 里,postData 还是一个异步的操作,所以此时 data 获取到的并不是查询结果,而是一个 promise 对象
    */

function query() {
let resJson = postData('/material/query');
console.log('resJson', resJson);
}

document.querySelector('#test-query').addEventListener('click', () => {
console.log('test-query');
query();
console.log('test-query end');
})

例子二:

/**

  • 注意这里的 fetch 是一个 promise 来的,所以这里的
  • await 保证了后面的内容可以在 fetch 执行完后再处理。
  • 而此时 response 获取到的是 fetch 执行后的返回值
    */

async function postData(url = '', data = {}) {
const response = await fetch(url, {

method: 'POST', 
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),

});

console.log("response", response);

if (response.ok) {

return response.json();

}
}

/**

  • query 这里定义了 async 和 await ,所以此时 data 就是 postData 的查询结果。
    */

async function query() {
let resJson = await postData('/material/query');
console.log('resJson', resJson);
}

/**

  • 注意这一层也得用 async ,否则获取到的还是一个 promise 对象
    */

async function testQuery(){
console.log('test-query');
await query();
console.log('test-query end');
}
document.querySelector('#test-query').addEventListener('click', testQuery)

promise

  • 参考资料:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Using_promises
  • 为什么要有 promise
    一层层的回调函数会导致整体代码难以阅读,而用 promise 可以用链式的方式把这些内容给呈现出现,更有利于理解与阅读。
  • promise 中最重要的理念就是:
    连续执行两个或者多个异步操作,在上一个操作执行成功之后,开始下一个的操作,并带着上一步操作所返回的结果。
  • 要理解 promise 主要得理解两块内容,第一块是定义 promise,第二块是使用 promise。

定义 promise

定义一个 promise 主要通过以下的方式进行定义,这个定义中,resolve, reject 这两个参数主要用来处理回调内容,前者是处理成功后的回调,后者是处理失败时的回调。

new Promise((resolve, reject) => {

console.log('初始化');

resolve();

})

再加一层函数将上面的函数包裹起来后,就可以把这个定义的行为做成一个公用的方法,比如以下:

function promiseObj(param) {
return new Promise((resolve, reject) => {

console.log("初始化");
setTimeout(() => {
  const result = param.value + 1;

  resolve(result);
}, 3000);

});
}

// 定义一个 promise
const promiseExample = promiseObj({ value: 1 });
promiseExample.then((result) => {
console.log(result);
});

无论是多复杂的 promise 定义都是按照上面的这两种方式进行定义的。

使用 promise

同一个 promise 可以有多个 then 配置,多个 promise 混合使用则是通过 then 中的 return 来处理,具体示例如下:
同一个 promise :

function promiseOne() {

      return new Promise((resolve, reject) => {
        console.log("promiseOne 初始化");

        resolve('promiseOne');
      });
    }

// 同一个 promise 可以有多个 then 和 catch,当其中一个 then 抛出错误时,then 与 catch 中间的 then 会被跳过

    promiseOne()
      .then((curObj) => {
        throw new Error("有哪里不对了");
        // 抛出错误后,当前函数不会再执行,但后面的 then  与 catch 会继续执行

        console.log(`${curObj} :执行「这个」`);
      })
      .then((curObj) => {
        // 当前面发生错误时,这里会被跳过
        console.log(`${curObj} :执行「catch 前的 then」`);
      })
      .catch((curObj) => {
        console.log(`${curObj} :执行「那个」`);
      })
      .then((curObj) => {
        console.log(`${curObj} :执行「这个」,无论前面发生了什么`);
      });

多个 promise 混合使用:

function promiseOne() {

      return new Promise((resolve, reject) => {
        console.log("promiseOne 初始化");

        resolve('promiseOne');
      });
    }

    function promiseTwo() {
      return new Promise((resolve, reject) => {
        console.log("promiseTwo 初始化");

        resolve('promiseTwo');
      });
    }

    function promiseThird() {
      return new Promise((resolve, reject) => {
        console.log("promiseThird 初始化");

        resolve('promiseThird');
      });
    }

// 这是多个 promise 混合使用的场景,通过 return 的方式变换下一个 promise 对象

    promiseOne()
      .then((curObj) => {
        console.log(`执行了 ${curObj}`);
        return promiseTwo();
      })
      .then((curObj) => {
        console.log(`执行了 ${curObj}`);
        return promiseThird();
      })
      .then((curObj) => {
        console.log(`执行了 ${curObj}`);
        console.log(`整个链条结束`);
      });

一、结论
先说结论,对于通过 new Promise 等创建的 promise 实例,一定要用 reject 处理报错信息,可以通过这里写入详细的出错信息以及出错的来源,以便调用方能够跟踪错误的信息。(即下面测试案例中的方案二)。而且 reject 的处理,调用方是会正常收到错误提示的,也会阻断后续的脚本操作。
而对于 async 和 await 创建的 promise 因为没有 reject 处理的入口,所以比较适合两种处理方案:1、不主动捕捉错误,让出错的信息直接抛到调用方。2、有需要特殊处理错误的,则在 catch 里处理完,对有需要往上抛错误的场景则需要再通过 throw 的方式向调用方传递错误信息。
另外,由于 async 和 await 无法直接处理 resolve 与 reject ,因此,如果是编写一个公用的方法,不建议直接用 async 去编写,最好是自定义一个 promise ,然后将 async 包裹在这里面,这样才能够处理 resolve 与 reject。比如下方示例:

   // 用 setTimeout 模拟一个异步操作
function timeoutPromise(content, time, func) {
  return new Promise((resolve, reject) => {
    try {
      func(); // 用来触发错误
      setTimeout(() => {
        console.log(content, time);
        resolve && resolve(content);
      }, time);
      console.log(content, '再做点啥');
    } catch (error) {
      reject({
        source: `timeoutPromise ${content}`,
        error: error
      })
    }
  });
}

// 公用方法建议采用这种
function asyncFunc() {
  return new Promise(async (resolve, reject) => {
    await timeoutPromise('异步1转同步', 50, () => {});

    return await Promise.all([
      timeoutPromise('异步2', 100, () => {}),
      timeoutPromise('异步3 会出错的这一步', 200),
      timeoutPromise('异步4', 300, () => {}),
    ]).then((result) => {
      resolve(result);
    }).catch((error) => {
      reject({
        source: `${error.source} => asyncFunc`,
        error: error.error
      });
    });
  });
}

// 这种方式只适合作为最后执行的流程,或非公用的方法
async function asyncFunc() {
  await timeoutPromise('异步1转同步', 50, () => {});

  return await Promise.all([
    await timeoutPromise('异步2', 100, () => {}),
    await timeoutPromise('异步3 会出错的这一步', 200),
    await timeoutPromise('异步4', 300, () => {}),
  ]);
}

二、测试案例
1、html

    <button id="btn">测试 async 函数的异常处理</button>
  <script src="./async.js"></script>
  <script>
    document.querySelector('#btn').addEventListener('click', start);
  </script>

2、js

    /**
* 方案一:每一层都不捕捉错误
* 最底层会直接往上抛出错误
* 中间的 promise.all 除了出错的异步不能正常执行外,其他的异步都会全部执行完毕。同时也会往上抛出错误
* 最上层的 start 也会从出错的异步那里停止执行,同时直接向浏览器抛出错误
* 这种方式虽然最上层能够捕捉到错误,但由于不知是哪一层出的错,所以不好跟踪错误
*/

// // 用 setTimeout 模拟一个异步操作
// function timeoutPromise(content, time, func) {
//   return new Promise((resolve, reject) => {
//     func(); // 用来触发错误
//     setTimeout(() => {
//       console.log(content, time);
//       resolve && resolve(content);
//     }, time);
//     console.log(content, '再做点啥');
//   });
// }

// async function asyncFunc() {
//   await timeoutPromise('异步1转同步', 50, () => {});

//   return Promise.all([
//     timeoutPromise('异步2', 100, () => {}),
//     timeoutPromise('异步3 会出错的这一步', 200),
//     timeoutPromise('异步4', 300, () => {}),
//   ]);
// }

// async function start() {
//   console.log('start');
//   await asyncFunc();
//   console.log('end');
// }

/** 方案二:最底层采用 reject 处理错误
 * 与方案一一样每一层都会抛出错误,但不同的地方是这种方式可以在最底层的错误信息里加上来源,从而提高错误处理的效率
*/

// 用 setTimeout 模拟一个异步操作
// function timeoutPromise(content, time, func) {
//   return new Promise((resolve, reject) => {
//     try {
//       func(); // 用来触发错误
//       setTimeout(() => {
//         console.log(content, time);
//         resolve && resolve(content);
//       }, time);
//       console.log(content, '再做点啥');
//     } catch (error) {
//       reject({
//         source: `timeoutPromise ${content}`,
//         error: error
//       })
//     }
//   });
// }

// async function asyncFunc() {
//   await timeoutPromise('异步1转同步', 50, () => {});

//   return Promise.all([
//     timeoutPromise('异步2', 100, () => {}),
//     timeoutPromise('异步3 会出错的这一步', 200),
//     timeoutPromise('异步4', 300, () => {}),
//   ]);
// }

// async function start() {
//   console.log('start');
//   await asyncFunc();
//   console.log('end');
// }

/** 方案三:最底层采用 reject 处理错误,中间层采用了 try catch 的方式
 * 这种方案的特点是:timeoutPromise 会抛出错误给 asyncFunc,但 asyncFunc 不会抛出错误给 start。
 * 因此 start 会将后面的脚本都执行完。
 * 如果希望出错的时候不要出现
*/

// // 用 setTimeout 模拟一个异步操作
// function timeoutPromise(content, time, func) {
//   return new Promise((resolve, reject) => {
//     try {
//       func(); // 用来触发错误
//       setTimeout(() => {
//         console.log(content, time);
//         resolve && resolve(content);
//       }, time);
//       console.log(content, '再做点啥');
//     } catch (error) {
//       reject({
//         source: `timeoutPromise ${content}`,
//         error: error
//       })
//     }
//   });
// }

// async function asyncFunc() {
//   try {
//     await timeoutPromise('异步1转同步', 50, () => {});

//     Promise.all([
//       timeoutPromise('异步2', 100, () => {}),
//       timeoutPromise('异步3 会出错的这一步', 200),
//       timeoutPromise('异步4', 300, () => {}),
//     ]);
//   } catch (error) {
//     console.log('asyncFunc 捕捉并处理了错误', error);
//   }
// }

// async function start() {
//   console.log('方案三start');
//   await asyncFunc();
//   console.log('end');
// }

/** 方案四:timeoutPromise 和 asyncFunc 都用 reject 处理错误
 * 与方案三一样,asyncFunc 不会抛出错误,导致 start 无法及时捕捉到错误
*/

// // 用 setTimeout 模拟一个异步操作
// function timeoutPromise(content, time, func) {
//   return new Promise((resolve, reject) => {
//     try {
//       func(); // 用来触发错误
//       setTimeout(() => {
//         console.log(content, time);
//         resolve && resolve(content);
//       }, time);
//       console.log(content, '再做点啥');
//     } catch (error) {
//       reject({
//         source: `timeoutPromise ${content}`,
//         error: error
//       })
//     }
//   });
// }

// async function asyncFunc() {
//   await timeoutPromise('异步1转同步', 50, () => {});

//   return Promise.all([
//     timeoutPromise('异步2', 100, () => {}),
//     timeoutPromise('异步3 会出错的这一步', 200),
//     timeoutPromise('异步4', 300, () => {}),
//   ]).catch((error) => {
//     console.log(error);
//   });
// }

// async function start() {
//   console.log('方案四start');
//   await asyncFunc();
//   console.log('end');
// }

/** 方案五:timeoutPromise 用 reject 处理错误,asyncFunc 用 catch 重新抛出错误
 * 这个方案其实跟方案二类似。如果在 asyncFunc 有需要对底层的错误进行处理的,则可以加这一层,否则用方案二即可。
*/

// 用 setTimeout 模拟一个异步操作
function timeoutPromise(content, time, func) {
  return new Promise((resolve, reject) => {
    try {
      func(); // 用来触发错误
      setTimeout(() => {
        console.log(content, time);
        resolve && resolve(content);
      }, time);
      console.log(content, '再做点啥');
    } catch (error) {
      reject({
        source: `timeoutPromise ${content}`,
        error: error
      })
    }
  });
}

async function asyncFunc() {
  await timeoutPromise('异步1转同步', 50, () => {});

  return Promise.all([
    timeoutPromise('异步2', 100, () => {}),
    timeoutPromise('异步3 会出错的这一步', 200),
    timeoutPromise('异步4', 300, () => {}),
  ]).catch((error) => {
    throw error;
  });
}

async function start() {
  console.log('方案五start');
  await asyncFunc();
  console.log('end');
}