前言
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)
发表回复