Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fab11f31d2 | ||
|
|
8af572d9f9 |
@@ -6,7 +6,7 @@
|
||||
|
||||
前端界的好文精读,每周更新!
|
||||
|
||||
最新精读:<a href="./前沿技术/208.%E7%B2%BE%E8%AF%BB%E3%80%8ATypescript%204.4%E3%80%8B.md">208.精读《Typescript 4.4》</a>
|
||||
最新精读:<a href="./前沿技术/210.%E7%B2%BE%E8%AF%BB%E3%80%8Aclass%20static%20block%E3%80%8B.md">210.精读《class static block》</a>
|
||||
|
||||
素材来源:[周刊参考池](https://github.com/ascoders/weekly/issues/2)
|
||||
|
||||
@@ -166,6 +166,8 @@
|
||||
- <a href="./前沿技术/206.%E7%B2%BE%E8%AF%BB%E3%80%8A%E4%B8%80%E7%A7%8D%20Hooks%20%E6%95%B0%E6%8D%AE%E6%B5%81%E7%AE%A1%E7%90%86%E6%96%B9%E6%A1%88%E3%80%8B.md">206.精读《一种 Hooks 数据流管理方案》</a>
|
||||
- <a href="./前沿技术/207.%E7%B2%BE%E8%AF%BB%E3%80%8ATypescript%20infer%20%E5%85%B3%E9%94%AE%E5%AD%97%E3%80%8B.md">207.精读《Typescript infer 关键字》</a>
|
||||
- <a href="./前沿技术/208.%E7%B2%BE%E8%AF%BB%E3%80%8ATypescript%204.4%E3%80%8B.md">208.精读《Typescript 4.4》</a>
|
||||
- <a href="./前沿技术/209.%E7%B2%BE%E8%AF%BB%E3%80%8A%E6%8D%95%E8%8E%B7%E6%89%80%E6%9C%89%E5%BC%82%E6%AD%A5%20error%E3%80%8B.md">209.精读《捕获所有异步 error》</a>
|
||||
- <a href="./前沿技术/210.%E7%B2%BE%E8%AF%BB%E3%80%8Aclass%20static%20block%E3%80%8B.md">210.精读《class static block》</a>
|
||||
|
||||
### 设计模式
|
||||
|
||||
|
||||
@@ -0,0 +1,255 @@
|
||||
成熟的产品都有较高的稳定性要求,仅前端就要做大量监控、错误上报,后端更是如此,一个未考虑的异常可能导致数据错误、服务雪崩、内存溢出等等问题,轻则每天焦头烂额的处理异常,重则引发线上故障。
|
||||
|
||||
假设代码逻辑没有错误,那么剩下的就是异常错误了。
|
||||
|
||||
由于任何服务、代码都可能存在外部调用,只要外部调用存在不确定性,代码就可能出现异常,所以捕获异常是一个非常重要的基本功。
|
||||
|
||||
所以本周就精读 [How to avoid uncaught async errors in Javascript](https://advancedweb.hu/how-to-avoid-uncaught-async-errors-in-javascript/) 这篇文章,看看 JS 如何捕获异步异常错误。
|
||||
|
||||
## 概述
|
||||
|
||||
之所以要关注异步异常,是因为捕获同步异常非常简单:
|
||||
|
||||
```typescript
|
||||
try {
|
||||
;(() => {
|
||||
throw new Error('err')
|
||||
})()
|
||||
} catch (e) {
|
||||
console.log(e) // caught
|
||||
}
|
||||
```
|
||||
|
||||
但异步错误却无法被直接捕获,这不太直观:
|
||||
|
||||
```typescript
|
||||
try {
|
||||
;(async () => {
|
||||
throw new Error('err') // uncaught
|
||||
})()
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
```
|
||||
|
||||
原因是异步代码并不在 `try catch` 上下文中执行,唯一的同步逻辑只有创建一个异步函数,所以异步函数内的错误无法被捕获。
|
||||
|
||||
要捕获 `async` 函数内的异常,可以调用 `.catch`,因为 `async` 函数返回一个 Promise:
|
||||
|
||||
```typescript
|
||||
;(async () => {
|
||||
throw new Error('err')
|
||||
})().catch((e) => {
|
||||
console.log(e) // caught
|
||||
})
|
||||
```
|
||||
|
||||
当然也可以在函数体内直接用 `try catch`:
|
||||
|
||||
```typescript
|
||||
;(async () => {
|
||||
try {
|
||||
throw new Error('err')
|
||||
} catch (e) {
|
||||
console.log(e) // caught
|
||||
}
|
||||
})()
|
||||
```
|
||||
|
||||
类似的,如果在循环体里捕获异常,则要使用 `Promise.all`:
|
||||
|
||||
```typescript
|
||||
try {
|
||||
await Promise.all(
|
||||
[1, 2, 3].map(async () => {
|
||||
throw new Error('err')
|
||||
})
|
||||
)
|
||||
} catch (e) {
|
||||
console.log(e) // caught
|
||||
}
|
||||
```
|
||||
|
||||
也就是说 `await` 修饰的 Promise 内抛出的异常,可以被 `try catch` 捕获。
|
||||
|
||||
但不是说写了 `await` 就一定能捕获到异常,一种情况是 Promise 内再包含一个异步:
|
||||
|
||||
```typescript
|
||||
new Promise(() => {
|
||||
setTimeout(() => {
|
||||
throw new Error('err') // uncaught
|
||||
}, 0)
|
||||
}).catch((e) => {
|
||||
console.log(e)
|
||||
})
|
||||
```
|
||||
|
||||
这个情况要用 `reject` 方式抛出异常才能被捕获:
|
||||
|
||||
```typescript
|
||||
new Promise((res, rej) => {
|
||||
setTimeout(() => {
|
||||
rej('err') // caught
|
||||
}, 0)
|
||||
}).catch((e) => {
|
||||
console.log(e)
|
||||
})
|
||||
```
|
||||
|
||||
另一种情况是,这个 `await` 没有被执行到:
|
||||
|
||||
```typescript
|
||||
const wait = (ms) => new Promise((res) => setTimeout(res, ms))
|
||||
|
||||
;(async () => {
|
||||
try {
|
||||
const p1 = wait(3000).then(() => {
|
||||
throw new Error('err')
|
||||
}) // uncaught
|
||||
await wait(2000).then(() => {
|
||||
throw new Error('err2')
|
||||
}) // caught
|
||||
await p1
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
})()
|
||||
```
|
||||
|
||||
`p1` 等待 3s 后抛出异常,但因为 2s 后抛出了 `err2` 异常,中断了代码执行,所以 `await p1` 不会被执行到,导致这个异常不会被 catch 住。
|
||||
|
||||
而且有意思的是,如果换一个场景,提前执行了 `p1`,等 1s 后再 `await p1`,那异常就从无法捕获变成可以捕获了,这样浏览器会怎么处理?
|
||||
|
||||
```typescript
|
||||
const wait = (ms) => new Promise((res) => setTimeout(res, ms))
|
||||
|
||||
;(async () => {
|
||||
try {
|
||||
const p1 = wait(1000).then(() => {
|
||||
throw new Error('err')
|
||||
})
|
||||
await wait(2000)
|
||||
await p1
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
})()
|
||||
```
|
||||
|
||||
结论是浏览器 1s 后会抛出一个未捕获异常,但再过 1s 这个未捕获异常就消失了,变成了捕获的异常。
|
||||
|
||||
这个行为很奇怪,当程序复杂时很难排查,因为并行的 Promise 建议用 Promise.all 处理:
|
||||
|
||||
```typescript
|
||||
await Promise.all([
|
||||
wait(1000).then(() => {
|
||||
throw new Error('err')
|
||||
}), // p1
|
||||
wait(2000),
|
||||
])
|
||||
```
|
||||
|
||||
另外 Promise 的错误会随着 Promise 链传递,因此建议把 Promise 内多次异步行为改写为多条链的模式,在最后 `catch` 住错误。
|
||||
|
||||
还是之前的例子,Promise 无法捕获内部的异步错误:
|
||||
|
||||
```typescript
|
||||
new Promise((res, rej) => {
|
||||
setTimeout(() => {
|
||||
throw Error('err')
|
||||
}, 1000) // 1
|
||||
}).catch((error) => {
|
||||
console.log(error)
|
||||
})
|
||||
```
|
||||
|
||||
但如果写成 Promise Chain,就可以捕获了:
|
||||
|
||||
```typescript
|
||||
new Promise((res, rej) => {
|
||||
setTimeout(res, 1000) // 1
|
||||
})
|
||||
.then((res, rej) => {
|
||||
throw Error('err')
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error)
|
||||
})
|
||||
```
|
||||
|
||||
原因是,用 Promise Chain 代替了内部多次异步嵌套,这样多个异步行为会被拆解为对应 Promise Chain 的同步行为,Promise 就可以捕获啦。
|
||||
|
||||
最后,DOM 事件监听内抛出的错误都无法被捕获:
|
||||
|
||||
```typescript
|
||||
document.querySelector('button').addEventListener('click', async () => {
|
||||
throw new Error('err') // uncaught
|
||||
})
|
||||
```
|
||||
|
||||
同步也一样:
|
||||
|
||||
```typescript
|
||||
document.querySelector('button').addEventListener('click', () => {
|
||||
throw new Error('err') // uncaught
|
||||
})
|
||||
```
|
||||
|
||||
只能通过函数体内 `try catch` 来捕获。
|
||||
|
||||
## 精读
|
||||
|
||||
我们开篇提到了要监控所有异常,仅通过 `try catch`、`then` 捕获同步、异步错误还是不够的,因为这些是局部错误捕获手段,当我们无法保证所有代码都处理了异常时,需要进行全局异常监控,一般有两种方法:
|
||||
|
||||
- `window.addEventListener('error')`
|
||||
- `window.addEventListener('unhandledrejection')`
|
||||
|
||||
`error` 可以监听所有同步、异步的运行时错误,但无法监听语法、接口、资源加载错误。而 `unhandledrejection` 可以监听到 Promise 中抛出的,未被 `.catch` 捕获的错误。
|
||||
|
||||
在具体的前端框架中,也可以通过框架提供的错误监听方案解决部分问题,比如 React 的 [Error Boundaries](https://reactjs.org/docs/error-boundaries.html)、Vue 的 [error handler](https://v3.vuejs.org/api/application-config.html#errorhandler),一个是 UI 组件级别的,一个是全局的。
|
||||
|
||||
回过头来看,本身 js 提供的 `try catch` 错误捕获是非常有效的,之所以会遇到无法捕获错误的经常,大多是因为异步导致的。
|
||||
|
||||
然而大部分异步错误,都可以通过 `await` 的方式解决,我们唯一要注意的是,`await` 仅支持一层,或者说一条链的错误监听,比如这个例子是可以监听到错误的:
|
||||
|
||||
```typescript
|
||||
try {
|
||||
await func1()
|
||||
} catch (err) {
|
||||
// caught
|
||||
}
|
||||
|
||||
async function func1() {
|
||||
await func2()
|
||||
}
|
||||
|
||||
async function func2() {
|
||||
throw Error('error')
|
||||
}
|
||||
```
|
||||
|
||||
也就是说,只要这一条链内都被 `await` 住了,那么最外层的 `try catch` 就能捕获异步错误。但如果有一层异步又脱离了 `await`,那么就无法捕获了:
|
||||
|
||||
```typescript
|
||||
async function func2() {
|
||||
setTimeout(() => {
|
||||
throw Error('error') // uncaught
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
针对这个问题,原文也提供了例如 `Promise.all`、链式 Promise、`.catch` 等方法解决,因此只要编写代码时注意对异步的处理,就可以用 `try catch` 捕获这些异步错误。
|
||||
|
||||
## 总结
|
||||
|
||||
关于异步错误的处理,如果还有其它未考虑到的情况,欢迎留言补充。
|
||||
|
||||
> 讨论地址是:[精读《捕获所有异步 error》· Issue #350 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/350)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
|
||||
|
||||
> 关注 **前端精读微信公众号**
|
||||
|
||||
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
|
||||
|
||||
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
|
||||
@@ -0,0 +1,142 @@
|
||||
[class-static-block](https://github.com/tc39/proposal-class-static-block) 提案于 [2021.9.1](https://github.com/tc39/proposal-class-static-block/commit/c0cabee0aa2d036a8d902fea7bc1d179e3de2477) 进入 stage4,是一个基于 Class 增强的提案。
|
||||
|
||||
本周我们结合 [ES2022 feature: class static initialization blocks](https://2ality.com/2021/09/class-static-block.html) 这篇文章一起讨论一下这个特性。
|
||||
|
||||
## 概述
|
||||
|
||||
为什么我们需要 class static block 这个语法呢?其中一个原因是对 Class 静态变量的灵活赋值需求。以下面为例,我们想在 Class 内部对静态变量做批量初始化,就不得不写一个无用的 `_` 变量用来做初始化的逻辑:
|
||||
|
||||
```typescript
|
||||
class Translator {
|
||||
static translations = {
|
||||
yes: 'ja',
|
||||
no: 'nein',
|
||||
maybe: 'vielleicht',
|
||||
};
|
||||
static englishWords = [];
|
||||
static germanWords = [];
|
||||
static _ = initializeTranslator( // (A)
|
||||
this.translations, this.englishWords, this.germanWords);
|
||||
}
|
||||
function initializeTranslator(translations, englishWords, germanWords) {
|
||||
for (const [english, german] of Object.entries(translations)) {
|
||||
englishWords.push(english);
|
||||
germanWords.push(german);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
而且我们为什么把 `initializeTranslator` 写在外面呢?就因为在 Class 内部不能写代码块,但这造成一个严重的问题,是外部函数无法访问 Class 内部属性,所以需要做一堆枯燥的传值。
|
||||
|
||||
从这个例子看出,我们为了自定义一段静态变量初始化逻辑,需要做出两个妥协:
|
||||
|
||||
1. 在外部定义一个函数,并接受大量 Class 成员变量传参。
|
||||
2. 在 Class 内部定义一个无意义的变量 `_` 用来启动这个函数逻辑。
|
||||
|
||||
这实在太没有代码追求了,我们在 Class 内部做掉这些逻辑不就简洁了吗?这就是 class static block 特性:
|
||||
|
||||
```typescript
|
||||
class Translator {
|
||||
static translations = {
|
||||
yes: 'ja',
|
||||
no: 'nein',
|
||||
maybe: 'vielleicht',
|
||||
};
|
||||
static englishWords = [];
|
||||
static germanWords = [];
|
||||
static { // (A)
|
||||
for (const [english, german] of Object.entries(this.translations)) {
|
||||
this.englishWords.push(english);
|
||||
this.germanWords.push(german);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
可以看到,`static` 关键字后面不跟变量,而是直接跟一个代码块,就是 class static block 语法的特征,在这个代码块内部,可以通过 `this` 访问 Class 所有成员变量,包括 `#` 私有变量。
|
||||
|
||||
原文对这个特性使用介绍就结束了,最后还提到一个细节,就是执行顺序。即所有 `static` 变量或区块都按顺序执行,父类优先执行:
|
||||
|
||||
```typescript
|
||||
class SuperClass {
|
||||
static superField1 = console.log('superField1');
|
||||
static {
|
||||
assert.equal(this, SuperClass);
|
||||
console.log('static block 1 SuperClass');
|
||||
}
|
||||
static superField2 = console.log('superField2');
|
||||
static {
|
||||
console.log('static block 2 SuperClass');
|
||||
}
|
||||
}
|
||||
|
||||
class SubClass extends SuperClass {
|
||||
static subField1 = console.log('subField1');
|
||||
static {
|
||||
assert.equal(this, SubClass);
|
||||
console.log('static block 1 SubClass');
|
||||
}
|
||||
static subField2 = console.log('subField2');
|
||||
static {
|
||||
console.log('static block 2 SubClass');
|
||||
}
|
||||
}
|
||||
|
||||
// Output:
|
||||
// 'superField1'
|
||||
// 'static block 1 SuperClass'
|
||||
// 'superField2'
|
||||
// 'static block 2 SuperClass'
|
||||
// 'subField1'
|
||||
// 'static block 1 SubClass'
|
||||
// 'subField2'
|
||||
// 'static block 2 SubClass'
|
||||
```
|
||||
|
||||
所以 Class 内允许有多个 class static block,父类和子类也可以有,不同执行顺序结果肯定不同,这个选择权交给了使用者,因为执行顺序和书写顺序一致。
|
||||
|
||||
## 精读
|
||||
|
||||
结合提案来看,class static block 还有一个动机,就是给了一个访问私有变量的机制:
|
||||
|
||||
```typescript
|
||||
let getX;
|
||||
|
||||
export class C {
|
||||
#x
|
||||
constructor(x) {
|
||||
this.#x = { data: x };
|
||||
}
|
||||
|
||||
static {
|
||||
// getX has privileged access to #x
|
||||
getX = (obj) => obj.#x;
|
||||
}
|
||||
}
|
||||
|
||||
export function readXData(obj) {
|
||||
return getX(obj).data;
|
||||
}
|
||||
```
|
||||
|
||||
理论上外部无论如何都无法访问 Class 私有变量,但上面例子的 `readXData` 就可以,而且不会运行时报错,原因就是其整个流程都是合法的,最重要的原因是,class static block 可以同时访问私有变量与全局变量,所以可以利用其做一个 “里应外合”。
|
||||
|
||||
不过我并不觉得这是一个好点子,反而像一个 "BUG",因为任何对规定的突破都会为可维护性埋下隐患,除非这个特性用在稳定的工具、框架层,用来做一些便利性工作,最终提升了应用编码的体验,这种用法是可以接受的。
|
||||
|
||||
最后要意识到,class static block 本质上并没有增加新功能,我们完全可以用普通静态变量代替,只是写起来很不自然,所以这个特性可以理解为对缺陷的补充,或者是语法完善。
|
||||
|
||||
## 总结
|
||||
|
||||
总的来说,class static block 在 Class 内创建了一个块状作用域,这个作用域内拥有访问 Class 内部私有变量的特权,且这个块状作用域仅在引擎调用时初始化执行一次,是一个比较方便的语法。
|
||||
|
||||
原文下方有一些反对声音,说这是对 JS 的复杂化,也有诸如 JS 越来越像 Java 的声音,不过我更赞同作者的观点,也就是 Js 中 Class 并不是全部,现在越来越多代码使用函数式语法,即便使用了 Class 的场景也会存在大量函数申明,所以 class static block 这个提案对开发者的感知实际上并不大。
|
||||
|
||||
> 讨论地址是:[精读《class static block》· Issue #351 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/351)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
|
||||
|
||||
> 关注 **前端精读微信公众号**
|
||||
|
||||
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
|
||||
|
||||
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
|
||||
Reference in New Issue
Block a user