js终止程序有两种方式(如果还有别的请告知我)
- throw
- return
这两个好像是两大阵营,前者我个人最推崇,但是很少见人用, 不知道啥原因(兴许是讨厌写try catch吧)。
刚入门那会,总觉得下面这样的验证好麻烦
js
const formValues = {
mobile: '',
name: '',
}
function onSubmit() {
if (!formValues.name) {
alert('请输入用户名')
return
}
if (!formValues.mobile) {
alert('请输入手机号')
return
}
// n个表单验证,return N次 alert N 次
}
后来发现,可以用throw改进一下
js
const formValues = {
mobile: '',
name: '',
}
function onSubmit() {
try {
if (!formValues.name) throw ('请输入用户名')
if (!formValues.mobile) throw String('请输入手机号')
} catch (error) {
alert(error)
}
}
这样就好看多了(但很多人觉得,try catch 难看 😂)。 后来验证多了,就把验证挪到单独一个函数里
js
const formValues = {
mobile: '',
name: '',
}
function validateFormValues() {
if (!formValues.name) throw ('请输入用户名')
if (!formValues.mobile) throw String('请输入手机号')
}
function onSubmit() {
try {
validateFormValues()
} catch (error) {
alert(error)
}
}
主函数看起来干净些了。这是throw才能做到的,报错跳出调用栈。
由此引出了之前看过的一种写法,用return(我不喜欢)
js
const formValues = {
mobile: '',
name: '',
}
function validateFormValues() {
if (!formValues.name) {
alert('请输入用户名')
return
}
if (!formValues.mobile) {
alert('请输入手机号')
return
}
return true
}
function onSubmit() {
const isValidateFormValuesSuccess = validateFormValues();
// 这点我不喜欢,因为还要再写一次判断
if (!isValidateFormValuesSuccess) return
}
如果是遇到嵌套深的复杂场景,函数套函数,是不是就很无力了,因为没法跳转函数栈,只能层层判断。
但是throw就可以无视嵌套,直接报错,最晚层接住错误就可以了。当我们写代码的时候,想终止程序,就直接throw。
看下面这段代码 promise async await 联合使用。可用空间就大了撒。 从此随便造,函数大了就拆逻辑成小函数,想终止就throw
js
// promise里面throw 错误 = reject(错误)
async function onSubmit() {
try {
await new Promise((resolve) => {
throw String('故意报错')
})
console.log('a'); // 不会执行
} catch (error) {
alert(error) // 结果:alert 故意报错
}
}
// promise里面 catch 也可以直接抛错误
async function onSubmit() {
try {
await new Promise((resolve, reject) => {
reject('故意报错')
}).catch(error => {
throw error
})
console.log('b'); // 不会执行
} catch (error) {
alert(error)
}
}
可能有的小伙伴会想,try catch 有性能问题。看下图,来源于经典书《高性能javaScript》
之前公司小伙伴也有这个疑问,我翻了书加上用chorme 微信小程序编辑器,去测过,最终差别不大,没问题的,使劲用。
由此启发,这时候引入一个catchError函数,专门用来接收报错
js
// 报错白名单,收到这个就不提示报错了,标明是主动行为
const MANUAL_STOP_PROGRAM = "主动终止程序";
/**
* @feat < 捕获错误 >
* @param { unknown } error 错误
* @param { string } location 错误所在位置,标识用的
* @param { Function } onParsedError 解析错误,可能需要把这个错误弄到页面上显示是啥错误
*/
function catchError(error, location, { onParsedError } = {}) {
try {
const errorIsObj = error && typeof error === "object";
if (errorIsObj) throw JSON.stringify(error);
// 其他处理,比如判断是取消网络请求,错误集中上报等等,大家自由发挥,有啥好想法欢迎评论区留言
throw error;
} catch (error) {
console.error(`${location || ""}-捕获错误`, error);
if (new RegExp(MANUAL_STOP_PROGRAM).test(error)) throw MANUAL_STOP_PROGRAM;
// 错误解析完毕
onParsedError && onParsedError(error);
alert(error) // 弹窗提示错误
throw MANUAL_STOP_PROGRAM;
}
}
在上面中 MANUAL_STOP_PROGRAM 就是个白名单了,专门用来标识是主动报错,但是不提示错误 每次 catchError 之后,要把 MANUAL_STOP_PROGRAM 继续抛出来,因为我们可能业务调用链很深,需要多个地方使用到 catchError,但是只需要报错一次,而且需要报错告知外层不执行后续逻辑。
再结合 location 参数,我们可以看到清晰的错误来源
catchError 这个是我得初步设想,一直想做统一的错误收集中心。如果您有好的想法,欢迎告知评论区。
上面执行后,控制台是有点难看的
通过window.onerror能收集到部分错误,但是异步的就收集不到了。(async promise 这些就没办法了),如果您有啥办法能收集到,麻烦告知一下。
但是控制台难看有啥关系呢。(反正用户和老板也看不到 😂)
js
/**
* @param { string } message
* @param { source } 表示发生错误的脚本文件的 URL
* @param { lineno } 表示发生错误的行号
*/
window.onerror = function(message, source, lineno) {
// 错误处理逻辑
};
下面是一个比较极端的例子,演示一下深层级的报错效果
js
// 生成假数据
async function genrateMockList() {
try {
const list = await Promise.all(
new Array(100).fill(',').map((item, index) => {
try {
if (index === 1) throw String('map 故意报错,嵌套比较深了')
return {
index: 'name'
}
} catch (error) {
catchError(error, 'genrateMockList__item')
}
})
)
return list
} catch (error) {
catchError(error, 'genrateMockList')
}
}
async function getDetails() {
try {
const dataList = await genrateMockList();
console.log('a') // 不会执行
} catch (error) {
catchError(error, 'getDetails')
}
}
getDetails();
笔者始终认为,写代码很重要的一点是 数据结构 和 程序流控制。
结构清楚了,所有的东西都能一生二二生四一直延伸变化。
程序流控制我们尽量做到简单清晰。
别再纠结用了多少个try catch 多难看了,多一个try catch 就多一分安心,特别是复杂的业务逻辑,可能需要经过5-6个小函数,这时候加上try catch 就能把报错范围缩小,等到代码完全可靠后再移除 try 也不迟。
兴许return 当初设计就只是为了返回值吧,我总觉得throw才是js设计者的终止程序的用意。这段历史有知道的也欢迎说一下。
以上内容供大家参考。有啥看法欢迎评论区留言。
预告:下周开始写一些组件设计思考。是一个系列,存货不多,顶多写几篇