前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >依赖追踪?Signal?如果你想要,React 中也能实现

依赖追踪?Signal?如果你想要,React 中也能实现

作者头像
用户6901603
发布2024-01-17 16:02:27
1490
发布2024-01-17 16:02:27
举报
文章被收录于专栏:不知非攻不知非攻

我前面有跟大家分享过 React 的一大优势就是他对 JS 的弱侵入性。

弱侵入性一个比较显著的体现就是,当你觉得你不喜欢 React 自带的 useState、useEffect 时,你可以轻松植入你自己的开发思维。把他调整成为你喜爱的形状。

我认识的一位腾讯大佬,就干了这么一件事情,把最细粒度响应式更新,带到了 React 的生态中来,它就是 helux,它已经在腾讯内部经历过真实的商业项目实践。

现在我们就来介绍一下这个状态管理框架。

注意,它只是一个简单易上手的工具库,你只需要记住他的特性,在需要的时候翻阅文档使用即可,不要有学习压力

0、简介

helux 是一个集 atomsignal依赖追踪为一体,支持细粒度响应式更新的状态引擎,兼容所有类 react 库,包括 react18。

btw:helux是目前唯一一个将细粒度响应式更新特性带到react开发者面前的框架

架构

helux包含了core层和适配层,core层基于最快的不可变数据操作库limu构建,包含了状态,动作和副作用3大模块,我们可以把core层理解为状态引擎的核心驱动包。

基于 core 层我们继续向上构建了适配 reacthelux 包,该包对接了 react 基础钩子,实现了 atomsignal依赖追踪双向绑定细粒度响应式更新观察派生等常用功能或特性。

注意架构里的红色区域里是 react-like,强调 helux 整体架构并非与 react 强绑定,只要满足提供了图示中几个 api 的类 react 库,core 就可以秒适配并导出所有功能。

helux 是我们默认适配好 react 而发布的包体

所以除了 react 自身,helux 还适配了 preact,同时也支持和现阶段各个生态的其他框架集成使用,例如 nextjs,可查看下来各个链接体验。

  • helux-react-starter helux & react 在线示例
  • helux-preact-starter helux & preact 在线示例
  • helux-nextjs-starter helux & nextjs 示例仓库

如果想在其他类react库中使用helux,也可以参考 helux-preact-starter 示例去自行适配。

1、优势

综合上面的架构图,不难看出,helux 相比现阶段开源社区较出名的状态管理库(reduxrecoiljotaizustandmobx等)的优势较为显著:

  • 内置依赖追踪特性,基于最快的不可变 js 库limu 开发,拥有超强性能
  • atom 支持任意数据结构且自带依赖收集功能, 无需拆分很细,天然对 DDD 领域驱动设计友好
  • 内置 signal 响应机制,实现 0 hook 编码 dom 粒度或块粒度的更新
  • 内置 loading 模块,可管理所有异步任务的运行状态、并捕捉错误抛给组件、插件
  • 内置 sync 系列 api,支持双向绑定,轻松应对表单处理
  • 内置 reactive 响应式对象,支持数据变更直接驱动关联 ui 渲染
  • 内置 define 系列 api,方便对状态模块化抽象,轻松驾驭大型前端应用架构
  • 内置事件系统
  • 支持可变派生mutate derive,适用于当共享对象 a 部分节点变化需引起其他节点自动变化的场景,数据更新粒度更小
  • 支持全量派生full derive,适用于不需要对数据做细粒度更新的场景
  • 全量派生、可变派生均支持异步任务
  • 全量派生、可变派生除数据变更驱动执行外,还支持人工重新触发运行
  • 支持中间件、插件系统,可无缝对接 redux 生态相关工具库
  • 100% ts 编码,类型安全

2、落地场景

腾讯新闻 web

腾讯新闻web是一个迭代了很多年的老项目,在 7 年前就引入了 react 技术栈,采用了 csr + ssr 混合渲染架构,在实际开发过程中,很多老组件在尽可能不动代码的情况下需要共享状态,即同一个组件的多个实例状态是通用的,例如这样一个运行多年的关注按钮。

