关于前端:你踩过-react-生态的-signal-坑吗且看-helux-如何应对

4次阅读

共计 7393 个字符,预计需要花费 19 分钟才能阅读完成。

前言

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

本文将重点介绍 react 生态里各方 signal 的具体实现,并探讨背地暗藏的坑导致的可用性问题,同时也会给出 heluxsignal实现的劣势,并教会你应用 signal 写出0 hookreact 状态组件。

signal 热潮

signal在 2023 年掀起了一股热潮,其实诸多框架很早就开始为其布局,Vue、Solid.js、Preact、Svelte 都在不同时的引入了 signal,某种程度来说 vue 甚至能够算是 signal 的鼻祖,Angular 作者发文 Signal 是前端框架的将来后,在 2023 年末的 Angular 17 里引入了 signal 实现,可见大家对 signal 是如许的渴望。

咱们察看如下 Angularsignal代码示例

import {signal, computed, effect} from '@angular/core';

export class SignalExample {count = signal(1);

  // Get (Same in Template || Typescript)
  getCount = () => this.count();

  // Setters
  reset = () => this.count.set(1);
  increment = () => this.count.update((c) => c + 1);

  // Computed
  doubled = computed(() => this.count() * 2);

  // Effects
  logCount = effect(() => console.log(this.doubled()));
}

发现放弃了 signal 里通用概念,即 状态 批改 派生 副作用 都存在,大家能很快上手,只是细节实现不一样,例如 angular 这里的批改基于 setupdate,获取通过get 函数。

接下来咱们将只针对 react 生态里的 signal 揭示可能存在的应用问题。

不知 angularsveltesolid 是否存在如下问题,欢送浏览本文的读者提出对应观点或给出示例。

signal 实质

为何前端对 signal 朝思暮想,究其原因就是它可让视图的数据依赖可锁定到最小范畴,以便实现点对点更新,即 dom 粒度的更新

既然要感知到视图的数据依赖,那么就存在依赖收集前置行为,而不是依赖比拟后置行为。

上图里的浅灰色局部的 compare deps 即 react.memo 的比拟函数

react.memo(Comp, ( prevProps, currProps)=> boolean)

红色局部的 compare deps 即当初 signal 里读取行为产生的值比拟(读取行为让框架感知到了以后视图的依赖,下一刻能够用来做比拟)

// 相似 solid 的设计
const [val, setVal] = createSignal(someVal);

// 视图调用 val()
<div>{val()}</div>

所以咱们可将 signal 看做是一种反对依赖收集的包装数据,数据的读依赖能够和各种副作用绑定起来(例如 render 函数,派生函数,察看函数等),写行为则是找到这些副作用函数再去执行一遍。

哎喂?这不就是 mobx 或者 vue 始终都在做的事么,换了个名词 signal 再来一遍 …..

react 里的 signal 三方实现

既然 signal 这么美妙,为什么 react 不原生反对,这也是很多人质问的点,react外围团队的 Andrew Clark 对此做了回答

咱们可能会在 React 中增加一个相似 Signals 的基元,但我并不认为这是一个编写 UI 代码的好办法。它对性能来说是很好的。但我更喜爱 React 的模式,在这种模式下,你每次都会伪装从新创立所有的内容。咱们的打算是应用一个编译器来实现与之相当的性能

这外面还存在一个问题就是 react 外部承受的不可变数据,以便做高效的 diff 比拟,而 signal 强调的数据数据可变,以便准确晓得变动的细节,但其实这不是妨碍在 react 里实现 signal 的外围因素,因为咱们只须要在可变 mutable 和不可变 immutable 之间搭一个桥梁,基于 mutable 库改写数据,生成快照数据喂给 react 就解决此问题了。

react 放弃克服只做本人外围的局部,是社区生态弱小的重要起因,所以很多三方库开始为 react 带来 signal 体验,这里咱们重点列举 mobx-react@preact/signals-react两个库,来看看他们存在的问题。

mobx-react

留神,如果你认可下面总结的 signal 实质的话,mobx-react也算是一种 signal 的实现

是不是很意外,原来很早很早 react 生态里就有了 signal 实现,只是过后大家还没吹热 signal 这个概念。

mobx-react和现有 signal 库的差别点在于,在函数组件大行其道的当下,任然须要显式地突出察看节点,即包装一般组件为可察看组件,再配合钩子函数能力感知变动

import {makeAutoObservable} from "mobx"
import {observer, useObservable} from "mobx-react-lite"

// 定义 store
class Store {
  count = 0

  constructor() {makeAutoObservable(this);
  }
  
