Skip to content

Commit

Permalink
273
Browse files Browse the repository at this point in the history
  • Loading branch information
ascoders committed Feb 27, 2023
1 parent bb4940d commit 215729e
Show file tree
Hide file tree
Showing 2 changed files with 191 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="./可视化搭建/272.%E5%AE%B9%E5%99%A8%E7%BB%84%E4%BB%B6%E8%AE%BE%E8%AE%A1.md">272.容器组件设计</a>
最新精读:<a href="./可视化搭建/273.%E7%BB%84%E4%BB%B6%E5%80%BC%E4%B8%8E%E8%81%94%E5%8A%A8.md">273.组件值与联动</a>

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

Expand Down Expand Up @@ -306,6 +306,7 @@
- <a href="./可视化搭建/270.%E7%94%BB%E5%B8%83%E4%B8%8E%E7%BB%84%E4%BB%B6%E5%85%83%E4%BF%A1%E6%81%AF%E6%95%B0%E6%8D%AE%E6%B5%81.md">270.画布与组件元信息数据流</a>
- <a href="./可视化搭建/271.%E5%8F%AF%E8%A7%86%E5%8C%96%E6%90%AD%E5%BB%BA%E5%86%85%E7%BD%AE%20API.md">271.可视化搭建内置 API</a>
- <a href="./可视化搭建/272.%E5%AE%B9%E5%99%A8%E7%BB%84%E4%BB%B6%E8%AE%BE%E8%AE%A1.md">272.容器组件设计</a>
- <a href="./可视化搭建/273.%E7%BB%84%E4%BB%B6%E5%80%BC%E4%B8%8E%E8%81%94%E5%8A%A8.md">273.组件值与联动</a>

### SQL

Expand Down
189 changes: 189 additions & 0 deletions 可视化搭建/273.组件值与联动.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
组件联动是指几个组件相互关联。也就是当一个组件状态变化时,其他组件可以响应。

组件联动是多对多关系的,且目的分为一次性与持续性:

- 多对多关系:即一个组件可以同时被多个组件联动;多个组件可以同时联动一个组件。
- 一次性与持续性:一次性事件可以被覆盖,持续性事件会同时生效,且要考虑叠加关系。

一定程度上,持续性事件可以覆盖一次性事件的场景:组件永远响应最后一个过来的事件即可。

接下来我们引入 **组件值****值联动** 两个概念,来实现持续性联动功能。

## 组件值

每个组件实例都有一个唯一的组件值。

我们可以通过 `getValue(componentId)``setValue(componentId, value)` 访问或更新组件值:

```js
const table = {
componentName: "table",
runtimeProps: ({ componentId, setValue }) => ({
// 给组件注入 onChange 函数,在其触发时更新当前组件实例的组件值
onChange: (value) => setValue(componentId, value),
}),
};
```

也可以通过 `componentMeta.value` 声明组件值,比如下面的例子,让组件值与 `props.value` 同步:

```js
const table = {
componentName: "table",
// 声明 value 的值为组件 props.value 的返回值,并随着组件 props.value 的更新而更新
value: ({ selector }) => selector(({ props }) => props.value),
};
```

以上两种方式任选一种使用即可。

**为什么一个组件实例只有一个组件值?**

一个组件可能同时拥有多个状态,比如该组件内部有一个输入框,还有一个按钮,可能输入框的值,与按钮的点击状态都会对其他组件产生联动效果。但这并不意味着一个组件实例需要多个组件值,我们可以将组件值定义为对象,并合理规划不同的 key 描述不同维度的值:

```js
// 组件值结构
{
// 组件内输入框的值
text: '123',
// 组件内按钮被按下的次数
buttonClickTimes: false
}
```

**为什么不用 `props.value` 代替组件值?**

理论上可以,但这样限定了组件对 props 的定义。也许有的组件用 `props.value` 描述输入框的值,但也有比如 Check 组件,用 `props.checked` 表示当前选中状态。只有抽象一个定义与组件元信息的规则,让业务自由对接,才可以让组件值适配任意类型的组件。

## 值联动

有了组件值这个概念,就可以以组件实例为粒度,设计组件的关联关系了。

为了让组件关联更加灵活,我们的设计需要满足以下几种能力:

1. 联动关系支持多对多。
2. 可以随着全局数据状态变化,或者组件自身 props 变化,随时改变组件关联关系。
3. 一个组件可以定义其他几个组件的关联关系,哪怕自己不参与到联动关系链中。
4. 当组件实例被删除时,由它定义的联动关系立刻失效。

估我们采用 `componentMeta.valueRelates` 声明式定义值联动关系:

