共计 5670 个字符,预计需要花费 15 分钟才能阅读完成。
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 属性的细粒度更新。当点击加号的时候,只有 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", 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 最近新出的个性,目前还不稳固,不倡议在生产环境应用,如果想尝试,能够思考在小型我的项目中应用。
下一篇文章将会从介绍 Signals 的实现原理,也会率领大家从零开始实现一个 Signals。
举荐浏览
- Introducing Signals
- Signals
- 应用 Solid
- 各流派 React 状态治理比照和原理实现