  add(){this.count += 1},
}

// 实例化 store
const mySotre = new Store();

const App = observer(() => {
  // 应用 store
  const store = useObservable(mySotre);
  return <button onClick={() => ++store.count}>{store.count}</button>
});

示例参考自 mobx 官网 react 集成

这种应用体验落后于其余不须要显式突出察看,api 齐全走函数式格调的 signal 实现库。

preact/signals-react

preact 本身实现了 signal后,也公布了 @preact/signals-react 包来为 react 带来 signal 能力,然而当我认真体验后,惟一的感触是:这可能只适宜写hello world…..

咱们依照官网 demo 写进去一个简略的示例是这样的:

import {signal, computed} from "@preact/signals-react";

// 定义 signal
const count = signal(0);
// 派生 signal
const comput = computed(() => count.value * 5);
// 批改 signal
const increment = () => {count.value++;};

export default function App() {
  return (
    <div className="App">
      <h1>Count -> {count}</h1>
      <h1>Commp -> {comput}</h1>
      <button onClick={increment}>Increment</button>
    </div>
  );
}

跑起来确实没问题,然而咱们的理论开发我的项目不可能是 count 这种简略构造数据,以及加和减这种逻辑。

坑 1:没有可变批改

我略微把数据调简单一点,代码变为如下:

const user = signal({a:{b:1}, name: 'test'});
const comput = computed(() => user.v.a.b + 1);

const increment = () => {user.value.a.b++;};

这样的代码就不能失常驱动视图从新渲染,所以我必须改为

user.value.a.b++;
+ user.value = {...user.value};

坑 2: 渲染粒度是信号自身

signals-react的渲染粒度是信号自身,即信号本身扭转后,任何援用信号的组件都会被重写渲染,对于简单对象也是一样的后果,就导致可能只改了对象的局部数据节点 A,而只应用了局部数据节点 B 的组件也会被重渲染。

代码如下,每个组件能够蕴含了一个 <h3>{Date.now()}</h3>,如果变动了示意以后组件被触发重渲染了

示例代码见 App3.tsx 文件

import {signal} from "@preact/signals-react";

const person = signal({
  firstName: "John",
  lastName: "Doe",
});

function FirstName() {
  return (
    <div>
      <h1>firstName: {person.value.firstName}</h1>
      <h3>{Date.now()}</h3>
    </div>
  );
}

function LastName() {
  return (
    <div>
      <h1>lastName: {person.value.lastName}</h1>
      <h3>{Date.now()}</h3>
    </div>
  );
}

const change = (e) => {
  person.value.firstName = e.target.value;
  person.value = {...person.value};
};

export default function App() {
  return (
    <div className="App">
      <FirstName />
      <LastName />
      <input onInput={change} />
    </div>
  );
}

输出 input,渲染后果如下,咱们只批改了 firstName,后果FirstNameLastName组件均触发重渲染

坑 3: 蹩脚的细粒度更新开发体验

可是官网明明声称反对细粒度更新的啊,为什么 坑 2 的示例触发了全副应用方渲染呢,通过仔细阅读官网,发现上述例子改为真正的细粒度渲染还须要进一步拆分 signal,所以代码须要优化为signalsignal的模式

// signal 定义
const person = signal({
++  firstName: "John",
--  firstName: signal("John"),
++  lastName: "Doe",
--  lastName: signal("Doe"),
});

// 批改形式
-- const change = (e) => {
--  person.value.firstName = e.target.value;
--  person.value = {...person.value};
--};
++ const change = (e) => {
++  person.value.firstName.value = e.target.value;
++};

// 组件应用
-- <p>{person.value.firstName}</p>
++ <p>{person.value.firstName.value}</p>

写完这样一段代码后,我的心田感触是这样的

如果咱们真的在我的项目铺开来这样应用 signal 的话,将陷入 signal 拆分的陆地里,以及漫天 .value 的暴雨中 ….

helux 带你进入真正的 signal 世界

helux引入 signal 设计时,明确了要实现以下几个指标,能力让 signalreact真正可用、好用。

纯正的函数式

相比 mobx-react 应用 class 创立 store,helux彻底拥抱函数式,有更强的可组合性

import {atom, share, derive, watch, watchEffect} from 'helux';

// primitive atom
const [numAtom, setNum] = atom(1);
const result = derive(()=> numAtom.val + 1); // {val: 2}
watch(()=> console.log(`numAtom changed`), ()=>[numAtom]);
// or
watchEffect(()=> console.log(`numAtom changed ${numAtom.val}`))
setNum(100); // 驱动 derive watch watchEffect

