介绍 Preact 信号

1.什么是信号?

Signals 是一种处理状态的方法,它参考了 SolidJS 并吸收了它的大部分优点。无论应用程序多么复杂,它都能保证快速响应。

Signals 的独特之处在于状态更改会以最有效的方式自动更新组件和 UI。

Signals 提供了基于自动状态绑定和依赖跟踪的出色人体工程学,并具有针对虚拟 DOM 优化的独特实现。

2. 为什么是信号?

2.1 状态管理的困境

随着应用越来越复杂,项目中的组件也会越来越多,需要管理的状态也越来越多。

为了实现组件状态共享,一般需要将状态提升到组件的共同祖先组件。 道具 传下来,问题是所有子组件都会随着更新而更新,需要配合 备忘录 使用备忘录 以优化性能。

虽然这听起来很合理,但随着项目代码的增长,很难确定这些优化应该放在哪里。

就算加了 记忆 , 并且经常因为依赖值不稳定而失效,而且由于 Hooks 没有明确的可以用来分析的依赖树,所以没有办法使用工具来查找原因。

另一种解决方案是将 语境 上面,子组件将自己作为消费者传递 使用上下文 获得所需的状态。

但是有一个问题,只能更新传递给Provider的值,而且只能整体更新,无法做到细粒度更新。

要解决这个问题,只有 语境 拆分,业务逻辑必然依赖多个 语境 ,就会出现 语境 娃娃现象。

2.2 未来的信号

看到这里你一定有似曾相识的感觉吧,是的,未来的解决方案一定是我——Recoil,不,这次的主角是Signals。

信号的核心是 价值 属性来保存对象的值。它有一个重要的特点,就是信号对象的值可以改变,但信号本身始终保持不变。

从“@preact/signals”导入{信号};

常数计数=信号(0);

// 通过访问 .value 来读取信号的值:
安慰。日志(计数。值); // 0

// 更新信号的值:
数数。值 += 1;

// 信号的值发生了变化:
安慰。日志(计数。值); // 1
复制代码

在 Preact 中,当信号作为 props 或 context 传递时,会传递对信号的引用。这允许在不重新渲染组件的情况下更新信号,因为组件传递的是信号对象而不是其值。

这使我们可以跳过所有昂贵的渲染工作并立即跳转到对信号的任意访问 。价值 组件的属性。

下面是 VDOM 和 Signals 在 Chrome 中更新时的火焰图对比,可以发现 Signals 的速度非常快。信号将比组件树更新更快地呈现,因为更新状态图所需的工作要少得多。

信号还有第二个重要的特性,那就是它们会跟踪它们的值何时被访问以及何时被更新。在 Preact 中,当信号的值发生变化时,从组件内部访问信号的属性会自动重新渲染组件。

2.3 栗子

我们可以通过一个例子来理解是什么让 Signals 与众不同:

从“@preact/signals”导入{信号};

常数计数=信号(0);

常量应用 = () => {
返回 (
<片段>
< h1 onClick = {() => count.value++;}> + {console.log("++")}</ h1 >
<跨度> {计数}</ span >
</ Fragment >
);
};
复制代码

当我们点击加号 10 次时,计数会从 0 变为 10,那么“++”会打印 10 次吗?

从我们编写 React 组件的经验来看,肯定会打印 10 次,但在 Signals 中并非如此。

从这个 Gif 可以看出,“++”没有打印一次,这是 Signals 的唯一性,整个组件没有重新渲染。

不仅h1没有重新渲染,就连span节点也没有重新渲染,唯一需要更新的地方就是 {数数} 这个文本节点。

提示:信号只会在设置新值时更新。如果设置的值没有改变,则不会触发更新。

除了文本节点,信号还可以对 DOM 属性进行细粒度的更新。单击加号时,仅 数据标识 已更新,甚至 跨度 里面 随机的 没有实施。

常数计数=信号(0);

常量应用 = () => {
返回 (

<h1 onClick={() =>计数值++;}>
+
{console.log("++") ;}

{数学.随机()}

) ;
} ;
复制代码

3.安装

可以通过把 @preact/信号 将包添加到项目中以安装 Signals:

npm install @preact/signals
复制代码

4. 用法

接下来我们将编写一个 TodoList Demo 来学习 Signals。

4.1 创建状态

首先,您需要一个包含待办事项列表的信号,可以用数组表示:

从“@preact/signals”导入{信号};

常量所有 = 信号([
{ text: "买杂货" },
{ text: "遛狗" },
]);
复制代码

接下来需要允许用户编辑输入框并新建一个Todo项,所以还需要为输入值创建一个信号,然后直接设置 。价值 实施修改。

// 我们稍后将使用它作为我们的输入
常量文本 = 信号(“”);

函数 addTodo() {
todos.value = [...todos.value, { text: text.value }] ;
text.value = "" ; // 添加时清除输入值
}
复制代码

我们要添加的最后一个功能是从列表中删除待办事项。为此,我们将添加一个从 todos 数组中删除给定 todo 项的函数:

功能删除待办事项(待办事项){
all.value = all.value.filter(t => t !== all) ;
}
复制代码

4.2 构建用户界面

现在我们已经创建了所有状态,我们需要编写使用 Preact 的用户界面。

