基于Effect的组件设计 | 京东云技术团队

news2024/9/21 14:51:01

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

参见地址栏参数

举个栗子🌰

组件诉求

  1. 支持分页

  2. 支持搜索

  3. 已选择的门店需要回显,但是已选择的门店只能分页获取,无法全部获取

  4. 需要知道用户移除了哪些选项,增加了哪些选项

  5. 支持服务端全选

组件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

作者:京东零售 刘威
来源:京东云开发者社区 转载请注明来源

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1080571.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

使用Perl脚本编写爬虫程序的一些技术问题解答

网络爬虫是一种强大的工具&#xff0c;用于从互联网上收集和提取数据。Perl 作为一种功能强大的脚本语言&#xff0c;提供了丰富的工具和库&#xff0c;使得编写的爬虫程序变得简单而灵活。在使用的过程中大家会遇到一些问题&#xff0c;本文将通过问答方式&#xff0c;解答一些…

循环神经网络时间序列预测

循环神经网络时间序列预测 循环神经网络用于时间序列预测比较好&#xff0c;先使用股票价格数据集来理解和掌握该算法。 数据来源 数据要先在Tushare注册登录一下&#xff0c;获取足够的积分 注册完之后有100积分&#xff0c;然后个人信息完善一下获取20积分&#xff0c;然后…

网络通信三要素

三要素概述 IP地址&#xff1a;设备在网络中的地址&#xff0c;是唯一的标识。 端口&#xff1a;应用程序在设备中唯一的标识。 协议: 数据在网络中传输的规则&#xff0c;常见的协议有UDP协议和TCP协议。 网络通信过程 A程序通过IP和端口找到B程序&#xff0c;再互相发…

沁恒CH582触摸功耗测试 BleTouchKey

准备用沁恒的CH582 做个低成本的蓝牙锁控&#xff0c;正好ch582 支持14通道的触摸&#xff0c;可以节省一个触摸芯片&#xff0c;测试了下功耗&#xff0c;具体如下 1、在BleTouchKey_Demo中 注释掉tmos_set_event( TouchKey_TaskID, NOTIFY_DATA_EVT); 2、把打印log 给屏蔽掉…

map合并数据