旧代码类似

代码语言:javascript
复制
function FlowButton(){
 const [state, setState] = useState({...});
 const clickButton = ()=>{
  // 逻辑略
  // setState({ isFollowed: true})
 };
}

这样一个按钮刚开始只显示一个,随着需求变化,按钮需要底部显示,或者其他排版显示时出现了一屏2个关注按钮同时存在,这时候旧代码面临着需要状态提升的问题,在改造老代码时尤为慎重,故如何已最小的代价完成状态共享,早点下班回家才是我们想要达成的目标。

为了不动原有代码,我们以useState作为切入点,接入heluxuseShared 将其替换掉,就完成了我们需要最小代价共享状态的目的。

注:useShared 是 v2 版本提供的接口,v3 已命名为 useAtom

代码语言:javascript
复制
import React from 'react';
+ import { createShared, useShared } from 'helux';
+ const { state: sharedObj } = createShared({a:100, b:2});

function HelloHelux(props: any) {
-  const [state, setState] = React.useState({ a: 100, b: 2 });
+  const [state, setState] = useShared(sharedObj);
   return <div>{state.a}</div>; // 当前组件仅依赖a变更才触发重渲染
}

腾讯新闻运营平台

C 端对包体大小敏感,故使用了的是裁剪了大量功能,只关注状态共享的 v2 版本(gzip 后 2kb),在对内使用的运营平台上,则可以放开手脚,尽一切可能提高开发体验和运行效率,故在 >=v3 版本后 helux 基于 limu 继续构建完全颠覆了传统开发模式的新版本。

在构建新版本 helux 的同时,还引入了工具链无关的微模块技术hel-micro 搭建了一套全新开发模式的 react 微前端架构的运营平台。

基于 helux + hel-micro 构建的基于微模块的 react 微前端元框架 helra 也将在上半年开源出去,敬请期待。

在这个模式下,我们可以精选化的管理动态模块资源,做到面向不同场景灵活组合出定制的应用(例如灰度、按地域放量、按分支提供某个子应用的测试链接等)。

继续结合公司的 ci&cd 体系可做到全生命周期的模块管控流程闭环(开发、部署、上线、运维)。

其他

其他内外部小伙伴也在使用中的项目,这里就不再一一提及,除此之外,也有其他大佬积极共建生态,贡献了面向特定场景的封装库,例如面向表单的speed-form

3、特性一览

我们先了解如何快速开始,然后简单介绍各个重磅特性,包含 atomsignal依赖追踪双向绑定细粒度响应式更新观察派生等特性,同时建议访问官网文档了解更多并体验,每一个 api helux 都提供了保姆级的配套 demo 代码和渲染好的可演示组件。

定义 atom

支持定义任意数据结构 atom 对象,被包装为{val:T}结构

代码语言:javascript
复制
import { atom } from 'helux';

// 原始类型 atom
const [numAtom] = atom(1);
// 字典对象类型 atom
const [objAtom] = atom({ a: 1, b: { b1: 1 } });

修改 atom

原始值修改

代码语言:javascript
复制
const [numAtom, setAtom] = atom(1);
setAtom(100);

字典对象修改,基于 setAtom 接口回调里的草稿对象直接修改即可

代码语言:javascript
复制
const [numAtom, setAtom] = atom({ a: 1, b: { b1: 1 } });
setAtom((draft) => {
  // draft 已拆箱 { val: T } 为 T
  draft.b.b1 += 1;
});

或基于 reactive 响应式对象修改,数据变更在下一次事件循环微任务开始前被提交。

代码语言:javascript
复制
const [numAtom, setAtom, {reactive}] = atom({ a: 1, b: { b1: 1 } });
function change(){
  reactive.b.b1 += 1;
}

或定义 action 修改