功能 TodoList() {
const onInput = event => (text.value = event.target.value);

返回 (
<>
< 输入值 = {text.value} onInput = {onInput} />
< button onClick = {addTodo} > 添加</ button >

    {todos.value.map(todo => (
  • {todo.text}{''} <button onClick = {() => removeTodo(todo)}>❌</ button >
    </ li >))}</ ul >
    </>
    );
    }
    复制代码

    至此,一个完整的TodoList已经完成,可以 它在这里 体验完整的功能。

    4.3 派生状态

    TodoList 中有一个常见的场景,就是显示已完成项目的数量。如何为此设计状态?

    相信你的第一反应一定是 Mobx 或者 Vue 的派生状态,恰好在 Signals 中。

    从“@preact/signals”导入{信号,计算};

    常量所有 = 信号([
    { text: "买杂货", 完成: true },
    { text: "遛狗", 完成: false },
    ]);

    // 根据其他信号创建派生信号
    常量完成 = 计算(() => {
    // 当 todos 发生变化时,会自动重新计算
    全部归还。价值。过滤器(全部=>全部。完成)。长度;
    });

    安慰。日志(完成。值); // 1
    复制代码

    4.4 管理全局状态

    到目前为止,我们已经在组件树之外创建了 信号 ,对于小型应用程序来说还好,但对于大型复杂应用程序,测试会比较困难。

    因此,我们可以把 信号 提升到最外面的组件,通过 语境 发送。

    从“preact”导入 { createContext };
    从“preact/hooks”导入 { useContext };

    // 创建应用状态
    函数 createAppState() {
    常量所有 = 信号([]);

    常量完成 = 计算(() => {
    全部归还。价值。过滤器(全部=>全部。完成)。长度
    });

    返回{待办事项,完成}
    }

    常量 AppState = createContext();

    // 通过 Context 传递给子组件
    使成为(
    < AppState.Provider 值 = {createAppState()} >
    < 应用 />
    </ AppState.Provider >
    );

    // 子组件接收后使用
    函数应用程序(){
    常量状态 = useContext(AppState);
    返回 < p > {state.completed}</ p > ;
    }
    复制代码

    4.5 管理本地状态

    除了直接通过 信号 要创建状态,我们还可以使用提供的钩子来创建组件内部状态。

    从“@preact/signals”导入 { useSignal, useComputed };

    函数计数器(){
    常量计数 = 使用信号(0);
    const double = useComputed( () => count.value * 2);

    返回 (


    < p > {count} x 2 = {double}</ p >
    < button onClick = {() => count.value++}>点击我</ button >
    </ div >
    );
    }
    复制代码

    使用信号 实施是基于 信号 是的,原理比较简单,使用 使用备忘录 来吧 信号 缓存以避免在更新时重新创建新的 信号 .

    函数使用信号(值){
    返回 useMemo(() => 信号(值), []);
    }
    复制代码

    4.6 订阅变更

    从前面的示例中您可以注意到,在组件外部访问 信号 直接读取值时,不涉及响应值的变化。

    在 Mobx 中提供 自动运行 订阅价值变化, 信号 它提供 影响 订阅方法。

    影响 接收回调函数作为参数,当回调函数依赖于 信号 值变了,这个回调函数也会重新执行

    从“@preact/signals-core”导入{信号、计算、效果};

    常量名称 = 信号(“简”);
    常量姓氏 = 信号(“Doe”);
    const fullName = computed(() => ${name.value} ${surname.value}) ;

    // 每次名字变化的时候就打印出来
    效果(()=> console.log(fullName.value)); //打印:“简·多伊”

    // 更新 name 的值
    name.value = "约翰" ;
    // 触发自动打印: "John Doe"
    复制代码

    影响 执行后,返回一个新函数用于退订。

    常量名称 = 信号(“简”);
    常量姓氏 = 信号(“Doe”);
    const fullName = computed(() => name.value + " " + surname.value) ;

    const dispose = effect(() => console.log(fullName.value)) ;

    // 取消订阅
    处置();

    // 更新 name,会触发 fullName 的更新,但不会触发 effect 回调执行了
    name.value = "约翰" ;
    复制代码

    在极少数情况下,您可能需要 效果(fn) 更新里面 信号 ,但不想 信号 在更新时重新运行,因此您可以使用 。窥视() 要得到 信号 但不要订阅。

    常量增量 = 信号(0);
    常数计数=信号(0);

    效果(()=> {
    // 更新 count 但不订阅变化
    count.value = count.peek() + delta.value ;
    }) ;

    delta.value = 1 ;

    // 不会触发 effect 回调函数重新执行
    计数值 = 10 ;
    复制代码

    4.7 批量更新

    有时我们可能同时有多个更新,但是我们不想触发多个更新,所以需要像 React 的 setState 那样合并更新。

    信号提供 方法允许我们 信号 进行批量更新。

    以我们创建待办事项和清空输入框为例:

    效果(()=>控制台.log(todos.length,text.value););

    函数 addTodo() {
    批处理(()=> {
    // 效果只会执行一次
    todos .value = [...todos.value, { text: text.value }];
    文本 .value = "";
    });
    }
    复制代码

    五、总结

    信号是 Preact 的一个新特性。目前还不稳定。不建议在生产环境中使用。如果你想尝试一下,你可以考虑在小项目中使用它。

    下一篇文章会介绍 Signals 的实现原理,也会带领大家从零开始实现一个 Signals。

    推荐阅读

    1. 介绍信号
    2. 信号
    3. 使用实体
    4. 各校React状态管理对比及原理实现

    版权声明:本文为博主原创文章,遵循CC 4.0 BY-SA版权协议。转载请附上原文出处链接和本声明。

    这篇文章的链接: https://homecpp.art/5918/8819/0721

    版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明

    本文链接:https://www.qanswer.top/37548/20101810

posted @ 2022-09-18 10:21  哈哈哈来了啊啊啊  阅读(298)  评论(0)    收藏  举报