介绍 Preact Signals

news/2024/5/7 9:56:37/文章来源:https://www.cnblogs.com/INGP/p/16704095.html

1. 什么是 Signals?

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

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

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

2. 为什么是 Signals?

2.1 状态管理的困境

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

为了实现组件状态共享,一般需要将状态提升到组件的共同的祖先组件里面,通过 props 往下传递,带来的问题就是更新时会导致所有子组件跟着更新,需要配合 memouseMemo 来优化性能。

虽然这听起来还挺合理,但随着项目代码的增加,我们很难确定这些优化应该放到哪里。

即使添加了 memoization,也常常因为依赖值不稳定变得无效,由于 Hooks 没有可以用于分析的显式依赖关系树,所以也没法使用工具来找到原因。

另一种解决方案就是放到 Context 上面,子组件作为消费者自行通过 useContext 来获取需要的状态。

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

为了处理这个问题,只能将 Context 进行拆分,业务逻辑又不可避免地会依赖多个 Context,这样就会出现 Context 套娃现象。

2.2 通向未来的 Signals

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

signal 的核心是一个通过 value 属性 来保存值的对象。它有一个重要特征,那就是 signal 对象的值可以改变,但 signal 本身始终保持不变。

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

const count = signal(0);

// Read a signal’s value by accessing .value:
console.log(count.value);   // 0

// Update a signal’s value:
count.value += 1;

// The signal's value has changed:
console.log(count.value);  // 1

在 Preact 中,当 signal 作为 props 或 context 向下传递时,传递的是对 signal 的引用。这样就可以在不重新渲染组件的情况下更新 signal,因为传给组件的是 signal 对象而不是它的值。

这让我们可以跳过所有昂贵的渲染工作,立即跳到任意访问 signal .value 属性的组件。

这里有 VDOM 和 Signals 在 Chrome 里面更新时的火焰图对比,可以发现 Signals 非常快。相比组件树更新,Signals 渲染会更快一些,这是因为更新状态图所需的工作要少得多。

Signals 具有第二个重要特征,即它们会跟踪其值何时被访问以及何时被更新。在 Preact 中,当 signal 的值发生变化时,从组件内访问 signal 的属性会自动重新渲染组件。

2.3 栗子

我们可以用一个例子来理解 Signals 的独特之处:

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

const count = signal(0);

const App = () => {
  return (
    <Fragment>
      <h1 onClick={() => count.value++;}>
        +
        {console.log("++")}
      </h1>
      <span>{count}</span>
    </Fragment>

  );
};

当我们点击10次加号之后,count会从0变成10,那么"++"是否会被打印10次呢?

从我们平时写 React 组件的经验来说,肯定会被打印10次,但在 Signals 里面不是这样。

从这个 Gif 可以看到,"++"一次都没被打印出来,这就是 Signals 的独特之处,整个组件没有被重新渲染。

不仅 h1 没有重新渲染,甚至连 span 节点都没有重新渲染,唯一更新的地方就只有 {count} 这个文本节点。

💡 提示:Signal 只有在设置新的值才会更新。如果设置的值没有发生变化,就不会触发更新。

除了文本节点,Signals 还能做到对 DOM 属性的细粒度更新。当点击加号的时候,只有 data-id 被更新了,甚至连 span 里面的 random 都没有被执行。

const count = signal(0);

const App = () => {
  return (
    <Fragment>
      <h1 onClick={() => count.value++;}>
        +
        {console.log("++");}
      </h1>
      <span data-id={count}>{Math.random()}</span>
    </Fragment>

  );
};

3. 安装

可以通过将 @preact/signals 包添加到项目中来安装 Signals:

npm install @preact/signals

4. 用法

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

4.1 创建状态

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

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

const todos = signal([
  { text"Buy groceries" },
  { text"Walk the dog" },
]);

接着,需要允许用户编辑输入框、创建新的 Todo 事项,所以还要创建输入值的 signal,然后直接设置 .value 来实现修改。

// We'll use this for our input later
const text = signal("");

