介绍 Preact Signals

更新日期: 2022-09-17阅读: 47标签: 状态

1. 什么是 Signals?

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

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

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

2. 为什么是 Signals?

2.1 状态管理的困境

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

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

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

即使添加了 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 属性的细粒度更新。当点击加号的时候,只有 >span 里面的 random 都没有被执行。

const count = signal(0);

const App = () => {
  return (
    <Fragment>
      <h1 onClick={() => count.value++;}>
        +
        {console.log("++");}
      </h1>
      <span >{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", completed: true },
  { text: "Walk the dog", completed: false },
]);

// 基于其他 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 最近新出的特性,目前还不稳定,不建议在生产环境使用,如果想尝试,可以考虑在小型项目中使用。

来自:https://segmentfault.com/a/1190000042505864

链接: https://www.fly63.com/article/detial/12143

Javascript 状态管理工具 DataSet ,实现数据的订阅、查询、撤销和恢复

网页是用户与网站对接的入口,当我们允许用户在网页上进行一些频繁的操作时,对用户而言,误删、误操作是一件令人抓狂的事情,“如果时光可以倒流,这一切可以重来……”。

理解 React 轻量状态管理库 Unstated

在React写应用的时候,难免遇到跨组件通信的问题。现在已经有很多的解决方案。React本身的Context,Redux结合React-redux,Mobx结合mobx-react

你再也不用使用 Redux、Mobx、Flux 等状态管理了

这个库的作者希望使用 React 内置 API ,直接实现状态管理的功能。看完这个库的说明后,没有想到代码可以这个玩。短短几行代码,仅仅使用 React Hooks ,就实现了状态管理的功能。

为什么要使用状态管理

我们平时开发的大部分项目,由于复杂度不够, 很少使用 Vuex、Redux 等状态管理库,就算引入了 Vuex 这些库,也只是当作一个全局数据引用,并非对应用状态进行管理。但一旦页面的复杂度比较高,必然要引入状态管理,今天就聊聊我理解中的状态管理。

React使用Hooks与Context替代Redux状态管理

React Hooks 在 2018 年年底就已经公布了,正式发布是在 2019 年 5 月,关于它到底能做什么用,并不在本文的探讨范围之内,本文旨在摸索,如何基于 Hooks 以及 Context,实现多组件的状态共享,完成一个精简版的 Redux。

如何使用react hooks来进行状态管理?

首先要明确为什么要使用redux,这一点很重要,如果不知道为什么使用redux,那么在开发的过程中肯定不能合理的使用redux.首先来看redux的本质:redux做为一款状态管理工具,主要是为了解决组件间通信的问题。

Flutter基础--状态管理

当我们使用编译器创建一个新Flutter应用的时候,我们可以在主界面看到两个小部件StatelessWidget和StatefulWidget。这是两个最常见使用最频繁的小部件了。StatelessWidget ,StatefulWidget

共享可变状态中出现的问题以及如何避免?

本文回答了以下问题:么是共享可变状态?为什么会出现问题?如何避免其问题?标有(高级)的部分会更深入,如果你想更快地阅读本文,可以跳过。

node如何实现保持登录状态?

当我们登录成功,在这个页面刷新,页面并没有保存登录状态;今天我们就来看一下如何在后台使用cookie保存用户登录状态。做到刷新页面仍然显示在用户登录界面。node实现保持登录状态的方法如下:

3条简单的React状态管规则

React组件内部的状态是在渲染之间保持不变的封装数据。useState()是React钩子,负责管理功能组件内部的状态。我喜欢useState()确实使状态处理变得非常容易。但是我经常遇到类似的问题:

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!