之前在做 code review 时分发现有同事运用 try catch 包装了一堆异步代码,于是个人就觉得很奇怪,难道不应该只 catch 或许出问题的代码吗?同事告诉我说 try catch 太细的话会出现表里作用域不一致,需求提前声明变量

let res: Data[] = [];
try {
  res = await fetchData();
} catch (err) {
  // 过错操作或许停止
  // return
}
// 持续履行正常逻辑

的确,一方面开发者不应该大范围包裹非反常代码,另一方面提前声明变量会让代码不连贯一起也会打断思路。其中一个方式是直接运用原生 Promie 而不是 async。

fetchData().then((res) => {
}).catch((err) => {
});

这样关于单个异步恳求当然没有任何问题,假如是具有依赖性的异步恳求。虽然能够再 Promise 中回来别的的 Promise 恳求,但是这样处理 catch 却只能有一个。

fetchData().then((res) => {
  // 事务处理
  return fetchData2(res);
}).then((res) => {
  // 事务处理
}).catch((err) => {
  // 只能做一个通用的过错处理了
});

假如需求多个 catch 处理,咱们就需求这样写。

fetchData().then((res) => {
  // 事务处理
  return fetchData2(res);
}).catch((err) => {
  // 过错处理并且回来 null
  return null;
}).then((res) => {
  if (res === null) {
    return;
  }
  // 事务处理
}).catch((err) => {
  // 过错处理
});

这时分开发者也要考虑 fetchData2 会不会回来 null 的问题。于是个人开端找一些方法来帮助咱们处理这个问题。

await-to-js

await-to-js 是一个辅助开发者处理异步过错的库。咱们先来看看该库是怎么处理咱们问题的。

import to from "await-to-js";
const [fetch1Err, fetch1Result] = await to(fetchData());
if (fetch1Err) {
  // 过错操作或许停止
  // return
}
const [fetch2Err, fetch1Result] = await to(fetchData2(fetch1Result));
if (fetch2Err) {
  // 过错操作或许停止
  // return
}

源码十分简略。

export function to(
  promise,
  errorExt,
) {
  return promise
    .then((data) => [null, data])
    .catch((err) => {
      if (errorExt) {
        const parsedError = Object.assign({}, err, errorExt);
        return [parsedError, undefined];
      }
      return [err, undefined];
    });
}

运用 try-run-js

看到 await-to-js 将过错作为正常流程的一部分,于是个人想到是不是能通过 try catch 处理一些异步代码问题呢?

我马上想到了需求获取 DOM 节点的需求。现有结构都运用了数据驱动的思路,但是 DOM 具体什么时分烘托是未知的,于是个人想到之前代码,Vue 需求获取 ref 并进行回调处理。

function resolveRef(refName, callback, time: number = 1) {
  // 超过 10 次跳出递归
  if (time > 10) throw new Error(`cannot find ref: ${refName}`);
  // 
  const self = this;
  // 获取 ref 节点
  const ref = this.$refs[refName];
  if (ref) {
    callback(ref);
  } else {
    // 没有节点就下一次
    this.$nextTick(() => {
      resolveRef.call(self, refName, callback, time + 1);
    });
  }
}

当然了,上述代码的确能够处理此类的问题,在处理此类问题时分咱们能够替换 ref 和 nextTick 的代码。于是 await-to-js 的逻辑下,个人开发了 try-run-js 库。咱们先看一下该库怎么运用。

import tryRun from "try-run-js";
tryRun(() => {
  // 直接测验运用正常逻辑代码
  // 千万不要添加 ?.
  // 代码不会犯错而不会重试
  this.$refs.navTree.setCurrentKey("xxx");
}, {
  // 重试次数
  retryTime: 10,
  // 下次操作前需求的延迟时刻
  timeout: () => {
    new Promise((resolve) => {
      this.$nextTick(resolve);
    });
  },
});

咱们也能够获取过错数据和成果。

import tryRun from "try-run-js";
const getDomStyle = async () => {
  // 获取一步的
  const { error: domErr, result: domStyle } = await tryRun(() => {
    // 回来 dom 节点样式,不必管是否存在 ppt
    // 千万不要添加 ?.
    // 代码不会犯错而回来 undefined
    return document.getElementById("domId").style;
  }, {
    // 重试次数
    retryTime: 3,
    // 回来数字的话,函数会运用 setTimeout
    // 参数为当时重试的次数,第一次重试 100 ms,第二次 200
    timeout: (time) => time * 100,
    // 还能够直接回来数字,不传递默许为 333
    // timeout: 333
  });
  if (domErr) {
    return {};
  }
  return domStyle;
};

当然了,该库也是支撑回来元组以及 await-to-js 的 Promise 过错处理的功用的。

import { tryRunForTuple } from "try-run-js";
const [error, result] = await tryRunForTuple(fetchData());