function addTodo({
  todos.value = [...todos.value, { text: text.value }];
  text.value = ""// Clear input value on add
}

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

function removeTodo(todo{
  todos.value = todos.value.filter(t => t !== todo);
}

4.2 构建用户界面

现在我们创建了所有的状态,接下来需要编写用户界面,这里使用了 Preact。

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

  return (
    <>
      <input value={text.value} onInput={onInput} />
      <button onClick={addTodo}>Add</button>
      <ul>
        {todos.value.map(todo => (
          <li>
            {todo.text}{' '}
            <button onClick={() => removeTodo(todo)}>❌</button>
          </li>
        ))}
      </ul>
    </>

  );
}

到这里,一个完整的 TodoList 就已经完成了,你可以在这里体验完整的功能。

4.3 衍生状态

在 TodoList 里面有一个常见的场景,那就是展示已完成事项数量,这个要怎么去设计状态呢?

相信你的第一反应肯定是 Mobx 或者 Vue 的衍生状态,刚好在 Signals 里面也有。

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

const todos = signal([
  { text"Buy groceries"completedtrue },
  { text"Walk the dog"completedfalse },
]);

// 基于其他 signals 创建衍生 signal
const completed = computed(() => {
  // 当 todos 变化,这里会自动重新计算
  return todos.value.filter(todo => todo.completed).length;
});

console.log(completed.value); // 1

4.4 管理全局状态

到目前为止,我们都是在组件树之外创建了 signal,对于小型应用来说没什么问题,但对于大型复杂应用来说,测试会比较困难。

因此,我们可以将 signal 提升至最外层组件里面,通过 Context 进行传递。

import { createContext } from "preact";
import { useContext } from "preact/hooks";

// 创建 App 状态
function createAppState({
  const todos = signal([]);

  const completed = computed(() => {
    return todos.value.filter(todo => todo.completed).length
  });

  return { todos, completed }
}

const AppState = createContext();

// 通过 Context 传递给子组件
render(
  <AppState.Provider value={createAppState()}>
    <App />
  </AppState.Provider>

);

// 子组件接收后使用
function App({
  const state = useContext(AppState);
  return <p>{state.completed}</p>;
}

4.5 管理局部状态

除了直接通过 signals 来创建状态,我们也可以使用提供的 hooks 来创建组件内部状态。

import { useSignal, useComputed } from "@preact/signals";

function Counter({
  const count = useSignal(0);
  const double = useComputed(() => count.value * 2);

  return (
    <div>
      <p>{count} x 2 = {double}</p>
      <button onClick={() => count.value++}>click me</button>
    </div>

  );
}

useSignal 的实现是基于 signal 的,原理比较简单,利用了 useMemo 来对 signal 进行缓存,避免更新时重新创建了新的 signal

function useSignal(value{
    return useMemo(() => signal(value), []);
}

4.6 订阅变化

从前面的例子里面可以注意到,在组件外访问 signal 的时候,都是直接读取它的值,并不涉及到响应值的变化。

在 Mobx 里面提供了 autoRun 来订阅值的变化,signal 里面提供了 effect 方法来订阅。

effect 接收一个回调函数作为参数,当回调函数中依赖的 signal 值发生了变化,这个回调函数也会被重新执行

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

const name = signal("Jane");
const surname = signal("Doe");
const fullName = computed(() => `${name.value} ${surname.value}`);

// 每次名字变化的时候就打印出来
effect(() => console.log(fullName.value)); // 打印: "Jane Doe"

// 更新 name 的值
name.value = "John";
// 触发自动打印: "John Doe"

effect 执行后会返回一个新的函数,用于取消订阅。


const name = signal("Jane");
const surname = signal("Doe");
const fullName = computed(() => name.value + " " + surname.value);

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

// 取消订阅
dispose();

// 更新 name,会触发 fullName 的更新,但不会触发 effect 回调执行了
name.value = "John";

在极少情况下,你可能需要在 effect(fn) 里面更新 signal,但又不希望在 signal 更新时重新运行,所以可以使用 .peek() 来获取 signal 但不订阅。

const delta = signal(0);
const count = signal(0);

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

delta.value = 1;

// 不会触发 effect 回调函数重新执行
count.value = 10;

4.7 批量更新

有时候我们可能会同时有多个更新,但又不希望触发多次更新,所以需要像 React 的 setState 一样合并更新。

Signals 提供了 batch 方法允许我们对 signal 进行批量更新。

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

effect(() => console.log(todos.length, text.value););

function addTodo({
  batch(() => {
    // effect 里面只会执行一次
    todos.value = [...todos.value, { text: text.value }];
    text.value = "";
  });
}

5. 总结

Signals 是 Preact 最近新出的特性,目前还不稳定,不建议在生产环境使用,如果想尝试,可以考虑在小型项目中使用。

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

推荐阅读

  1. Introducing Signals
  2. Signals
  3. 使用 Solid
  4. 各流派 React 状态管理对比和原理实现

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.luyixian.cn/news_show_9381.aspx

如若内容造成侵权/违法违规/事实不符,请联系dt猫网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

手写RPC框架Feign

Feign原理实现手写RPC框架Feign什么是RPCFeign注入原理EnableFeignClientsFeign调用入口手写Feign手写RPC框架Feign 阅读本文你可获得&#xff1a; 1、RPC原理 2、feign注入原理 3、如何手写feign框架 4、动态代码设计模式应用场景 什么是RPC RPC&#xff1a;远程过程调用&…

基于ssm的校运会管理系统设计与实现-计算机毕业设计源码+LW文档

开发语言&#xff1a;Java 框架&#xff1a;ssm JDK版本&#xff1a;JDK1.8 服务器&#xff1a;tomcat7 数据库&#xff1a;mysql 5.7&#xff08;一定要5.7版本&#xff09; 数据库工具&#xff1a;Navicat11 开发软件&#xff1a;eclipse/myeclipse/idea Maven包&#xff1a;…

CSS基础篇---01选择器、字体与文本样式

CSDN话题挑战赛第2期 参赛话题&#xff1a;学习笔记 文章目录 1.初识CSS CSS引入方式 2.选择器 标签&#xff08;元素&#xff09;选择器 class选择器 id选择器 通配符选择器 案例演示 3.字体样式 字体大小 字体粗细 字体样式 字体类型 字体系列 层叠性 font复…

VM关闭虚拟机之后,连接不上前一天设置的静态ip

错误场景&#xff1a; 问题原因&#xff1a; centos7 默认的网卡 ens33 在虚拟机启动时&#xff0c;没有自动启动。导致设置的静态ip不生效&#xff0c;故连接不上。 解决方案&#xff1a; 查看托管是否启动&#xff0c;倘若网卡没有启动很可能是没有加入托管。 查看托管是否…

【强化学习】《动手学强化学习》动态规划算法

【强化学习】《动手学强化学习》动态规划算法一、基本思想二、悬崖漫步环境三、策略迭代算法3.1 策略评估3.2 策略提升3.3 悬崖漫步环境下的策略迭代四、价值迭代算法一、基本思想 动态规划算法在计算机专业课中是特别重要的思想&#xff0c;将待求问题分解成若干个子问题&…

Springboot 集成kafka

一、创建项目并导入pom依赖 <dependency><groupId>org.springframework.kafka</groupId><artifactId>spring-kafka</artifactId> </dependency> 二、修改application.yml配置 1. producer 生产端的配置 spring:#重要提示:kafka配置,该…

Redis介绍和安装

Redis介绍 Redis是一个开源的、基于Key-Value(键-值&#xff09;存储的NoSQL数据库。Redis因其丰富的数据结构、极快的速度、齐全的功能而为人所知&#xff0c;它是目前内存数据库方面的事实标准&#xff0c;是目前使用广泛的开源缓存中间件。 Redis特点 结构丰富&#xff0…

CS231a课程笔记:Lecture2 Camera Models

关于齐次坐标&#xff1a;(15条消息) 为什么要引入齐次坐标&#xff0c;齐次坐标的意义&#xff08;一&#xff09;_追求卓越583的博客-CSDN博客_齐次坐标的意义(15条消息) 为什么要引入齐次坐标&#xff0c;齐次坐标的意义&#xff08;二&#xff09;_追求卓越583的博客-CSDN博…

DNS 解析流程

一、背景 最近&#xff0c;在S3协议项目中调研通过DNS域名解析处理流量负载均衡问题。原来对dns也有一些粗浅的了解&#xff0c;知道通过DNS可以将域名转换为IP地址&#xff0c;也可以做负载均衡。但是DNS的解析流程以及缓存等机制&#xff0c;只是一知半解。正好&#xff0c;…

windows安装nginx并设置开机自启动

在macOS和linux中使用nginx我早已经轻车熟路。突然切到windows的环境中&#xff0c;我反而不会用了。 之前写了《windows使用nginx探索笔记》内容比较冗长&#xff0c;所以本文尽量精简一下。 环境 操作系统&#xff1a;windows 2008R2 Datacenter 已经安装的软件&#xff1…

C语言中malloc(),free(),calloc(),realloc()

申请内存malloc()在申请内存时不会对内存进行初始化赋值 在申请内存后&#xff0c;没有对内存进行初始化的话&#xff0c;这段内存中就存储着系统随机值。 int n 5; int* p (int*)malloc(n * sizeof(int));malloc(size):size就是你想开辟的内存的字节大小。我们通常想要用这段…

SpringCloud基础6——分布式事务,Seata

用于复习快速回顾。 目录 1.分布式事务问题 1.1.本地事务&#xff0c;ACID原则 1.2.分布式事务 1.3.演示分布式事务问题 2.理论基础 2.1.CAP定理 2.1.1.一致性&#xff0c;数据同步 2.1.2.可用性&#xff0c;节点正常访问 2.1.3.分区容错 2.1.4.矛盾 2.2.BASE理论 …

vulnhub-xxe lab: 1

ifconfig nmap 192.168.61.0/24 找到192.168.61.145 目录扫描&#xff08;御剑&#xff09; 192.168.61.145/xxe 192.168.61.145/admin.php 无法访问&#xff0c;但是robots.txt里面写的应该不会是无效网站&#xff0c;所以可能是被拒绝访问了 抓xxe的包 可以发现是用xml写的…

[ web基础篇 ] Burp Suite 爆破 Basic 认证密码

&#x1f36c; 博主介绍 &#x1f468;‍&#x1f393; 博主介绍&#xff1a;大家好&#xff0c;我是 _PowerShell &#xff0c;很高兴认识大家~ ✨主攻领域&#xff1a;【渗透领域】【数据通信】 【通讯安全】 【web安全】【面试分析】 &#x1f389;点赞➕评论➕收藏 养成习…

层次选择器

层次选择器 后代选择器简介后代选择器可以选择作为某元素后代的元素(包括儿子,孙子,重孙子) 两个元素之间的层次间隔可以是无限的示例<!DOCTYPE html> <html lang="en"><head><meta charset="UTF-8"><title>Title</t…

怎么把握住股票每天的最佳交易时机?

每个股民都希望自己能够在每天的股价最高点卖出&#xff0c;然后在最低点再买回来&#xff1b;但是怎么去判断最好的交易时机呢&#xff0c;很多人会想很多方法去识别判断最佳交易点&#xff0c;今天给大家分享一种方法&#xff1b;我一直在思考股票交易的底层逻辑是啥&#xf…

如何在基础镜像中安装指定python版本

背景 由于规范要求要使用指定的镜像版本,但是由于该镜像中的python与我使用的版本有差异,怕引起一些不必要的兼容问题,所以我需要自己按基础镜像基础上安装对应版本的python。 Dockerfile 直接上最终dockerfile,为什么这样写,后面说到。 FROM centos:7 # 指定工作目录 WOR…

【2022中国高校计算机大赛 微信大数据挑战赛】Top 1-6 方案总结

前段时间参加了 2022中国高校计算机大赛 微信大数据挑战赛&#xff0c;比赛链接&#xff1a;https://algo.weixin.qq.com/。 由于时间原因精力有限&#xff0c;我们队伍的方案做的比较简陋&#xff1a; 【初赛&#xff1a;rank-18&#xff0c;复赛&#xff1a;rank-40&#xff…

网课查题接口 搜题公众号对接题库教程 (附赠题库接口)

网课查题接口 搜题公众号对接题库教程 &#xff08;附赠题库接口&#xff09; 本平台优点&#xff1a; 多题库查题、独立后台、响应速度快、全网平台可查、功能最全&#xff01; 1.想要给自己的公众号获得查题接口&#xff0c;只需要两步&#xff01; 2.题库&#xff1a; 查…

bm19bm7

为什么不定义如果两点相等呢 等于的话峰值统一取右 以右来比较 波峰就行 不一定是最大的 在这里插入代码片 import java.util.*;public class Solution {/*** 代码中的类名、方法名、参数名已经指定&#xff0c;请勿修改&#xff0c;直接返回方法规定的值即可** * param nums…