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

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

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

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

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

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

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

0

简介

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

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

架构

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

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

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

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

注意架构里的红色区域里是 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 混合渲染架构,在实际开发过程中,很多老组件在尽可能不动代码的情况下需要共享状态,即同一个组件的多个实例状态是通用的,例如这样一个运行多年的关注按钮。

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

旧代码类似

function FlowButton(){
 const [state, setState] = useState({...});
 const clickButton = ()=>{
  // 逻辑略
  // setState({ isFollowed: true})
 };
}

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

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

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

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 也将在上半年开源出去,敬请期待。

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

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

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

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

其他

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

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

3

特性一览

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

定义 atom

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

import { atom } from 'helux';

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

修改 atom

原始值修改

const [numAtom, setAtom] = atom(1);
setAtom(100);

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

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

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

const [numAtom, setAtom, {reactive}] = atom({ a: 1, b: { b1: 1 } });
function change(){
  reactive.b.b1 += 1;
}

或定义 action 修改

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

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

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

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); // 设置相同结果,派生函数不会再次执行

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

const plus100 = derive(() => atom.val + 100);
const plus200 = derive(() => plus100.val + 200);

2、可变派生

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

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 类似,区别在于对于非原始对象,回调提供草稿供用户直接修改,内部会生成结构化共享的新状态

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 对象提供给多个组件实例使用

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 粒度更新

import { $ } from 'helux';

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

块粒度更新

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

// UserBlock 已被 memo
const UserBlock = block(() => (
  <div>
    name: {user.name}
    desc: {user.detail.desc}
  </div>
));

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

依赖追踪

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

依赖收集

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

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

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 条件时,每一轮渲染期间收集的依赖将实时发生变化

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 内部可以高效的比较快照变更部分,当用户重复设置相同的值组件将不被渲染

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 也提供响应式对象,可直接操作修改,变化部分数据会在下一次事件循环微任务开始前执行

直接修改

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 钩子来获得响应式对象

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 块响应

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 接口刷新状态,避免中文输入法出现中文无法提示的问题。

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 生成数据同步器来绑定

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

import { Select } from 'antd';

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

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

const { syncer, useState } = atomx('');

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

深层数据绑定

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

// 数据自动同步到 to.b.b1 下
<input value={state.b.b1} onChange={sync((to) => to.b.b1)} />

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

<input value={state.b.b1} onChange={sync(['b', 'b1'])} />

拦截修改

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

<input
value={num}
onChange={sync(
(to) => to.b.b1,
(val) => {
return val === '888' ? 'boom' : val;
},
)}
/>

支持 before 回调里修改其他值

<input
value={num}
onChange={sync(
(to) => to.b.b1,
(val, params) => {
if (val === '888') {
params.draft.b2 = 'b2 changed';
return 'boom';
}
},
)}
/>

派生

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

全量派生

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

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 函数来完成这种变化的连锁反应关系,对数据做最小粒度的更新

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 对象自身变化或任意多个子节点的变化。

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

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 函数定义需要观察的数据,观察的粒度可以任意定制

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 共同收集到并合并而得。

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

watchEffect

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

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 让开发者在组件内部观察变化

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 无闭包陷阱问题,总能感知闭包外的最新值

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` 会立即执行回调,自动对首次运行时函数内读取到的值完成变化监听。

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 获取

// 【可选】约束各个函数入参 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

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 } }

type DR = {
  a: { result: number };
  c: { deps: [numberstring]; 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 整个历程),如果你也喜欢这种新的开发方式,可以在你的项目中尝试一下。

5

友链

同时也欢迎关注以下项目:

limu 最快的不可变数据js操作库.

hel-micro 工具链无关的运行时模块联邦 sdk.


原文始发于微信公众号(这波能反杀):依赖追踪?Signal?如果你想要,React 中也能实现

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/240147.html

(0)
葫芦侠五楼的头像葫芦侠五楼

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!