代码语言:javascript
复制
const [numAtom, setAtom, { action, defineActions }] = atom({ a: 1, b: { b1: 1 } });
// 方式1:裸写 action
const change = action()(({draft})=>{
 draft.b.b1 += 1;
}, 'change');
change(); // 触发变更

// 方式2:调用可读性更友好的 defineActions
const { actions } = defineActions()({
  change({draft}){
    draft.b.b1 += 1;
  },
  // 可以继续定义其他 action
});
actions.change(); // 触发变更

观察 atom

可观察整个根对象变化,也可以观察部分节点变化

代码语言:javascript
复制
import { atom, watch, getSnap } from 'helux';

watch(
  () => {
    console.log(`change from ${getSnap(numAtom).val} to ${numAtom.val}`);
  },
  () => [atom],
);

watch(
  () => {
    console.log(
      `change from ${getSnap(numAtom).val.b.b1} to ${numAtom.val.b.b1}`,
    );
  },
  () => [objAtom.val.b.b1],
);

派生 atom

1、全量派生

derive 接口接受一个派生函数实现,返回一个全新的派生值对象,该对象是一个只可读的稳定引用,全局使用可总是读取到最新值。

代码语言:javascript
复制
import { atom, derive } from 'helux';

const [numAtom, setAtom] = atom(1);
const plus100 = derive(() => atom.val + 100);

setAtom(100);
console.log(plus100); // { val: 200 }

setAtom(100); // 设置相同结果,派生函数不会再次执行

使用已派生结果继续派生新的结果

代码语言:javascript
复制
const plus100 = derive(() => atom.val + 100);
const plus200 = derive(() => plus100.val + 200);

2、可变派生

当共享对象 a 的发生变化后需要自动引起共享状态 b 的某些节点变化时,可定义 mutate 函数来完成这种变化的连锁反应关系,对数据做最小粒度的更新

代码语言:javascript
复制
import { atom, derive } from 'helux';

const  [ objAtom1, setAtom ] = atom({a:1,b:{b1:1}});

const [objAtom2] = atom(
  { plusA100: 0 }
  {
    // 当 objAtom1.val.a 变化时,重计算 plusA100 节点的值
    mutate: {
      changePlusA100: (draft) => draft.plusA100 = objAtom1.val.a + 100,
    }
  },
);

setAtom(draft=>{ draft.a=100 });
console.log(objAtom2.val.plusA100); // 200

使用 atom

react 组件通过 useAtom 钩子可使用 atom 共享对象,该钩子返回一个元组,使用方式和 react.useState 类似,区别在于对于非原始对象,回调提供草稿供用户直接修改,内部会生成结构化共享的新状态

代码语言:javascript
复制
import { atom, useAtom } from 'helux';
const [numAtom] = atom(1);

export default function Demo() {
  // 返回结果自动拆箱
  const [num, setAtom] = useAtom(numAtom);
  return <h1 onClick={() => setAtom(Math.random())}>{num}</h1>;
}

atom 对象天然是全局共享的,可将 atom 对象提供给多个组件实例使用

代码语言:javascript
复制
import { atom, useAtom } from 'helux';
const [objAtom, setAtom] = atom({ name: 'hello helux', info: { age: 1 } });

function Demo() {
  const [obj, setAtom] = useAtom(objAtom);
  const changeName = () =>
    setAtom((draft) => {
      draft.info.age += 1;
    });

  return (
    <h1 onClick={() => setAtom(Math.random())}>
      {obj.name} {obj.info.age}
      <button onClick={changeName}>changeName</button>
    </h1>
  );
}

export default () => (
  <>
    <Demo />
    <Demo />
  </>
);

Signal

signal 响应机制允许用户跳过 useAtom 直接将数据绑定到视图,实现 0 hook 编码、dom 粒度块粒度更新。

dom 粒度更新

使用$符号绑定单个原始值创建信号响应块,实现 dom 粒度更新

代码语言:javascript
复制
import { $ } from 'helux';

