从 await-to-js 到 try-run-js

之前在做 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) => {// 错误处理并且返回 nullreturn 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// 千万不要添加 ?.// 代码不会出错而返回 undefinedreturn document.getElementById("domId").style;}, {// 重试次数retryTime: 3,// 返回数字的话,函数会使用 setTimeout// 参数为当前重试的次数,第一次重试 100 ms,第二次 200timeout: (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 = (val: any): val is Promise => {// 继承了 Promise// 拥有 then 和 catch 函数,对应手写的 Promisereturn val instanceof Promise || (isObject(val) &&typeof val.then === "function" &&typeof val.catch === "function");
};

const tryRun = async (// 函数或者 promisepromiseOrFun: Promise | Function,// 配置项目options?: TryRunOptions,
): Promise> => {// 当前参数是否为 Promiseconst 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;}if (runParamIsPromise) {// 直接使用 await-to-js 代码return runPromise(promiseOrFun as Promise);}
}; 

执行错误重试

接下来我们开始利用 try catch 捕获函数的错误并且重试。

// 默认 timeout
const DEFAULT_TIMEOUT: number = 333

// 异步等待
const sleep = (timeOut: number) => {return new Promise(resolve => {setTimeout(() => {resolve()}, timeOut)})
}

const tryRun = async (promiseOrFun: Promise | Function,options?: TryRunOptions,
): Promise> => {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 = 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 都会变成 0await sleep(finalTimeout);}}}}// 成功或者失败的返回if (isSuccess) {return { result, error: null };}return { error: error!, result: undefined };
}; 

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

添加 tryRunForTuple 函数

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

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

最后

整理了一套《前端大厂面试宝典》,包含了HTML、CSS、JavaScript、HTTP、TCP协议、浏览器、VUE、React、数据结构和算法,一共201道面试题,并对每个问题作出了回答和解析。

有需要的小伙伴,可以点击文末卡片领取这份文档,无偿分享

部分文档展示:



文章篇幅有限,后面的内容就不一一展示了

有需要的小伙伴,可以点下方卡片免费领取

你可能感兴趣的:(javascript,前端,vue.js)