聊聊前端框架的未来Signals
来源 | OSCHINA 社区
作者 | jump--jump
原文链接:https://my.oschina.net/wsafight/blog/10115779
Signals 在目前前端框架的选型中遥遥领先!
国庆节前最后一周在 Code Review 新同学的 React 代码,发现他想通过 memo 和 useCallback 只渲染被修改的子组件部分。事实上该功能在 React 中是难以做到的。因为 React 状态变化后,会重新执行 render 函数。也就是在组件中调用 setState 之后,整个函数将会重新执行一次。
React 本身做不到。但是基于 Signals 的框架却不会这样,它通过自动状态绑定和依赖跟踪使得当前状态变化后仅仅只会重新执行用到该状态代码块。
个人当时没有过多的解释这个问题,只是匆匆解释了一下 React 的渲染机制。在这里做一个 Signals 的梳理。
优势
对比 React,基于 Signals 的框架状态响应粒度非常细。这里以 Solid 为例:
import { createSignal, onCleanup } from "solid-js";
const CountingComponent = () => {
// 创建一个 signal
const [count, setCount] = createSignal(0);
// 创建一个 signal
const [count2] = createSignal(666);
// 每一秒递增 1
const interval = setInterval(() => {
setCount((c) => c + 1);
}, 1000);
// 组件销毁时清除定时器
onCleanup(() => clearInterval(interval));
return (
<div>
<div>
count: {count()}
{console.log("count is", count())}
</div>
<div>
count2: {count2()}
{console.log("count2 is", count2())}
</div>
</div>
);
};
count is 0
count2 is 666
count is 1
count is 2
...
const [, forceRender] = useReducer((s) => s + 1, 0);
除了更新粒度细之外,使用 Signals 的框架心智模型也更加简单。其中最大的特点是:开发者完全不必在意状态在哪定义,也不在意对应状态在哪渲染。如下所示:
import { createSignal } from "solid-js";
// 把状态从过组件中提取出来
const [count, setCount] = createSignal(0);
const [count2] = createSignal(666);
setInterval(() => {
setCount((c) => c + 1);
}, 1000);
// 子组件依然可以使用 count 函数
const SubCountingComponent = () => {
return <div>{count()}</div>;
};
const CountingComponent = () => {
return (
<div>
<div>
count: {count()}
{console.log("count is", count())}
</div>
<div>
count2: {count2()}
{console.log("count2 is", count2())}
</div>
<SubCountingComponent />
</div>
);
};
上述代码依然可以正常运行。因为它是基于状态驱动的。开发者在组件内使用 Signal 是本地状态,在组件外定义 Signal 就是全局状态。
Signals 本身不是那么有价值,但结合派生状态以及副作用就不一样了。代码如下所示:
import {
createSignal,
onCleanup,
createMemo,
createEffect,
onMount,
} from "solid-js";
const [count, setCount] = createSignal(0);
setInterval(() => {
setCount((c) => c + 1);
}, 1000);
// 计算缓存
const doubleCount = createMemo(() => count() * 2);
// 基于当前缓存
const quadrupleCount = createMemo(() => doubleCount() * 2);
// 副作用
createEffect(() => {
// 在 count 变化时重新执行 fetch
fetch(`/api/${count()}`);
});
const CountingComponent = () => {
// 挂载组件时执行
onMount(() => {
console.log("start");
});
// 销毁组件时执行
onCleanup(() => {
console.log("end");
});
return (
<div>
<div>Count value is {count()}</div>
<div>doubleCount value is {doubleCount()}</div>
<div>quadrupleCount value is {quadrupleCount()}</div>
</div>
);
};
实现机制
依赖收集
type useState = (initial: any) => [state, setter];
type createSignal = (initial: any) => [getter, setter];
模版编译
const CountingComponent = () => {
const [count, setCount] = createSignal(0);
const interval = setInterval(() => {
setCount((c) => c + 1);
}, 1000);
onCleanup(() => clearInterval(interval));
return <div>Count value is {count()}</div>;
};
对应编译后的的组件代码。
const _tmpl$ = /*#__PURE__*/ _$template(`<div>Count value is `);
const CountingComponent = () => {
const [count, setCount] = createSignal(0);
const interval = setInterval(() => {
setCount((c) => c + 1);
}, 1000);
onCleanup(() => clearInterval(interval));
return (() => {
const _el$ = _tmpl$(),
_el$2 = _el$.firstChild;
_$insert(_el$, count, null);
return _el$;
})();
};
执行 _tmpl$ 函数,获取对应组件的静态模版
提取组件中的 count 函数,通过 _$insert 将状态函数和对应模版位置进行绑定
调用 setCount 函数更新时,比对一下对应的 count,然后修改对应的 _el$ 对应数据
其他
Vue Ref
Angular Signals
Preact Signals
Solid Signals
Qwik Signals
Svelte 5 (即将推出)
Signals 性能很好,但不是编写 UI 代码的好方式
计划通过编译器来提升性能
可能会添加类似 Signals 的原语
参考资料
精读《SolidJS》:https://juejin.cn/post/7137100589208436743?searchId=2023100323265799EF4CF92C95049F6276
Solid.js:https://www.solidjs.com/
Introducing runes:https://svelte.dev/blog/runes
往期推荐
点这里 ↓↓↓ 记得 关注✔ 标星⭐ 哦
微信扫码关注该文公众号作者