// 数据变更仅触发 $符号区域内重渲染
<h1>{$(numAtom)}</h1> // 包含原始值的atom可安全绑定
<h1>{$(shared.b.b1)}</h1>// 对象型需自己取到原始值绑定

块粒度更新

使用block绑定多个原始值创建局部响应块,实现块粒度更新

代码语言:javascript
复制
// UserBlock 已被 memo
const UserBlock = block(() => (
  <div>
    name: {user.name}
    desc: {user.detail.desc}
  </div>
));

// 其他地方使用 UserBlock
<UserBlock />;

依赖追踪

除了对$block 这些静态节点建立起视图对数据变化的依赖关系,使用 useAtom 方式的组件渲染期间将实时收集到数据依赖

依赖收集

组件时读取数据节点值时就产生了依赖,这些依赖被收集到 helux 内部为每个组件创建的实例上下文里暂存着,作为更新凭据来使用。

helux 内部默认的收集深度为 6,可自己按需调节。

代码语言:javascript
复制
const { state, setDraft, useState } = atomx({ a: 1, b: { b1: 1 } });

// 修改草稿,生成具有数据结构共享的新状态,当前修改只会触发 Demo1 组件渲染
const changeObj = () => setDraft((draft) => (draft.a = Math.random()));

function Demo1() {
  const [obj] = useState();
  // 仅当 obj.a 发生变化时才触发重渲染
  return <h1>{obj.a}</h1>;
}

function Demo2() {
  const [obj] = useState();
  // 仅当 obj.b.b1 发生变化时才触发重渲染
  return <h1>{obj.b.b1}</h1>;
}

依赖变更

存在 if 条件时,每一轮渲染期间收集的依赖将实时发生变化

代码语言:javascript
复制
import { atomx } from 'helux';

const { state, setDraft, useState } = atomx({ a: 1, b: { b1: 1 } });
const changeA = () => setDraft((draft) => (draft.a += 1));
const changeB = () => setDraft((draft) => (draft.a.b1 += 1));

function Demo1() {
  const [obj] = useState();
  // 大于 3 时,依赖为 a, b.b1
  if (obj.a > 3) {
    return (
      <h1>
        {obj.a} - {obj.b.b1}
      </h1>
    );
  }

  return <h1>{obj.a}</h1>;
}

依赖比较

得益于limu产生的结构共享数据,helux 内部可以高效的比较快照变更部分,当用户重复设置相同的值组件将不被渲染

代码语言:javascript
复制
import { atomx } from 'helux';

const { state, setDraft, useState } = atomx({
  a: 1,
  b: { b1: { b2: 1, ok: true } },
});
const changeB1 = () => setDraft((draft) => (draft.b.b1 = { ...draft.b.b1 }));
const changeB1_Ok_oldValue = () =>
  setDraft((draft) => (draft.b.b1.ok = draft.b.b1.ok));
const changeB1_Ok_newValue = () =>
  setDraft((draft) => (draft.b.b1.ok = !draft.b.b1.ok));

// 调用 changeB1_Ok_oldValue changeB1 Demo1 不会被重渲染
// 调用 changeB1_Ok_newValue ,Demo1 被重渲染
function Demo1() {
  const [obj] = useState();
  return <h1>obj.b.b1.ok {`${obj.b.b1.ok}`}</h1>;
}

响应式

atom 返回的 state 是只可读数据,变更必须配合setState,同时 atom 也提供响应式对象,可直接操作修改,变化部分数据会在下一次事件循环微任务开始前执行

直接修改

代码语言:javascript
复制
import { atom } from 'helux';
import { delay } from '@helux/demo-utils';

// reactive 已自动拆箱
const { state, reactive } = atomx({ a: 1, b: { b1: { b2: 1, ok: true } } });

async function change() {
  reactive.a = 100;
  console.log(state.val.a); // 1
  await delay(1);
  console.log(state.val.a); // 100
}

组件中使用

组件中可使用 useReactive 钩子来获得响应式对象

