大家好,这里是大家的林语冰。
免责声明
本文属于是语冰的直男翻译了属于是,仅供粉丝参考,英文原味版请临幸 React + Signals = Vue 3。
所有现代 JS(JavaScript)框架都在迅速采用 Signals(信号)。Signals 由来已久,但最近因为 Solidjs 带货焕发第二春,现在每个主流框架都在采用 Signals,包括但不限于 Qwik、Preact 和 Angular。Signals 可以显著减少样板(boilerplate),但无论底层框架如何,具体实现看起来或多或少难免趋同。举个栗子,一起来瞄一眼两个现在更流行的框架,React 和 Vue。
理解问题
JS 是一种非常强大的语言,但它不太适合响应式编程。为了演示这个问题,我们使用一个简单的代码片段:
js
let a = 1
let b = 2
let c = a + b // 3
a += 1
console.log(c) // 3,但 a + b 现在实际上等于 4
这本质上是一大坨响应式库正试图解决的问题。
理解 Signals
Signals 是一种表示和管理状态的响应式方案。Signals 本质上是事件驱动的变量,可以被 App 的其他部分订阅。当 Signal 的值变化时,其所有订阅者都会收到通知,且它们的代码将重新运行。这赋能一种非常经济的方法来更新 UI 以响应状态更改。
用 SolidJs 编写的一个小例子浅显易懂:
js
const [count, setCount] = createSignal(0)
// 设置 Signal 的初始值
count = 10
// 订阅 Signal 并打印当前值
count.subscribe(value => console.log(value))
// 更改 Signal 的值
setCount(20)
在 React 中使用 Signal(从 useState 到 Signals)
目前,要在 React 中管理组件间的状态,您需要通过 props 来传递它,创建若干 context 或使用类似 redux 的东东,这有自己的问题集和样板。
Preact 被标榜为轻快版 React,通过实现 Signal 正式首秀。
从本质上讲,Preact 中的 Signal 是一个具有保存值的 .value
属性的对象。该值可以更改,但 Signal 本身始终保持不变。
js
import { signal } from '@preact/signals'
const count = signal(0)
// 通过读写 .value 来读取 Signal 的值:
console.log(count.value) // 0
// 更新 Signal 的值:
count.value += 1
// Signal 的值已更新:
console.log(count.value) // 1
在 Preact 中,当 Signal 作为 props 或 context 沿树向下传递时,我们只是传递对 Signal 本身的引用。这意味着,Signal 可以更新而无需重新渲染任何未订阅该 Signal 的组件。这是因为组件看到的是 Signal,而不是其值。
因此,Preact 可以跳过昂贵的渲染工作,只重新渲染实际需要更新的组件。
信号还有第二个重要特征:它们跟踪何时访问其值以及何时更新其值。这意味着,当您 .value 从组件内部访问信号时,如果信号的值自上次渲染组件以来发生了变化,Preact 将自动重新渲染该组件。这可确保 UI 始终反映应用程序的当前状态。
jsx
import { signal } from '@preact/signals'
// 创建可被订阅的 Signal
const count = signal(0)
function Counter() {
// 当组件变化时,在其中读写 .value 会自动重新渲染
const value = count.value
const increment = () => {
// 通过给 .value 属性赋值更新 Signal:
count.value++
}
return (
<div>
<p>Count: {value}</p>
<button onClick={increment}>click me</button>
</div>
)
}
此外,可以使用 computed
函数组合多个 Signal。返回的计算 Signal 是只读的,当从回调函数中读写的任何 Signal 变化时,其值会自动更新。
jsx
import { signal, computed } from '@preact/signals'
const todos = signal([
{ text: 'Buy groceries', completed: true },
{ text: 'Walk the dog', completed: false }
])
// 创建一个根据其他 Signal 计算得到的 Signal
const completed = computed(() => {
// 当 todo 变化时,这里会回自动重新运行
return todos.value.filter(todo => todo.completed).length
})
// 打印:1,因为一个 todo 被标记为完成
console.log(completed.value)
上述的所有内容都是用 Preact 编写的,但可以使用 signals-react
包并使用相同的语法直接转移到 React。
jsx
// React 组件
import { signal } from '@preact/signals-react'
const count = signal(0)
function CounterValue() {
// 只要 count Signal 更新
// 我们就会为您自动重新渲染此组件
return <p>Value: {count.value}</p>
}
Vue 中的 Signals
如果您是 Vue 爱好者,但并不熟悉 Signals,上述所有内容应该一目了然。代码看起来与 Vue 3 几乎相同。
这是因为响应性是 Vue 1 幕后的基本思想,甚至从版本 1 开始。Vue 中的相同代码如下所示:
js
import { reactive, computed } from 'vue'
const todos = reactive([
{ text: 'Buy groceries', completed: false },
{ text: 'Walk the dog', completed: false }
])
const completed = computed(() => {
return todos.filter(todo => todo.completed).length
})
todos[0].completed = true
console.log(completed.value)
此代码与诉诸 @preact/signals
编写的 React 代码"图灵等价"!
完结撒花
显而易见,代码看起来一毛一样。但这是一件坏事吗?Signals 肯定棒棒哒,这就是为什么所有主流框架都在迅速采用 Signals。
但归根结底,可维护的代码和良好的开发人体工程学是我们想要的,让所有框架都使用同款工具将产生协同效应,每个人都会从中受益。
此外,拥有在框架和不同代码库之间反复横跳的技能只会对某人的职业生涯有益。
友情赞助
您现在收看的是前端翻译计划,学废了的小伙伴可以订阅此专栏合集,我们每天佛系投稿,欢迎持续关注前端生态。谢谢大家的点赞,掰掰~