printNodeProperties(node, data, type) {if (type preview) {console.log(333)this.selectedNodes [] // 清空之前收集的节点this.filteredData.forEach((rootNode) > {this.collectPreviewNodes(rootNode) // 递归收集节点})// 所有具有预览功能的节点console.log(具有 …

Gradle安装

文章目录 官网配置环境变量 官网 Gradle官网&#xff1a;https://gradle.org/ Gradle官方下载安装教程页面&#xff1a;https://gradle.org/install/ Gradle官方用户手册&#xff1a;https://docs.gradle.org/current/userguide/userguide.html 注意&#xff1a; 1.SpringBoo…

华为数通方向HCIP-DataCom H12-831题库(多选题:221-240)

第221题 以下哪些项能被正则表达式^30.成功匹配? A、200 100 300 B、100 200 300 C、300 200 100 D、300 100 200 答案:CD 解析: 30.其中的“点”表示的是任何的一个数字,表示的是as-path的开头;所以以300开头的都是满足题目需求的。 第222题 以下哪些项的Community属性能…

Jetson Orin NX 开发指南(3): 安装 ROS 系统

一、前言 由于本专栏主要介绍如何将 Jetson Orin NX 作为自主无人机的机载电脑&#xff0c;因此需要 ROS 操作系统来实现各个模块的衔接&#xff0c;ROS支持在Linux系统上安装部署&#xff0c;因为ROS与Ubuntu兼容性最好&#xff0c;使用它的首选开发平台是Ubuntu&#xff0c;…

YOLOv7改进: 多分支卷积模块RFB,扩大感受野提升小目标检测精度

💡💡💡本文属于原创独家改进:多分支卷积模块RFB,使用不同的卷积核,设计了多分支的conv、pooling操作(makes use of multi-branch pooling with varying kernels),扩大感受野提升小目标检测精度 RFB | 亲测在多个数据集实现涨点,对小目标效果佳; 收录: YOLO…

spy++ 用法

spy 用法 win10 64位系统 VC6 里面的SPYXX.EXE D:\safe_software\VC\vc6\VC6CN\COMMON\TOOLS\SPYXX.EXE 把那个红色框住的地方 &#xff0c;拖动到 要监控的软件上面 点击OK 0000243F 9279 0111 Posted WM_COMMAND 另外一个版本的&#xff0c;不好用

批量给每一段文字 段落加上符号

批量给每一段文字 段落加上符号 例如 要给下面的内容统一加上[xxxxxxxx] 查找替换–>查找内容 &#xff1a;输入前面统一的标识 替换为&#xff1a;–>特殊格式–>查找内容–>全部替换 先在前面加符号 在后面加符号&#xff1a; 最后效果&#xff1a;

CPU vs GPU 的区别详解

今天我们将讨论 CPU 和 GPU 之间的区别&#xff0c;尽管两者用途相似&#xff0c;其应用领域却有所不同。CPU 更多用于传统桌面处理&#xff0c;而 GPU 的运算能力更多用于其他领域。以下我们将探讨其中的几个领域&#xff0c;以及 CPU 与 GPU 之间的一些关键区别。 为何需要两…

Xception:使用Tensorflow从头开始实现

一、说明 近年来&#xff0c;卷积神经网络已成为计算机视觉领域的主要算法&#xff0c;开发设计它们的方法一直是相当的关注。Inception模型似乎能够用更少的参数学习更丰富的表示。它们是如何工作的&#xff0c;以及它们与常规卷积有何不同&#xff1f;本文将用tensorflow实现…

Linux 网络编程 tcp server 笔记

一、TCP 服务器的创建 在 Linux 上创建一个简单的 tcp 服务器步骤如下&#xff1a; ①创建套接字 ②将套接字绑定到 IP 地址和端口号 ③监听来自客户端的连接 ④接受连接并创建新的套接字用于与客户端通信 ⑤通过新建的套接字发送和接收数据 ⑥关闭套接字 流程框图如下&#xf…

AI 法律助手与原创奇幻小说生成 | 开源专题 No.33

eosphoros-ai/DB-GPT Stars: 6.4k License: MIT DB-GPT是一个实验性的开源项目&#xff0c;利用本地化的GPT大型模型与数据和环境进行交互&#xff0c;旨在提供私密性和安全性的数据处理解决方案。 主要功能: SQL语言能力SQL生成和诊断私有领域问答和数据处理知识管理&#x…

AIGC Excel办公应用实现行与列交叉多条件求和

这样的一个多条件求和函数的应用,我们可以使用ChatGPT,OpenAI,人工智能来实现。 Prompt提示词: 有一个表格A3至A186为部门,B3至B186为月份,D3至D186为费用,请根据G2单元格的月份与F3的部门汇总费用,写出Excel函数公式,并且加上绝对引用与相对引用,可以向右填充与向…

在北欧设计中,有种轻奢叫石膏线。福州中宅装饰,福州装修

很多人在追求精致的路上&#xff0c; 大都选择一个喜欢的城市&#xff0c; 小到睫毛也要刷出太阳花。 在家装中&#xff0c;我们也追求更精致的家居生活。 现如今石膏线的运用也很广泛 有一个不起眼的东西&#xff0c; 它对于家装的精致感提升&#xff0c; 起到了画龙点…

Echarts图例图标和文字居中

前言&#xff1a;设置了verticalAlign:"middle"&#xff0c;lineHeight&#xff0c;效果没有生效&#xff0c;然后通过设置文本的padding&#xff0c;实现居中。 代码实现&#xff1a;

FPGA面试题(3)

一.FPGA和CPLD区别 FPGA&#xff1a;现场可编程门阵列CPLD&#xff1a;复杂可编程逻辑器件 二.多位异步信号如何同步 单比特异步信号 慢时钟域->快时钟域&#xff1a;同步打拍快时钟域->慢时钟域&#xff1a;先拓展位宽再同步打拍 多比特异步信号 1.异步FIFO2.保持…

vs code 添加vue3代码模板方法

最终效果 vs code 添加vue文件模板用于通过简写自动生成代码 操作步骤如下 1.找到vue模板代码编写入口 2.修改模板内容 2.1 vue.json内容 {// Place your snippets for vue here. Each snippet is defined under a snippet name and has a prefix, body and// description. T…