基于Effect的组件设计
Effect的概念起源
从输入输出的角度理解Effect https://link.excalidraw.com/p/readonly/KXAy7d2DlnkM8X1yps6L
编程中的Effect起源于函数式编程中纯函数的概念
纯函数是指在相同的输入下,总是产生相同的输出,并且没有任何副作用(side effect)的函数。
副作用是指函数执行过程中对函数外部环境进行的可观察的改变,比如修改全局变量、打印输出、写入文件等。
前端的典型副作用场景是 浏览器环境中在window上注册变量
副作用引入了不确定性,使得程序的行为难以预测和调试。为了处理那些需要进行副作用的操作,函数式编程引入了Effect的抽象概念。
它可以表示诸如读取文件、写入数据库、发送网络请求、DOM渲染等对外部环境产生可观察改变的操作。通过将这些操作包装在Effect中,函数式编程可以更好地控制和管理副作用,使得代码更具可预测性和可维护性。
实际工作中我们也是从React的useEffect开始直接使用Effect的说法
React: useEffect
useEffect
is a React Hook that lets you synchronize a component with an external system.
import { useState, useEffect } from 'react';
// 模拟异步事件
function getMsg() {
return new Promise((resolve) => {
setTimeout(() => {
resolve('React')
}, 1000)
})
}
export default function Hello() {
const [msg, setMsg] = useState('World')
useEffect(() => {
getMsg().then((msg) => {
setMsg(msg)
})
const timer = setInterval(() => {
console.log('test interval')
})
return () => {
// 清除异步事件
clearTimeout(timer)
}
}, [])
return (
<h1>Hello { msg }</h1>
);
}
Effect中处理异步事件,并在此处消除异步事件的副作用clearTimeout(timer)
,避免闭包一直无法被销毁
Vue: watcher
运行期自动依赖收集 示例
<script setup>
import { ref } from 'vue'
const msg = ref('World!')
setTimeout(() => {
msg.value = 'Vue'
}, 1000)
</script>
<template>
<h1>Hello {{ msg }}</h1>
</template>
_createElementVNode("h1", null, _toDisplayString(msg.value), 1 /* TEXT */)
runtime的render期间通过msg.value对msg产生了引用,此时产生了一个watch effect:msg的watchlist中多了一个render的watcher,在msg变化的时候 render会通过watcher重新执行
Svelte: $
编译器依赖收集 示例
suffix的值依赖name,在name变化之后,suffix值也更新
<script>
let name = 'world';
$: suffix = name + '!'
setTimeout(() => {
name = 'svelte'
}, 1000)
</script>
<h1>Hello {suffix}</h1>
// 编译后部分代码
function instance($$self, $$props, $$invalidate) {
let suffix
let name = 'world'
setTimeout(() => {
$$invalidate(1, (name = 'svelte'))
}, 1000)
// 更新关系
$$self.$$.update = () => {
if ($$self.$$.dirty & /*name*/ 2) {
$: $$invalidate(0, (suffix = name + '!'))
}
}
return [suffix, name]
}
Effect分类
React先介绍了两种典型的Effect
- 渲染逻辑中可以获取 props 和 state,并对它们进行转换,然后返回您希望在屏幕上看到的 JSX。渲染代码必须是纯的,就像数学公式一样,它只应该计算结果,而不做其他任何事情。
- 事件处理程序是嵌套在组件内部的函数,它们执行操作而不仅仅做计算。事件处理程序可以更新输入字段、提交HTTP POST请求以购买产品或将用户导航到另一个页面。它包含由用户特定操作(例如按钮点击或输入)引起的 “副作用”(它们改变程序的状态)。
Consider a ChatRoom
component that must connect to the chat server whenever it’s visible on the screen. Connecting to a server is not a pure calculation (it’s a side effect) so it can’t happen during rendering. However, there is no single particular event like a click that causes ChatRoom
to be displayed.
考虑一个ChatRoom
组件,每当它在屏幕上可见时都必须连接到聊天服务器。连接到服务器不是一个纯粹的计算(它是一个副作用),因此不能在渲染期间发生(渲染必须是纯函数)。然而,并没有单个特定的事件(如点击)会触发ChatRoom
的展示
Effects let you specify side effects that are caused by rendering itself, rather than by a particular event. Sending a message in the chat is an event because it is directly caused by the user clicking a specific button. However, setting up a server connection is an Effect because it should happen no matter which interaction caused the component to appear. Effects run at the end of a commit after the screen updates. This is a good time to synchronize the React components with some external system (like network or a third-party library).
Effect 允许指定由渲染本身引起的副作用,而不是由特定事件引起的副作用。在聊天中发送消息是一个事件,因为它直接由用户点击特定按钮引起。然而不管是任何交互触发的组件展示,_设置服务器连接_都是一个Effect。Effect会在页面更新后的commit结束时运行。这是与某个外部系统(如网络或第三方库)同步React组件的好时机
以下Effect尽量达到不重不漏,不重的意义是他们之间是相互独立的,每个模块可以独立实现,这样可以在系统设计的初期可以将业务Model建设和Effect处理分离,甚至于将Effects提取成独立的utils
渲染
生命周期
组件被初始化、更新、卸载的时候我们需要做一些业务逻辑处理,例如:组件初始化时调用接口更新数据
React
react基于自己的fiber结构,通过闭包完成状态的管理,不会建立值和渲染过程的绑定关系,通过在commit之后执行Effect达到值的状态更新等副作用操作,因此声明周期需要自己模拟实现
import { useState, useEffect } from 'react';
export default function Hello() {
const [msg, setMsg] = useState('World')
// dependency是空 因此只会在第一次执行 声明周期上可以理解为onMounted
useEffect(() => {
// 异步事件
const timer = setTimeout(() => {
// setMsg会触发重渲染 https://react.dev/learn/render-and-commit
setMsg('React')
}, 1000)
return () => {
// 卸载时/重新执行Effect前 清除异步事件
clearTimeout(timer)
}
// 如果dependency有值 则每次更新如果dependency不一样就会执行Effect
}, [])
return (
<h1>Hello { msg }</h1>
);
}
<script setup>
import { onMounted, onUnmounted, onUpdated, ref } from 'vue'
const msg = ref('Hello World!')
// 挂载
onMounted(async () => {
function getValue() {
return Promise.resolve('hello, vue')
}
const value = await getValue()
msg.value = value
})
onUpdated(() => {}) // 更新
onUnmounted(() => {}) // 卸载
</script>
<template>
<h1>{{ msg }}</h1>
<input v-model="msg">
</template>
<script>
import { onMount, onDestroy, beforeUpdate } from 'svelte'
let name = 'world'
$: suffix = name + '!'
onMount(() => {
setTimeout(() => {
name = 'svelte'
}, 1000)
})
beforeUpdate(() => {}) // 更新
onDestroy(() => {}) // 卸载/销毁
</script>
<h1>Hello {suffix}</h1>
Action 用户行为
对应React中提到的两个典型Effect中的 事件处理程序
在不考虑跳出应用(location.href='xxx'
)的情况下,我们的行为都只能改变当前应用的状态,不管是输入、选择还是触发异步事件的提交,网络相关的副作用在下节讨论
点击/输入
<!-- 原生 要求onClick是全局变量 -->
<div onclick="onClick"/>
<!-- React -->
<div onClick={onClick}/>
<!-- Vue -->
<div @click="onClick"/>
<!-- Svelte -->
<div on:click="onClick"/>
滑动输入、键盘输入等
<!-- React view和model的关系需要自己处理 -->
<input value={value} onChange={val => setValue(val)} placeholder="enter your name" />
<!-- Vue 通过指令自动建立view和model的绑定关系 -->
<input v-model="name" placeholder="enter your name" />
<!-- Svelte -->
<input bind:value={name} placeholder="enter your name" />
所谓的MVVM即是视图和模型的绑定关系通过框架(v-mode,bind:valuel)
完成,所以需要自己处理绑定关系的React不是MVVM
滚动
同上
Network 网络请求
NPM包:Axios,useSwr
Storage 存储
任何存储行为都是副作用:POST请求、变量赋值、local存储、cookie设置、URL参数设置
Remote
缓存/数据库,同上 网络请求
Local
内存
- 局部变量 闭包
React的函数式组件中的useState的值的变更
- 全局变量 window
浏览器环境初始化完成之后,我们的context中就会有window
全局变量,修改window的属性会使同一个页面环境中的所有内容都被影响(微前端的window隔离方案除外)
LocalStorage
兼容localStorage存储和 原生APP存储;返回Promise 其实也可以兼容从接口获取、存储数据
export function getItem(key) {
const now = Date.now();
if (window.XWebView) {
window.XWebView.callNative(
'JDBStoragePlugin',
'getItem',
JSON.stringify({
key,
}),
`orange_${now}`,
'-1',
);
} else {
setTimeout(() => {
window[`orange_${now}`](
JSON.stringify({
status: '0',
data: {
result: 'success',
data: localStorage.getItem(key),
},
}),
);
}, 0);
}
return new Promise((resolve, reject) => {
window[`orange_${now}`] = (result) => {
try {
const obj = JSON.parse(result);
const { status, data } = obj;
if (status === '0' && data && data.result === 'success') {
resolve(data.data);
} else {
reject(result);
}
} catch (e) {
reject(e);
}
window[`orange_${now}`] = undefined;
};
});
}
export function setItem(key, value = BABEL_CHANNEL) {
const now = Date.now();
if (window.XWebView) {
window.XWebView.callNative(
'JDBStoragePlugin',
'setItem',
JSON.stringify({
key,
value,
}),
`orange_${now}`,
'-1',
);
} else {
setTimeout(() => {
window[`orange_${now}`](
JSON.stringify({
status: '0',
data: {
result: 'success',
data: localStorage.setItem(key, value),
},
}),
);
}, 0);
}
return new Promise((resolve, reject) => {
window[`orange_${now}`] = (result) => {
console.log('MKT ~ file: storage.js:46 ~ returnnewPromise ~ result:', result);
try {
const obj = JSON.parse(result);
const { status, data } = obj;
if (status === '0' && data && data.result === 'success') {
resolve(data.data);
} else {
reject(result);
}
} catch (e) {
reject(e);
}
window[`orange_${now}`] = undefined;
};
});
}
Cookie
https://www.npmjs.com/package/js-cookie
URL
参见地址栏参数
举个栗子
组件诉求
支持分页
支持搜索
已选择的门店需要回显,但是已选择的门店只能分页获取,无法全部获取
需要知道用户移除了哪些选项,增加了哪些选项
支持服务端全选
组件Effect分析
- 业务组件可以视
load-data
为纯函数,因为loda-data
的调用不会影响外部业务组件,清晰的Effects归属可以降低业务的复杂度,最大程度上降低组件的耦合 - 用户在组件内的行为(除了确定之外)产生的Effect只对组件自身产生影响,提升了组件的内聚
组件模型设计
- 组件list兼容搜索和下拉场景
const { result: list, hasNext } = await this.loadData(param).catch(() => ({ hasNext: false, result: [] }))
const lastRemove = this.remove // 本次新增之前移除的内容
if (param.pageNo === 1 && !param.search) {
this.list = list
} else {
// 建立新值的索引 接口返回的信息是无状态属性的(选中与否)
const map = list.reduce((pre, cur) => {
pre[cur.id] = Object.assign(cur, { from: param.search })
return pre
}, {})
// 此处应该遍历list 而不是 this.list
this.list = this.list.map(item => {
const diff = map[item.id]
// 找到之前已经有的数据 就从map中移动到之前list的位置做替换
if (diff) delete map[item.id]
return diff || item
// 剩余的值补充到最后面
}).concat(Object.values(map))
}
const value = diffBy(this.last.add.concat(this.remote, this.local, this.checked), lastRemove)
this.value = value
- 接口返回选中的值通过
checked-by-remote
纯函数的依赖反转实现惰性计算 - 业务组件默认选中的值通过
checked-by-local
纯函数的依赖反转实现惰性计算 - 增加或者移除的值通过相应的diff计算出来
- Reactivity极大提升了Model的表达能力
{
computed: {
/**
* 接口返回已选中的数据且不能在已移除的数据中, 否则上次移除的数据会被自动选中
*/
remote() {
return diffBy(this.list.filter(this.checkedByRemote || emptyFilter).map(it => it.id), this.last.remove)
},
/**
* 本地默认选中 且不是从remote选中的 且不是上次选中的
*/
local() {
return diffBy(this.list.filter(this.checkedByLocal || emptyFilter).map(it => it.id), this.remote, this.last.add)
},
// 用户选择的
checked() {
return diffBy(this.value, this.remote, this.last.add, this.local)
},
// 1. 本地有接口没有的 是新增,this.value中已包含了last.add 2. 需要新增的且不在上次本地移除的范围内:上次移除的可能不在this.remote范围内
add() {
return diffBy(this.value, this.remote, this.last.remove)
},
// 1. 接口有本地没有的 是移除 2. 需要移除的 且 不在上次本地新增的范围内
remove() {
return this.last.remove.concat(diffBy(this.remote, this.value, this.last.remove))
}
},
}
参考资料
- 面向 Model 编程的前端架构设计 https://mp.weixin.qq.com/s/g4hnfirDmyeuXAdEt-zk9w
- Synchronizing with Effects https://react.dev/learn/synchronizing-with-effects
作者:京东零售 刘威
来源:京东云开发者社区 转载请注明来源
基于Effect的组件设计的更多相关文章
- SOA实践之基于服务总线的设计
在上文中,主要介绍了SOA的概念,什么叫做“服务”,“服务”应该具备哪些特性.本篇中,我将介绍SOA的一种很常见的设计实践--基于服务总线的设计. 基于服务总线的设计 基于总线的设计,借鉴了计算机内部 ...
- HTML5拓扑图形组件设计之道(一)
HT for Web(http://www.hightopo.com/guide/readme.html)提供了涵盖通用组件.2D拓扑图形组件以及3D引擎的一站式解决方案,正如Hightopo官网所表 ...
- HT图形组件设计之道(四)
在<HT图形组件设计之道(二)>我们展示了HT在2D图形矢量的数据绑定功能,这种机制不仅可用于2D图形,HT的通用组件甚至3D引擎都具备这种数据绑定机制,此篇我们将构建一个3D飞机模型,展 ...
- HT图形组件设计之道(三)
上篇我们通过定制了CPU和内存展示界面,体验了HT for Web通过定义矢量实现图形绘制与业务数据的代码解耦及绑定联动,这类案例后续文章还会继续以便大家掌握更多的矢量应用场景,本篇我们先切换个话题, ...
- HT图形组件设计之道(一)
HT for Web简称HT提供了涵盖通用组件.2D拓扑图形组件以及3D引擎的一站式解决方案,正如Hightopo官网所表达的我们希望提供:Everything you need to create ...
- xmlplus 组件设计系列之零 - xmlplus 简介
xmlplus 是什么 xmlplus 是博主写的一个 JavaScript 框架,用于快速开发前后端项目. xmlplus 基于组件设计,组件是基本的构造块.评价组件设计好坏的一个重要标准是封装度. ...
- 基于SOA的组件化业务基础平台[转]
转自https://www.ibm.com/developerworks/cn/webservices/1111_xiaojg_soa/index.html 业务基础平台是业务逻辑和基础架构平台之间的 ...
- Go/Python/Erlang编程语言对比分析及示例 基于RabbitMQ.Client组件实现RabbitMQ可复用的 ConnectionPool(连接池) 封装一个基于NLog+NLog.Mongo的日志记录工具类LogUtil 分享基于MemoryCache(内存缓存)的缓存工具类,C# B/S 、C/S项目均可以使用!
Go/Python/Erlang编程语言对比分析及示例 本文主要是介绍Go,从语言对比分析的角度切入.之所以选择与Python.Erlang对比,是因为做为高级语言,它们语言特性上有较大的相似性, ...
- 【Web技术】314- 前端组件设计原则
点击上方"前端自习课"关注,学习起来~ 译者:@没有好名字了译文:https://github.com/lightningminers/article/issues/36,http ...
- 基于Lumisoft.NET组件,使用IMAP协议收取邮件
在早期一直使用Lumisoft.NET组件来进行邮件的处理查找,对于邮件的处理非常方便,之前在随笔<基于Lumisoft.NET组件的POP3邮件接收和删除操作>中也介绍过基于POP3和S ...
随机推荐
- [ARM 汇编]高级部分—性能优化与调试—3.4.3 使用模拟器进行调试与测试
在ARM汇编程序开发过程中,使用模拟器(emulator)进行调试和测试是一种非常有效的方法.模拟器可以在不同的处理器上测试代码,帮助我们发现潜在的问题,并提供丰富的调试功能.本节将介绍如何使用QEM ...
- 自然语言处理 Paddle NLP - 任务式对话系统-理论
什么是任务型对话: 任务型:用于帮助用户完成某领域的特定任务,例如订餐.查天气.订票等 闲聊型:也称作开放域对话系统,目标是让用户持续的参与到交互过程,提供情感陪伴 问答型:提供知识满足,具体类型比较 ...
- 理解ffmpeg
ffmpeg是一个完整的.跨平台的音频和视频录制.转换和流媒体解决方案. 它的官网:https://ffmpeg.org/ 这里有一份中文的文档:https://ffmpeg.p2hp.com/ ff ...
- 【VUE】 文件预览
[VUE] 文件预览 上传前预览 word文档:docx.doc 核心代码 import {renderAsync} from "docx-preview"; /** * 渲染do ...
- 【Python】数据可视化利器PyCharts在测试工作中的应用
PyCharts 简介 PyCharts 是一个基于 Python 的数据可视化库,它支持多种图表类型,如折线图.柱状图.饼图等.PyCharts 提供了简洁的 API,使得用户能够轻松地创建各种图表 ...
- [TSG开发日志4]算法组件、个人编写的库文件如何封装成DLL,如何更好地对接软件开发?
写在前面 这个内容确实是我有点疏忽了,我以为做算法的同事应该多少对这方面会有点了解的.但是我想了一下我刚毕业的时候,确实对这方面的理解不深,查了很多资料才勉强搞懂什么意思,也是后来随着工程学习的愈加深 ...
- 【转载】Linux虚拟化KVM-Qemu分析(二)之ARMv8虚拟化
原文链接: 作者:LoyenWang 出处:https://www.cnblogs.com/LoyenWang/ 公众号:LoyenWang 版权:本文版权归作者和博客园共有 转载:欢迎转载,但未经作 ...
- 记一次使用pagehelper的坑(返回的total和size每页条数一致的问题)
问题描述 众所周知,pagehelper使用时应该在dao查询语句的前一句加上PageHelper.startPage,所以标题的问题由此引出-- 原因 PageHelper.startPage使用后 ...
- python中的注释noqa: F401
在Python中,"noqa: F401" 是一个特殊的注释指示.它主要用于在静态代码检查工具(例如Flake8)运行时,告知工具忽略特定的 "F401" 错误 ...
- 浏览器端模块化方式es module详解
在es module出现之前还有社区推出amd和cmd的规范,这两者还有其特定的编写方式,使用起来不算很方便.es module被官方推出来就成为了浏览器端实现模块化的一个很好的方案. 想要在浏览 ...