代码语言:javascript
复制
import { sharex } from 'helux';

const { reactive, useReactive } = sharex({
  a: 1,
  b: { b1: { b2: 1, ok: true } },
});

// 定时修改 a b2
setTimeout(() => {
  reactive.a += 1;
  reactive.b.b1.b2 += 1;
}, 2000);

// 组件外部修改 ok
function toogleOkOut() {
  reactive.b.b1.ok = !reactive.b.b1.ok;
}

function Demo() {
  const [reactive] = useReactive();
  return <h1>{reactive.a}</h1>;
}
function Demo2() {
  const [reactive] = useReactive();
  return <h1>{reactive.b.b1.b2}</h1>;
}
function Demo3() {
  const [reactive] = useReactive();
  // 组件内部切换 ok
  const toogle = () => (reactive.b.b1.ok = !reactive.b.b1.ok);
  return <h1>{`${reactive.b.b1.ok}`}</h1>;
}

signal 中使用

可直接将 reactive 值传给 $ 原始值响应或 block 块响应

代码语言:javascript
复制
import { $, block, sharex } from 'helux';

const { reactive } = sharex({
  a: 1,
  b: { b1: { b2: 1, ok: true } },
});

function InSignalZone() {
  return <h1>{$(reactive.a)}</h1>;
}

const InBlockZone = block(() => {
  return (
    <div>
      <h3>{reactive.a}</h3>
      <h3>{reactive.b.b1.b2}</h3>
    </div>
  );
});

主动 flush

input 组件实时输入过程中,需主动调用 flush 接口刷新状态,避免中文输入法出现中文无法提示的问题。

代码语言:javascript
复制
import { sharex } from 'helux';
const { reactive, useState, flush } = sharex({ str: '' });
function change(e) {
  reactive.str = e.target.value;
  // 去掉 flush 调用,中文输入法无法录入汉字
  flush();
}

双向绑定

提供 syncersync 函数生成数据同步器,可直接绑定到表达相关 onChange 事件,同步器会自动提取事件值并修改共享状态,达到双向绑定的效果!

浅层数据绑定

只有一层 json path 的对象,可以使用 syncer 生成数据同步器来绑定

代码语言:javascript
复制
const { syncer, state } = sharex({ a: 1, b: { b1: 1 }, c: true });

<input value={state.a} onChange={syncer.a} />;
<input type="checkbox" checked={state.c} onChange={syncer.c} />;

syncer 会自动分析是否是事件对象,是就提取值不是就直接传值,所以也可以很方便的绑定 ui 组件库

代码语言:javascript
复制
import { Select } from 'antd';

<Select value={state.a} onChange={syncer.a} />;

原始值 atom 绑定时,传递 syncer 自身即可

代码语言:javascript
复制
const { syncer, useState } = atomx('');

function Demo1() {
  const [state] = useState();
  return <input value={state} onChange={syncer} />;
}

深层数据绑定

多层 json path 的对象,使用 sync 生成数据同步器来绑定,可通过回调设定绑定节点

代码语言:javascript
复制
// 数据自动同步到 to.b.b1 下
<input value={state.b.b1} onChange={sync((to) => to.b.b1)} />

可传递路径字符串数组定义绑定目标节点

代码语言:javascript
复制
<input value={state.b.b1} onChange={sync(['b', 'b1'])} />

拦截修改

sync 函数提供 before 回调给用户,支持数据提交前做二次修改

代码语言:javascript
复制
<input
  value={num}
  onChange={sync(
    (to) => to.b.b1,
    (val) => {
      return val === '888' ? 'boom' : val;
    },
  )}
/>

支持 before 回调里修改其他值

代码语言:javascript
复制
<input
  value={num}
  onChange={sync(
    (to) => to.b.b1,
    (val, params) => {
      if (val === '888') {
        params.draft.b2 = 'b2 changed';
        return 'boom';
      }
    },
  )}
/>

派生

