Skip to content

Commit

Permalink
279
Browse files Browse the repository at this point in the history
  • Loading branch information
ascoders committed May 22, 2023
1 parent 33fbb1c commit 79a3129
Show file tree
Hide file tree
Showing 2 changed files with 125 additions and 1 deletion.
3 changes: 2 additions & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

前端界的好文精读,每周更新!

最新精读:<a href="./可视化搭建/278.ComponentLoader%20%E4%B8%8E%E5%8A%A8%E6%80%81%E7%BB%84%E4%BB%B6.md">278.ComponentLoader 与动态组件</a>
最新精读:<a href="./可视化搭建/279.%E8%87%AA%E5%8A%A8%E6%89%B9%E5%A4%84%E7%90%86%E4%B8%8E%E5%86%BB%E7%BB%93.md">279.自动批处理与冻结</a>

素材来源:[周刊参考池](https://github.com/ascoders/weekly/issues/2)

Expand Down Expand Up @@ -312,6 +312,7 @@
- <a href="./可视化搭建/275.%E7%BB%84%E4%BB%B6%E5%80%BC%E6%A0%A1%E9%AA%8C.md">275.组件值校验</a>
- <a href="./可视化搭建/276.keepAlive%20%E6%A8%A1%E5%BC%8F.md">276.keepAlive 模式</a>
- <a href="./可视化搭建/278.ComponentLoader%20%E4%B8%8E%E5%8A%A8%E6%80%81%E7%BB%84%E4%BB%B6.md">278.ComponentLoader 与动态组件</a>
- <a href="./可视化搭建/279.%E8%87%AA%E5%8A%A8%E6%89%B9%E5%A4%84%E7%90%86%E4%B8%8E%E5%86%BB%E7%BB%93.md">279.自动批处理与冻结</a>

### SQL

Expand Down
123 changes: 123 additions & 0 deletions 可视化搭建/279.自动批处理与冻结.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
性能在可视化搭建也是极为重要的,如何尽可能减少业务感知,最大程度的提升性能是关键。

其实声明式一定程度上可以说是牺牲了性能换来了可维护性,所以在一个完全声明式的框架下做性能优化还是非常有挑战的。我们采取了两种策略来优化性能,分别是自动批处理与冻结。

## 自动批处理

首先,框架内任何状态更新都不会立即触发响应,而是统一收集起来后,一次性触发响应,如下面的例子:

```jsx
const divMeta: ComponentMeta = {
// ...
fetcher: ({ selector, setRuntimeProps, componentId }) => {
const name = selector(({ props }) => props.name)
const email = selector(({ props }) => props.email)
fetch('...', {
data: { name, email }
}).then((res) => {
setRuntimeProps(componentId, old => ({
...old ?? {},
data: res.data
}))
})
}
}

const App = () => {
const { setProps } = useDesigner()
const onClick = useCallback(() => {
setProps('1', props => ({ ...props, name: 'bob' }))
setProps('1', props => ({ ...props, email: '[email protected]' }))
}, [])
}
```
上面例子中,`fetcher` 通过 `selector` 监听了 `props.name``props.email`,当连续调用两次 `setProps` 分别修改 `props.name``props.email` 时,只会合并触发一次 `fetcher` 而不是两次,这种设计让业务代码减少了重复执行的次数,简化了业务逻辑复杂度。
另一方面,在自动批处理的背后,还有一个框架如何执行 `selector` 的性能优化点,即框架是否能感知到 `fetcher` 依赖了 `props.name``props.email`?如果框架知道,那么当比如 `props.appId` 或者其他 `state.` 状态变化时,根本不需要执行 `fetcher` 内的 `selector` 判断返回引用是否变化,这能减少巨大的碎片化堆栈时间。
一个非常有效的收集方式是利用 Proxy,将 `selector` 内用到的数据代理化,利用代理监听哪些函数绑定了哪些变量,并在这些变量变化时按需重新执行。
笔者用一段较为结构化的文字描述这背后的性能优化是如何发生的。
一、组件元信息声明式依赖了某些值
比如下面的代码,在 `meta.fetcher` 利用 `selector` 获取了 `props.name``props.email` 的值,并在这些值变化时重新执行 `fetcher`
```jsx
const divMeta: ComponentMeta = {
// ...
fetcher: ({ selector, setRuntimeProps, componentId }) => {
const name = selector(({ props }) => props.name)
const email = selector(({ props }) => props.email)
}
}
```
在这背后,其实 `selector` 内拿到的 `props` 或者 `state` 都已经是 Proxy 代理对象,框架内部会记录这些调用关系,比如这个例子中,会记录组件 ID 为 1 的组件,`fetcher` 绑定了 `props.name``props.email`
二、状态变化
当任何地方触发了状态变化,都不会立刻计算,而是在 `nextTick` 时机触发清算。比如:
```js
setProps('1', props => ({ ...props, name: 'bob' }))
setProps('1', props => ({ ...props, email: '[email protected]' }))
```
虽然连续触发了两次 `setProps`,但框架内只会在 `nextTick` 时机总结出发生了一次变化,此时组件 ID 为 1 的组件实例 `props.name``props.email` 发生了变化。
接着,会从内部 selector 依赖关系的缓存中找到,发现只有 `fetcher` 函数依赖了这两个值,所以就会精准的执行 `fetcher` 中两个 `selector`,执行结果发现相比之前的值引用变化了,最后判定需要重新执行 `fetcher`,至此响应式走完了一次流程。
当然在 `fetcher` 函数内可能再触发 `setProps` 等函数修改状态,此时会立刻进入判定循环直到所有循环走完。另外假设此次状态变化没有任何 meta 声明式函数依赖了,那么即便画布有上千个组件,每个组件实例绑定了十几个 meta 声明式函数,此时都不会触发任何一个函数的执行,性能不会随着画布组件增加而恶化。
## 冻结
冻结可以把组件的状态凝固,从而不再响应任何事件,也不会重新渲染。
```js
const chart: ComponentMeta = {
/** 默认 false */,
defaultFreeze: true
}
```
或者使用 `setFreeze` 修改冻结状态:
```js
const { setFreeze } = useDesigner()
// 设置 id 1 的组件为冻结态
setFreeze('1', true)
```
### 为什么要提供冻结能力?
当仪表盘内组件数量过多时,业务上会考虑做按需加载,或者按需查询。但因为组件间存在关联关系,可视化搭建框架(我们用 Designer 指代)在初始化依然会执行一些初始函数,比如 `init`,同时组件依然会进行一次初始化渲染,虽然业务层会做一些简化处理,比如提前 `Return null`, 但组件数量多了之后想要扣性能依然还有优化空间。
所以 Designer 就提供了冻结能力,从根本上解决视窗外组件造成的性能影响。为什么可以根本解决性能影响呢?因为处于冻结态的组件:
- 前置性。通过 `defaultFreeze` 在组件元信息初始化设置为 `false`,那么所有初始化逻辑都不会执行。
- 不会响应任何状态变更,连内置的 `selector` 执行都会直接跳过,完全屏蔽了这个组件的存在,可以让 Designer 内部调度逻辑大大提效。
- 不会触发重渲染。如果组件初始化就设置为冻结,那么初始化渲染也不会执行。
### 怎么使用冻结能力?
建议统一把所有组件 `defaultFreeze` 设置为 true,然后找一个地方监听滚动或者视窗的变化,通过 `setFreeze` 响应式的把视窗内组件解冻,把移除视窗的组件冻结。
特别注意,如果有组件联动,冻结了触发组件会导致联动失效,因此业务最好把那些 **即便不在视窗内,也要作用联动** 的组件保持解冻状态。
## 总结
总结一下,首先因为声明式代码中修改状态的地方很分散,甚至执行时机都交由框架内部控制,因此手动 batch 肯定是不可行的,基于此得到了更方便,性能全方面优化了的自动 batch。
其次是业务层面的优化,当组件在视窗外后,对其所有响应监听都可以停止,所以我们想到定义出冻结的概念,让业务自行决定哪些组件处于冻结态,同时冻结的组件从元信息的所有回调函数,到渲染都会完全停止,可以说,画布即便存在一万个冻结状态的组件,也仅仅只有内存消耗,完全可以做到 0 CPU 消耗。
> 讨论地址是:[精读《自动批处理与冻结》· Issue #484 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/484)
**如果你想参与讨论,请 [点击这里](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 comments on commit 79a3129

Please sign in to comment.