之前在做 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 一下。