支持全量派生可变派生,两种派生都支持定义异步计算任务,执行时间除了观察数据变化自执行以外,均可人工触发。

全量派生

derive 接口该接受一个派生函数实现,返回一个全新的派生值对象,该对象是一个只可读的稳定引用,全局使用可总是读取到最新值。

代码语言:javascript
复制
import { atom, derive } from 'helux';

const [numAtom] = atom(5);
const [info] = share({
  a: 50,
  c: { c1: 100, c2: 1000 },
  list: [{ name: 'one', age: 1 }],
});

// 仅在 numAtom.val 或 info.c.c1 发生变化后才会重运行计算出新的 result
const result = derive(() => {
  return numAtom.val + info.c.c1;
});

// 定义异步全量派生
const resultAsync = derive({
  fn: ()=>0, // 初始值
  deps: ()=>[numAtom.val, info.c.c1], // 依赖函数,返回值会透传给 input
  task: async({ input }){
   await delay(1000);
 return input[0] + input[1];
  },
});

可变派生

由于 atomshare 返回的对象天生自带依赖追踪特性,当共享对象 a 的发生变化后需要自动引起共享状态 b 的某些节点变化时,可定义 mutate 函数来完成这种变化的连锁反应关系,对数据做最小粒度的更新

代码语言:javascript
复制
import { atom, share, mutate } from 'helux';

const [baseAtom] = atom(1);
const [numAtom] = atom(3000);

// baseAtom 变化计算 numAtom
mutate(numAtom)({
  fn: (draft) => (draft.val = baseAtom.val + 100),
  desc: 'mutateNumAtomVal',
});

// 定义异步可变派生
mutate(numAtom)({
  deps: ()=>[baseAtom.val], // 依赖函数,返回值会透传给 input
  task: async({draftRoot, input}){
     await delay(1000);
     draftRoot.val = input[0] + 100; // 直接修改 draft
  },
  desc: 'mutateNumAtomVal',
});

观察

helux 在内部为实现更智能的自动观察变化做了大量优化工作,同时也暴露了相关接口支持用户在一些特殊场景做人工的观察变化。

watch

使用watch可观察 atom 对象自身变化或任意多个子节点的变化。

观察函数立即执行,首次执行时收集到相关依赖

代码语言:javascript
复制
import { share, watch, getSnap } from 'helux';

const [priceState, setPrice] = share({ a: 1 });

watch(
  () => {
    // 首次执行日志如下
    // price change from 1 to 1
    //
    // 反复调用 changePrice,日志变化如下
    // price change from 1 to 101
    // price change from 101 to 201
    console.log(
      `price change from ${getSnap(priceState).a} to ${priceState.a}`,
    );
  },
  { immediate: true },
);

const changePrice = () =>
  setPrice((draft) => {
    draft.a += 100;
  });

观察函数不立即执行,通过 deps 函数定义需要观察的数据,观察的粒度可以任意定制

代码语言:javascript
复制
const [priceState, setPrice] = share({ a: 1 });
const [numAtom, setNum] = atom(3000);

// 观察 priceState.a 的变化
watch(
  () => {
    console.log(`found price.a changed: () => [priceState.a]`);
  },
  () => [priceState.a],
  // 或写为
  // { deps: () => [priceState.a] }
);

// 观察整个 priceState 的变化
watch(
  () => {
    console.log(`found price changed: [ priceState ]`);
  },
  () => [priceState],
);

// 观察整个 priceState 和 numAtom 的变化
watch(
  () => {
    console.log(`found price or numAtom changed: ()=>[ priceState, numAtom ]`);
  },
  () => [priceState, numAtom],
);

即设置依赖函数也设置立即执行,此时的依赖由 depswatch 共同收集到并合并而得。

代码语言:javascript
复制
watch(
  () => {
    const { a } = priceState;
    console.log(`found one of them changed: [ priceState.a, numAtom ]`);
  },
  { deps: () => [numAtom], immediate: true },
);