```js
const table = {
componentName: "table",
valueRelates: ({ componentId, selector }) => {
return [
{
sourceComponentId: componentId, // 自己为触发源
targetComponentId: selector(({ props }) => props.targetComponentId), // 目标组件 ID 为 props.targetComponentId
},
];
},
};
```

这样设计可以同时满足以上四个要求,解释如下:

1. 可以在任意组件实例定义多个联动关系,自然可以实现多对多联动。
2. `valueRelates` 引入 `selector` 可以响应 `state``props` 的变化,可以由任意状态驱动联动关系更新。
3. 如果 `source``target` 都不指向自己,则自己不参与到联动关系链中。
4. 声明式定义方式,自然在组件实例被销毁时失效。

那么组件如何响应联动呢?重点就在这里,组件可以通过 `selector(({ relates }) =>)``relates` 拿到自己当前的联动状态,比如:

```js
const table = {
componentName: "table",
runtimeProps: ({ selector }) => {
// relates 结构如下,对于每一个作用于自己的组件实例 ID 与最新 value 值都可以拿到
// [{
// sourceComponentId: 'abc',
// value: '123'
// }]
const relates = selector(({ relates }) => relates);
return {
status: relates.length > 0 ? "linked" : "free",
};
},
};
```

如果我们在 `runtimeProps` 里使用 `selector` 监听 `relates`,就可以在联动状态变化时,驱动组件渲染,并传入联动相关状态;如果在 `fetcher` 里使用 `selector` 监听 `relates`,就可以在联动状态变化时,驱动组件触发查询,等等。

以后我们拓展越来越多的组件元信息回调函数,支持了 `selector` 之后,都可以声明式的响应 `relates` 变化,也就是组件可以声明式灵活响应联动,真正意义上让联动可以用在任何场景。

框架没有对联动做太多的联动内置行为,实现的都是灵活规则,虽然业务需要补全不少声明,但胜在灵活与用法统一。

## 描述联动行为

不同的联动可能做不同的事,比如一个输入框组件,可能同时有以下两种作用:

1. 让另一个组件查询条件增加 "where name=" 当前输入框的值。
2. 当组件的值为 "delete" 时,让画布另一个组件隐藏。

为了区分联动的功能,可以在 `valueRelates` 增加 `payload` 参数,描述该联动的目的:

```js
const table = {
componentName: "table",
valueRelates: ({ componentId, selector }) => {
return [
{
sourceComponentId: componentId,
targetComponentId: selector(({ props }) => props.targetComponentId),
// 作用为目标组件的查询筛选条件
payload: "filter",
},
{
sourceComponentId: componentId,
targetComponentId: selector(({ props }) => props.targetComponentId),
// 作用为目标组件是否隐藏
payload: "hide",
},
];
},
};
```

然后目标组件就可以根据实际情况,在 `fetcher` 过滤 `relates``payload="filter"` 的值,在 `runtimeProps` 过滤 `relates``payload="hide"` 的值。

## 用持续联动实现一次性联动

每一次组件更新 value 值后,都会刷新对目标组件 `relates` 的位置,具体来说,会将其置顶,所以目标组件可以根据 `relates` 先来后到顺序判断,比如在联动效果冲突时,让排在前面的优先生效。

比如:

```js
const table = {
componentName: "table",
runtimeProps: ({ selector }) => {
// 找到最初生效的,payload 为 color 的联动,覆盖 props.color
const relateColor = selector(({ relates }) =>
relates.find((each) => each.payload === "color")
);
return {
color: relateColor,
};
},
};
```

当另一个组件触发 value 变化时,它会排在目标组件 `relates` 最前面,这样的话,如果目标组件按照如上方式编写响应代码,就总会响应最后一次生效的联动。

## 总结

这一节介绍了如何设置联动,并引出了组件值概念。

在框架层定义抽象的组件值概念,并通过声明式或调用式对接到 `state` 状态或组件 `props`,这种抽象理念会贯穿整个框架的设计过程。相似的 `valueRelates` 也具有声明式能力,并将联动作用通过 `selector``relates` 对象传递给组件实例使用,让联动的消费灵活度大大增加。

可视化搭建框架设计思路可能都大同小异,但可惜的是,许多搭建框架都对比如联动、查询等场景做了定制化约束,使每个框架或多或少存在着私有协议,而我在这个系列想强调的是,可以进一步抽象,让框架提供业务自由定义协议的能力,而不是提供某个固定的协议。

> 讨论地址是:[精读《组件值与联动》· Issue #469 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/469)
**如果你想参与讨论,请 [点击这里](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 215729e

Please sign in to comment.