try-run-js 项目演进

try-run-js 中心在于 try catch 的处理,下面是关于 try-run-js 的编写思路。期望能对我们有一些帮助

支撑 await-to-js

const isObject = (val: any): val is Object =>
  val !== null &&
  (typeof val === "object" || typeof val === "function");
const isPromise = <T>(val: any): val is Promise<T> => {
  // 继承了 Promise
  // 拥有 then 和 catch 函数,对应手写的 Promise
  return val instanceof Promise || (
    isObject(val) &&
    typeof val.then === "function" &&
    typeof val.catch === "function"
  );
};
const tryRun = async <T>(
  // 函数或许 promise
  promiseOrFun: Promise<T> | Function,
  // 配置项目
  options?: TryRunOptions,
): Promise<TryRunResultRecord<T>> => {
  // 当时参数是否为 Promise
  const runParamIsPromise = isPromise(promiseOrFun);
  const runParamIsFun = typeof promiseOrFun === "function";
  // 既不是函数也不是 Promise 直接回来过错
  if (!runParamIsFun && !runParamIsPromise) {
    const paramsError = new Error("first params must is a function or promise");
    return { error: paramsError } as TryRunResultRecord<T>;
  }
  if (runParamIsPromise) {
    // 直接运用 await-to-js 代码
    return runPromise(promiseOrFun as Promise<T>);
  }
};

履行过错重试

接下来咱们开端利用 try catch 捕获函数的过错并且重试。

// 默许 timeout
const DEFAULT_TIMEOUT: number = 333
// 异步等待
const sleep = (timeOut: number) => {
  return new Promise<void>(resolve => {
    setTimeout(() => {
      resolve()
    }, timeOut)
  })
}
const tryRun = async <T>(
  promiseOrFun: Promise<T> | Function,
  options?: TryRunOptions,
): Promise<TryRunResultRecord<T>> => {
  const { retryTime = 0, timeout = DEFAULT_TIMEOUT } = {
    ...DEFAULT_OPTIONS,
    ...options,
  };
  // 当时第几次重试
  let currentTime: number = 0;
  // 是否成功
  let isSuccess: boolean = false;
  let result;
  let error: Error;
  while (currentTime <= retryTime && !isSuccess) {
    try {
      result = await promiseOrFun();
      // 履行完并获取成果后认为当时是成功的
      isSuccess = true;
    } catch (err) {
      error = err as Error;
      // 测验次数加一
      currentTime++;
      // 留意这儿,笔者在这儿犯了一些过错
      // 假如没有处理好就会履行不需求处理的 await
      // 1.假如当时不需求重新恳求(重试次数为 0),直接越过
      // 2.最终一次也失利了(重试完了)也是要越过的
      if (retryTime > 0 && currentTime <= retryTime) {
        // 获取时刻
        let finalTimeout: number | Promise<any> = typeof timeout === "number"
          ? timeout
          : DEFAULT_TIMEOUT;
        // 假如是函数履行函数
        if (typeof timeout === "function") {
          finalTimeout = timeout(currentTime);
        }
        // 当时回来 Promise 直接等待
        if (isPromise(finalTimeout)) {
          await finalTimeout;
        } else {
          // 假如最终成果不是 number,改为默许数据
          if (typeof finalTimeout !== "number") {
            finalTimeout = DEFAULT_TIMEOUT;
          }
          // 这儿我测验运用了 NaN、 -Infinity、Infinity 
          // 发现 setTimeout 都进行了处理,下面是浏览器的处理方式
          // If timeout is an Infinity value, a Not-a-Number (NaN) value, or negative, let timeout be zero.
          // 负数,无穷大以及 NaN 都会变成 0
          await sleep(finalTimeout);
        }
      }
    }
  }
  // 成功或许失利的回来
  if (isSuccess) {
    return { result, error: null };
  }
  return { error: error!, result: undefined };
};

这样,咱们基本完成了 try-run-js.

添加 tryRunForTuple 函数

这个就很简略了,直接直接 tryRun 并改造其成果:

const tryRunForTuple = <T>(
  promiseOrFun: Promise<T> | Function,
  options?: TryRunOptions): Promise<TryRunResultTuple<T>> => {
  return tryRun<T>(promiseOrFun, options).then(res => {
    const { result, error } = res
    if (error) {
      return [error, undefined] as [any, undefined]
    }
    return [null, result] as [null, T]
  })
}

代码都在 try-run-js 中,我们还会在什么情况下运用 try-run-js 呢?一起也欢迎各位提交 issue 以及 pr。

鼓励一下

假如你觉得这篇文章不错,期望能够给与我一些鼓励,在我的 github 博客下帮忙 star 一下。

博客地址

参考资料

await-to-js

try-run-js