watchEffect

watchEffect 回调会立即执行,自动对首次运行时函数内读取到的值完成变化监听

代码语言:javascript
复制
import { watchEffect, getSnap } from ' helux ';
const [priceState, setPrice] = share({ a: 1 });

// 观察 priceState.a 的变化
watchEffect(() => {
  console.log(`found price.a changed from ${getSnap(priceState).a} to ${priceState.a}`);
});

useWatch

提供 useWatch 让开发者在组件内部观察变化

代码语言:javascript
复制
import { getSnap, share, useWatch } from 'helux';
const [priceState, setPrice] = share({ a: 1 });

function changeA() {
  setPrice((draft) => void (draft.a += 1));
}

function Comp(props: any) {
  const [tip, setTip] = React.useState('');
  // watch 回调随组件销毁会自动取消监听
  useWatch(
    () => {
      setTip(
        `priceState.a changed from ${getSnap(priceState).a} to ${priceState.a}`,
      );
    },
    () => [priceState.a],
  );

  return <h1>watch tip: {tip}</h1>;
}

useWatch 无闭包陷阱问题,总能感知闭包外的最新值

代码语言:javascript
复制
import { $, share, useObject, useWatch } from 'helux';
const [priceState, setPrice] = share({ a: 1 });
function changeA() {
  setPrice((draft) => void (draft.a += 1));
}

function Comp(props: any) {
  const [obj, setObj] = useObject({ num: 1 });
  const [tip, setTip] = React.useState('');

  useWatch(
    () => {
      // priceState.a changed, here can read the latest num
      setTip(`num in watch cb is ${obj.num}`);
    },
    () => [priceState.a],
  );
}

useWatchEffect

在组件中使用 useWatchEffect 来完成状态变化监听,会在组件销毁时自动取消监听。

