基于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 网络请求
基础:XMLHttpRequest,Fetch
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
作者:京东零售 刘威
来源:京东云开发者社区 转载请注明来源
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
OpenJDK17-JVM源码阅读-ZGC-并发标记 | 京东物流技术团队
1、ZGC简介 1.1 介绍 ZGC 是一款低延迟的垃圾回收器,是 Java 垃圾收集技术的最前沿,理解了 ZGC,那么便可以说理解了 java 最前沿的垃圾收集技术。 从 JDK11 中作为试验特性推出以来,ZGC 一直在不停地发展中。 从 JDK14 开始,ZGC 开始支持 Windows。 在 JDK15 中,ZGC 不再是实验功能,可以正式投入生产使用了。 在最新的 JDK 开源库中,已经出现了分代收集的 ZGC 代码,预计不久的将来会正式发布,到时相信 ZGC 各项表现将会更加优秀。 图1 分代收集的ZGC 如上图,JDK21 中已经有了分代 ZGC 的 Feature。 1.2 ZGC 特征 1. 低延迟 2. 大容量堆 3. 染色指针 4. 读屏障 1.3 垃圾收集阶段 图2 ZGC 运作过程 如上图,主要有以下几个阶段,初始标记、并发标记/并发重映射、并发预备重分配、初始重分配、并发重分配,本次主要分析的就是”并发标记/并发重映射“部分源代码。 1.4 带着问题去读源码 1. ZGC 是如何在指针上标记的 2. ZGC 三色标记的过程 3. ZGC 只用...
- 下一篇
微宏科技基于 KubeSphere 的微服务架构实践
作者:尹珉,KubeSphere Ambassador、contributor,KubeSphere 社区用户委员会杭州站站长。 公司简介 杭州微宏科技有限公司于 2012 年成立,专注于业务流程管理和自动化(BPM&BPA)软件研发和解决方案供应商。创始团队毕业于浙江大学、清华大学、美国 Rice 大学和 University of Texas 等海内外知名高校,曾服务于世界知名软件公司和 500 强企业。 微宏已为超过 1000 家的国内国外大中型企业和政府提供了从流程规划设计、流程运行、流程自动化、流程集成、流程挖掘的全生命周期流程软件产品和解决方案,客户分布于制造、金融、电器电子、医药、服务业、高科技和政府等十多个行业。 微宏科技是国家高新技术企业、浙江省专精特新企业,通过了 ISO9001 质量管理体系认证、CMMI 认证、ISO27001 信息安全管理体系认证。获赛迪“2021 年智能 BPM 领域最佳产品”奖、“2021-2022 业务流程管理&自动化领域优秀产品”奖、中国软件网“2021 年度智能流程平台优秀产品奖”、“2022 应龙杯最佳 BPA 业务...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- CentOS8安装Docker,最新的服务器搭配容器使用
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果
- Windows10,CentOS7,CentOS8安装Nodejs环境
- SpringBoot2初体验,简单认识spring boot2并且搭建基础工程
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- SpringBoot2更换Tomcat为Jetty,小型站点的福音
- SpringBoot2整合Thymeleaf,官方推荐html解决方案
- SpringBoot2全家桶,快速入门学习开发网站教程
- MySQL8.0.19开启GTID主从同步CentOS8