// object atom
const [state, setState] = share({a:1, b:2});
const result = derive(()=> state.a + 1);
watch(()=> console.log(`state.a changed`), ()=>[state.a]);
// or
watchEffect(()=> console.log(`state.a ${state.a}`))
setState(draft=>{draft.a=100}); // 驱动 derive watch watchEffect

足够简略的 api

组件里应用 observer 配合 useObservable 能力驱动函数组件,只须要 useAtom 即可

function Demo1() {cosnt [ num] = useAtom(numAtom); // 主动拆箱 {val: T}
  // or useAtom(state);
  return (<div>{num}</div>
  );
}

function Demo2() {const [ state] = useAtom(state);
  return (<div>{state.a}</div>
  );
}

依赖是细粒度的,对象逐渐开展依赖就会逐渐放大

// 依赖仅仅是 state.a,只有 state.a 变动才会引起重渲染
<div>{state.a}</div>

如果不须要驱动这个组件从新渲染,只须要应用 $ 符号包含即可,此时组件都不须要useAtom,实现 0 hook 编码。

import {$} from 'helux';

function Demo(){
  // 仅 div 外部这一小块区域会重渲染
  <div>{$(state.a)}</div>
}

没有任何心智累赘的应用信号对象

咱们能够把对象构建得足够简单,也能够像应用原生 json 对象应用信号对象,不须要 signalsignal的诡异操作。

atom(()=>({
    a:1,
    b: {....}
    c : {e: { list: [ ...] } }
}));

强悍的依赖追踪性能

基于 limu 的强悍性能,能够无所畏惧的跟踪到任意深度节点的依赖,helux默认对数组只跟踪到下标地位,下标里的对象属性持续开展后的依赖就不再收集,防止超大数组产生过多的跟踪,但如果你违心破费额定的内存空间收集所有子节点依赖,调整收集策略即可实现。

例如上面这个例子里,批改数组任意一个子节点的某个子属性,只会触发指标节点更新,甚至咱们齐全克隆了一份新的 set 回去也没有触发更新,因为 helux 外部在比拟新值和上一刻的快照是否相等,相等则跳过更新。

示例见 list 依赖,本链接里还蕴含了其余简略示例

import React from "react";
import {sharex} from "helux";
import {MarkUpdate, Entry} from "./comps";

const stateFn = () => ({
  list: [{ name: "one1", age: 1},
    {name: "one2", age: 2},
    {name: "one3", age: 3},
  ],
});
// stopArrDep=false,反对数组子项依赖收集
const {useState, reactive} = sharex(stateFn, { stopArrDep: false});

// 更新其中一个子项,触发更新
function updateListItem() {reactive.list[1].name = `${Date.now()}`;
}

// 新克隆一个更新回去,不触发更新,因为具体值没变
function updateAllListItem() {reactive.list = JSON.parse(JSON.stringify(reactive.list));
}

// 新克隆一个并批改其中一个,仅触发 list[0]更新
function updateAllListItemAndChangeOne() {const list = JSON.parse(JSON.stringify(reactive.list));
  list[1].name = `${Date.now()}`;
  reactive.list = list;
}

const ListItem = React.memo(function (props: any) {
    // arrDep = false 数组本身依赖疏忽
    const [state, , info] = useState({arrDep: false});
    return (<MarkUpdate info={info}>name: {state.list[props.idx].name}</MarkUpdate>
    );
  },
  () => true);

function List() {const [state] = useState();
  return (
    <div>
      {state.list.map((item, idx) => (<ListItem key={idx} idx={idx} />
      ))}
    </div>
  );

更新成果如下,可察看色块的变动确定是否被更新

结语

其余框架的 signal 体验成果暂无验证,本文只针对 react 生态中的 signal 相干作品做比照,helux处处未体现 signal,但当你应用atom 创立进去共享转态时,处处都在收集读取行为产生的信号(derivewatchuseAtom), 这些都是内置的,你只管像操作一般 json 一样操作携带信号性能的 atom 共享状态即可,这或者才是咱们再 react 世界里须要的面向开发者编码敌对,面向用户运行高效的 signal 实现。

友链:

❤️ 你的小星星是咱们开源最大的精力能源,欢送关注以下我的项目:

helux 集 atom、signal、依赖追踪为一体,反对细粒度响应更新的状态引擎

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

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

📦 理解更多

欢送入群理解更多,因为微信探讨群号 200 人已满,需加作者微信号或 qq 群号,再邀请你如 helux & hel 探讨群(加号时记得备注 helux 或 hel)

正文完
 0