useWatchEffect 功能同 watchEffect``一样,区别在于 useWatchEffect` 会立即执行回调,自动对首次运行时函数内读取到的值完成变化监听。

代码语言:javascript
复制
import { share, useMutable, useWatchEffect, getSnap } from 'helux';

const [priceState, setState, ctx] = share({ a: 1, b: { b1: { b2: 200 } } });

function changeA() {
  setState((draft) => {
    draft.a += 1;
  });
}

export default function Comp(props: any) {
  const [ state, setState] = useMutable({tip:'1'})
  useWatchEffect(() => {
    // 自动收集到 priceState.a 依赖
    setState(draft=>{
      draft.tip = `priceState.a changed from ${getSnap(priceState).a} to ${priceState.a}`;
    });
  });
}

模块化

尽管 atom 共享上下文提供了 actionderivemutateuserStateuserActionLoadinguserMutateLoading 等一系列 api 方便用户使用各项功能,但这些 api 比较零碎,处理大型前端应用时用户更希望面向领域模型对状态的 statederiveaction 建模,故共享上下文还提供 define 系列 api 来轻松驾驭此类场景。

为了开发者工具能够查看模块化相关变更动作记录,配置 moduleName 即可

defineActions

批量定义状态对应的修改函数,返回 { actions, eActions, getLoading, useLoading, useLoadingInfo }, 组件中可通过 useLoading 读取异步函数的执行中状态 loading、是否正常执行结束 ok、以及执行出现的错误 err, 其他地方可通过 getLoading 获取

代码语言:javascript
复制
// 【可选】约束各个函数入参 payload 类型
type Payloads = {
  changeA1: number;
  foo: boolean | undefined;
  // 不强制要求为每一个action key 都定义 payload 类型约束,但为了可维护性建议都补上
};

// 不约束 payloads 类型时写为 ctx.defineActions()({ ... });
const { actions, eActions, useLoading, getLoading } =
  ctx.defineActions<Payloads>()({
    // 同步 action,直接修改草稿
    changeA1({ draft, payload }) {
      draft.a.b.c += payload;
    },
    // 同步 action,返回结果
    changeA2({ draft, payload }) {
      draft.a.b.c += payload;
      return true;
    },
    // 同步 action,直接修改草稿深节点数据,使用 merge 修改浅节点数据
    changeA3({ draft, payload, merge }) {
      draft.a.b.c += payload;
      merge({ c: 'new desc' }); // 等效于 draft.c = 'new desc';
      return true;
    },
    // 异步 action,直接修改草稿
    async foo1({ draft, payload }) {
      await delay(3000);
      draft.a.b.c += 1000;
    },
    // 异步 action,多次直接修改草稿,合并修改多个状态,同时返回一个结果
    async foo2({ draft, payload, merge }) {
      draft.a.b.c += 1000;
      await delay(3000); // 进入下一次事件循环触发草稿提交
      draft.a.b.c += 1000;
      await delay(3000); // 再次进入下一次事件循环触发草稿提交
      const { list, total } = await fetchList();
      merge({ list, total }); // 等价于 draft.list = list, draft.tatal = total
      return true;
    },
  });

多个 action 组合为一个新的 action

代码语言:javascript
复制
const { actions, eActions, useLoading, getLoading } =
  ctx.defineActions<Payloads>()({
    foo() {},
    bar() {},
    baz() {
      actions.foo();
      actions.bar();
    },
  });

调用 actions.xxx 执行修改动作,actions 方法调用只返回结果,如出现异常则抛出,同时也会发送给插件和伴生 loading 状态

defineFullDerive

批量定义状态对应的全量派生函数,返回结果形如 { result, helper: { [key]: runDeriveFn, runDeriveTask, useDerived, useDerivedInfo } }

代码语言:javascript
复制
type DR = {
  a: { result: number };
  c: { deps: [number, string]; result: number };
  // 不强制要求为每一个 result key 都定义 deps 返回类型约束和 result 类型约束,但为了可维护性建议都补上
};

const df = ctx.defineFullDerive<DR>()({
  a: () => priceState.a.b.c + 10000,
  b: () => priceState.a.b.c + 20000,
  c: {
    // DR['c']['result'] 将约束此处的 deps 返回类型
    deps: () => [priceState.a.b1.c1, priceState.info.name],
    fn: () => 1,
    async task(params) {
      const [c1, name] = params.input; // 获得类型提示
      await delay(2000);
      return 1 + c1;
    },
  },
});

4、结语

一直以来,支持细粒度响应式更新成为部分 react 开发者追求的特性,而支持此特性,就需要 singal 原语和依赖收集特性,本质来说这和 react 追求不可变是相互矛盾的,而 helux 则跳出常规思维,保持 react 不可变的精髓,把可变放置到另一个空间去操作,每次生成一份全新的具有结构共享特性的数据快照后,再传递给 react 即可。

注意这样去做的不只是 helux,采取开辟新空间做可变修改,再生成快照给 react 策略还有 mobx-reactvaltio,采取 atom 路线的有 recoiljotai

而同时做到 atom + signal + 依赖追踪,并支持细粒度响应式只有 helux,所以 helux 的目标是期望重定义 react 开发范式,并全面提升 react 应用的 DXUX (开发体验、用户体验),为了这个目标 helux 耕耘近 3 年(包含了初代仅支持浅层依赖追踪的 concent,到打磨不可变数据操作库 limu,再到构建 helux 整个历程),如果你也喜欢这种新的开发方式,可以在你的项目中尝试一下。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2024-01-13,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 这波能反杀 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 0、简介
  • 1、优势
  • 2、落地场景
  • 3、特性一览
  • 4、结语
相关产品与服务
消息队列 TDMQ
消息队列 TDMQ (Tencent Distributed Message Queue)是腾讯基于 Apache Pulsar 自研的一个云原生消息中间件系列,其中包含兼容Pulsar、RabbitMQ、RocketMQ 等协议的消息队列子产品,得益于其底层计算与存储分离的架构,TDMQ 具备良好的弹性伸缩以及故障恢复能力。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档