前言
helux 是一个集 atom
、signal
、 依赖追踪
为一体,反对细粒度响应式更新的状态引擎,兼容所有类 react 库,包含 react18。
本文将重点介绍 react 生态里各方 signal
的具体实现,并探讨背地暗藏的坑导致的可用性问题,同时也会给出 helux
中signal
实现的劣势,并教会你应用 signal
写出0 hook 的react
状态组件。
signal 热潮
signal在 2023 年掀起了一股热潮,其实诸多框架很早就开始为其布局,Vue、Solid.js、Preact、Svelte 都在不同时的引入了 signal,某种程度来说 vue 甚至能够算是 signal 的鼻祖,Angular 作者发文 Signal 是前端框架的将来后,在 2023 年末的 Angular 17
里引入了 signal
实现,可见大家对 signal
是如许的渴望。
咱们察看如下 Angular
的signal
代码示例
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
这里的批改基于 set
、update
,获取通过get
函数。
接下来咱们将只针对 react
生态里的 signal
揭示可能存在的应用问题。
不知
angular
、svelte
、solid
是否存在如下问题,欢送浏览本文的读者提出对应观点或给出示例。
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
,后果FirstName
和LastName
组件均触发重渲染
坑 3: 蹩脚的细粒度更新开发体验
可是官网明明声称反对细粒度更新的啊,为什么 坑 2 的示例触发了全副应用方渲染呢,通过仔细阅读官网,发现上述例子改为真正的细粒度渲染还须要进一步拆分 signal
,所以代码须要优化为signal
套signal
的模式
// 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
设计时,明确了要实现以下几个指标,能力让 signal
在react
真正可用、好用。
纯正的函数式
相比 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 对象应用信号对象,不须要 signal
叠signal
的诡异操作。
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
创立进去共享转态时,处处都在收集读取行为产生的信号(derive
,watch
,useAtom
), 这些都是内置的,你只管像操作一般 json 一样操作携带信号性能的 atom
共享状态即可,这或者才是咱们再 react
世界里须要的面向开发者编码敌对,面向用户运行高效的 signal
实现。
友链:
❤️ 你的小星星是咱们开源最大的精力能源,欢送关注以下我的项目:
helux 集 atom、signal、依赖追踪为一体,反对细粒度响应更新的状态引擎
limu 最快的不可变数据 js 操作库.
hel-micro 工具链无关的运行时模块联邦 sdk.
📦 理解更多
欢送入群理解更多,因为微信探讨群号 200 人已满,需加作者微信号或 qq 群号,再邀请你如 helux & hel
探讨群(加号时记得备注 helux 或 hel)