本文档是针对具有扎实 React 经验的开发者,对 Svelte 进行快速介绍。我将提供一个简要的介绍,然后重点关注状态管理和 DOM 互操作性等方面。我计划快速推进,以便涵盖更多主题。归根结底,我主要希望激发大家对 Svelte 的兴趣。
要了解 Svelte 的基础知识,没有任何博文能比官方的 教程 或 文档 更好。
Svelte 风格的“Hello, World!”
让我们从快速了解 Svelte 组件的外观开始。
<script>
let number = 0;
</script>
<style>
h1 {
color: blue;
}
</style>
<h1>Value: {number}</h1>
<button on:click={() => number++}>Increment</button>
<button on:click={() => number--}>Decrement</button>
该内容位于 .svelte
文件中,并由 Rollup 或 webpack 插件处理以生成 Svelte 组件。这里包含几个部分。让我们逐一了解。
首先,我们添加一个 <script>
标签,其中包含我们需要的任何状态。
我们还可以添加一个 <style>
标签,其中包含我们想要的任何 CSS。这些样式以作用域限定到组件的方式,使得此处此组件中的 <h1>
元素将显示为蓝色。是的,作用域样式内置于 Svelte 中,无需任何外部库。在 React 中,通常需要使用第三方解决方案来实现作用域样式,例如 css-modules、styled-components 或类似的库(有数十种,甚至数百种选择)。
然后是 HTML 标记。正如您所料,您需要学习一些 HTML 绑定,例如 {#if}
、{#each}
等。这些特定于领域的语言特性可能看起来像是从 React 退步了一步,在 React 中,所有内容都是“仅仅是 JavaScript”。但有一些值得注意的地方:Svelte 允许您在这些绑定内部放置任意 JavaScript 代码。因此,类似这样的代码是完全有效的
{#if childSubjects?.length}
如果您是从 Knockout 或 Ember 迁移到 React 并从未回头,那么这可能会让您感到惊喜(惊喜)。
此外,Svelte 处理组件的方式与 React 非常不同。React 在组件内部或任何祖先(除非您“记忆化”)的任何状态发生变化时,都会重新运行所有组件。这可能会导致效率低下,这就是 React 提供 useCallback
和 useMemo
等功能来防止不必要的重新计算数据的原因。
另一方面,Svelte 会分析您的模板,并在任何相关状态发生变化时创建有针对性的 DOM 更新代码。在上面的组件中,Svelte 将看到 number
发生变化的位置,并在突变完成后添加更新 <h1>
文本的代码。这意味着您无需担心记忆化函数或对象。实际上,您甚至不必担心副作用依赖项列表,尽管我们稍后会讲到这一点。
但首先,让我们谈谈……
状态管理
在 React 中,当我们需要管理状态时,我们会使用 useState
钩子。我们向它提供一个初始值,它会返回一个元组,其中包含当前值和一个用于设置新值的函数。它看起来像这样
import React, { useState } from "react";
export default function (props) {
const [number, setNumber] = useState(0);
return (
<>
<h1>Value: {number}</h1>
<button onClick={() => setNumber(n => n + 1)}>Increment</button>
<button onClick={() => setNumber(n => n - 1)}>Decrement</button>
</>
);
}
我们的 setNumber
函数可以传递到任何我们想要的地方,例如子组件等。
在 Svelte 中,事情变得更简单。我们可以创建一个变量,并根据需要更新它。Svelte 的提前编译(与 React 的即时编译相反)将完成跟踪其更新位置并强制更新 DOM 的工作。上面相同的简单示例可能如下所示
<script>
let number = 0;
</script>
<h1>Value: {number}</h1>
<button on:click={() => number++}>Increment</button>
<button on:click={() => number--}>Decrement</button>
这里还需要注意的是,Svelte 不需要像 JSX 那样进行单一包装元素。Svelte 没有 React 片段 <></>
语法的等效项,因为它不需要。
但是,如果我们想将更新器函数传递给子组件以便它可以更新这部分状态,就像我们可以在 React 中做的那样,该怎么办?我们可以像这样编写更新器函数
<script>
import Component3a from "./Component3a.svelte";
let number = 0;
const setNumber = cb => number = cb(number);
</script>
<h1>Value: {number}</h1>
<button on:click={() => setNumber(val => val + 1)}>Increment</button>
<button on:click={() => setNumber(val => val - 1)}>Decrement</button>
现在,我们将它传递到需要的地方——或者继续关注更自动化的解决方案。
Reducers 和 Stores
React 还有 useReducer
钩子,它允许我们建模更复杂的状态。我们提供一个 reducer 函数,它会提供给我们当前值和一个 dispatch 函数,该函数允许我们使用给定参数调用 reducer,从而触发状态更新,更新为 reducer 返回的任何内容。我们上面提到的计数器示例可能如下所示
import React, { useReducer } from "react";
function reducer(currentValue, action) {
switch (action) {
case "INC":
return currentValue + 1;
case "DEC":
return currentValue - 1;
}
}
export default function (props) {
const [number, dispatch] = useReducer(reducer, 0);
return (
<div>
<h1>Value: {number}</h1>
<button onClick={() => dispatch("INC")}>Increment</button>
<button onClick={() => dispatch("DEC")}>Decrement</button>
</div>
);
}
Svelte 没有直接提供类似的功能,但它提供了一个名为Store的功能。最简单的 Store 类型是可写 Store。它是一个保存值的 Object。要设置新值,您可以调用 Store 上的 set
并传递新值,或者您可以调用 update 并传入一个回调函数,该函数接收当前值并返回新值(与 React 的 useState
完全一样)。
要在某个时间点读取 Store 的当前值,有一个 get
函数 可以调用,它会返回其当前值。Store 还有一个 subscribe 函数,我们可以向它传递一个回调函数,并且只要值发生变化,该回调函数就会运行。
Svelte 毕竟是 Svelte,所有这一切都有一些不错的语法快捷方式。例如,如果您在组件内部,您可以简单地在 Store 前缀上添加美元符号以读取其值,或者直接为其赋值以更新其值。以下是上面使用 Store 的计数器示例,以及一些额外的副作用日志,以演示 subscribe 的工作原理
<script>
import { writable, derived } from "svelte/store";
let writableStore = writable(0);
let doubleValue = derived(writableStore, $val => $val * 2);
writableStore.subscribe(val => console.log("current value", val));
doubleValue.subscribe(val => console.log("double value", val))
</script>
<h1>Value: {$writableStore}</h1>
<!-- manually use update -->
<button on:click={() => writableStore.update(val => val + 1)}>Increment</button>
<!-- use the $ shortcut -->
<button on:click={() => $writableStore--}>Decrement</button>
<br />
Double the value is {$doubleValue}
请注意,我在上面还添加了一个派生 Store。文档 对此进行了深入介绍,但简而言之,derived
Store 允许您使用与可写 Store 相同的语义,将一个 Store(或多个 Store)投影到一个新的值。
Svelte 中的 Store 非常灵活。我们可以将它们传递给子组件、更改、组合它们,甚至可以通过派生 Store 传递它们来使它们只读;如果我们要将一些 React 代码转换为 Svelte,我们甚至可以重新创建我们可能喜欢或需要的某些 React 抽象。
使用 Svelte 模拟 React API
解决了这些问题后,让我们回到之前 React 的 useReducer
钩子。
假设我们非常喜欢定义 reducer 函数来维护和更新状态。让我们看看利用 Svelte Store 模拟 React 的 useReducer
API 会有多么困难。我们基本上希望调用我们自己的 useReducer
,传入一个带有初始值的 reducer 函数,并获得一个包含当前值的 Store 以及一个调用 reducer 并更新 Store 的 dispatch 函数。实现这一点实际上并不太困难。
export function useReducer(reducer, initialState) {
const state = writable(initialState);
const dispatch = (action) =>
state.update(currentState => reducer(currentState, action));
const readableState = derived(state, ($state) => $state);
return [readableState, dispatch];
}
在 Svelte 中的使用方式几乎与 React 完全相同。唯一的区别是我们的当前值是一个 Store,而不是一个原始值,因此我们需要在它前面加上 $
来读取值(或手动调用 get
或 subscribe
)。
<script>
import { useReducer } from "./useReducer";
function reducer(currentValue, action) {
switch (action) {
case "INC":
return currentValue + 1;
case "DEC":
return currentValue - 1;
}
}
const [number, dispatch] = useReducer(reducer, 0);
</script>
<h1>Value: {$number}</h1>
<button on:click={() => dispatch("INC")}>Increment</button>
<button on:click={() => dispatch("DEC")}>Decrement</button>
useState
怎么样?
如果您真的喜欢 React 中的 useState
钩子,那么实现它也同样简单。在实践中,我发现这不是一个有用的抽象,但它是一个有趣的练习,它确实展示了 Svelte 的灵活性。
export function useState(initialState) {
const state = writable(initialState);
const update = (val) =>
state.update(currentState =>
typeof val === "function" ? val(currentState) : val
);
const readableState = derived(state, $state => $state);
return [readableState, update];
}
双向绑定真的很糟糕吗?
在结束本状态管理部分之前,我想谈谈 Svelte 独有的最后一个技巧。我们已经看到,Svelte 允许我们以任何 React 可以使用的方式将更新器函数向下传递到组件树。这通常是为了允许子组件通知其父组件状态更改。我们已经做了无数次了。子组件以某种方式更改状态,然后调用从父组件传递给它的函数,以便父组件可以知道该状态更改。
除了支持此回调传递之外,Svelte 还允许父组件双向绑定到子组件的状态。例如,假设我们有这个组件
<!-- Child.svelte -->
<script>
export let val = 0;
</script>
<button on:click={() => val++}>
Increment
</button>
Child: {val}
这将创建一个组件,并带有一个 val
prop。export
关键字是 Svelte 中组件声明 prop 的方式。通常,使用 prop 时,我们会将它们传递给组件,但这里我们会做一些不同的事情。如我们所见,此 prop 由子组件修改。在 React 中,此代码将是错误的且存在 bug,但在 Svelte 中,渲染此组件的组件可以执行以下操作
<!-- Parent.svelte -->
<script>
import Child from "./Child.svelte";
let parentVal;
</script>
<Child bind:val={parentVal} />
Parent Val: {parentVal}
在这里,我们将父组件中的一个变量绑定到子组件的 val
prop。现在,当子组件的 val
prop 发生变化时,我们的 parentVal
将由 Svelte 自动更新。
双向绑定对某些人来说是有争议的。如果您不喜欢它,那么请随意,不要使用它。但如果谨慎使用,我发现它是一个非常方便的工具,可以减少样板代码。
Svelte 中的副作用,无需泪水(或过时的闭包)
在 React 中,我们使用 useEffect
钩子来管理副作用。它看起来像这样
useEffect(() => {
console.log("Current value of number", number);
}, [number]);
我们在函数末尾编写依赖项列表。在每次渲染时,React 会检查列表中的每个项目,如果任何项目与上次渲染时的引用不同,则回调函数会重新运行。如果我们希望在上次运行后进行清理,则可以从效果中返回一个清理函数。
对于简单的事情,例如数字变化,这很容易。但是,任何有经验的 React 开发人员都知道,对于非平凡的用例,useEffect
可能非常难以处理。意外地遗漏依赖项数组中的某些内容并最终导致过时的闭包,这出奇地容易。
在 Svelte 中,处理副作用的最基本形式是反应式语句,它看起来像这样
$: {
console.log("number changed", number);
}
我们在代码块前加上 $:
并将我们希望执行的代码放在其中。Svelte 会分析读取了哪些依赖项,并在它们发生变化时重新运行我们的代码块。没有直接的方法让清理从反应式代码块上次运行时开始运行,但如果我们确实需要它,可以很容易地解决。
let cleanup;
$: {
cleanup?.();
console.log("number changed", number);
cleanup = () => console.log("cleanup from number change");
}
不,这不会导致无限循环:反应式代码块内部的重新赋值不会重新触发代码块。
虽然这可以工作,但通常这些清理效果需要在组件卸载时运行,而 Svelte 为此内置了一个功能:它有一个 onMount
函数,它允许我们返回一个在组件销毁时运行的清理函数,更直接地,它还有一个 onDestroy
函数,它可以执行您期望的操作。
使用 action 增添趣味
以上所有方法都足够有效,但 Svelte 凭借 action 更加出色。副作用通常与我们的 DOM 节点相关联。我们可能希望在一个 DOM 节点上集成一个旧的(但仍然很棒的)jQuery 插件,并在该节点离开 DOM 时将其拆除。或者我们可能希望为一个节点设置一个 ResizeObserver
,并在该节点离开 DOM 时将其拆除,等等。这是一个非常常见的要求,因此 Svelte 使用 action 内置了它。让我们看看如何操作。
{#if show}
<div use:myAction>
Hello
</div>
{/if}
请注意 use:actionName
语法。在这里,我们将此 <div>
与名为 myAction
的 action 关联,该 action 只是一个函数。
function myAction(node) {
console.log("Node added", node);
}
此 action 在 <div>
进入 DOM 时运行,并将 DOM 节点传递给它。这是我们添加 jQuery 插件、设置 ResizeObserver
等的机会。不仅如此,我们还可以从中返回一个清理函数,如下所示
function myAction(node) {
console.log("Node added", node);
return {
destroy() {
console.log("Destroyed");
}
};
}
现在,destroy()
回调函数将在节点离开 DOM 时运行。在这里,我们将拆除我们的 jQuery 插件等。
但是,还有更多!
我们甚至可以将参数传递给 action,如下所示
<div use:myAction={number}>
Hello
</div>
该参数将作为第二个参数传递给我们的 action 函数
function myAction(node, param) {
console.log("Node added", node, param);
return {
destroy() {
console.log("Destroyed");
}
};
}
如果您希望在该参数发生变化时执行其他工作,则可以返回一个更新函数
function myAction(node, param) {
console.log("Node added", node, param);
return {
update(param) {
console.log("Update", param);
},
destroy() {
console.log("Destroyed");
}
};
}
当 action 的参数发生变化时,更新函数将运行。要将多个参数传递给 action,我们传递一个对象
<div use:myAction={{number, otherValue}}>
Hello
</div>
……并且 Svelte 在对象的任何属性发生变化时重新运行我们的更新函数。
Action 是我最喜欢的 Svelte 功能之一;它们功能非常强大。
其他功能
Svelte 还提供了一些在 React 中没有对应功能的强大特性。它有许多表单绑定(教程中有介绍),以及 CSS 辅助工具。
来自 React 的开发者可能会惊讶地发现,Svelte 也自带动画支持。无需在 npm 上搜索并寄希望于最好的,它……是内置的。它甚至包括对 弹簧物理和进入/退出动画的支持,Svelte 将其称为转换。
Svelte 对 React.Chidren
的答案是插槽,可以命名也可以不命名,并且在 Svelte 文档中进行了很好的介绍。我发现它们比 React 的 Children API 更容易理解。
最后,我最喜欢的一个几乎隐藏的 Svelte 功能是,它可以将其组件编译成真正的 Web Components。该 svelte:options
辅助工具有一个 tagName
属性可以启用此功能。但请确保在 webpack 或 Rollup 配置中设置相应的属性。使用 webpack,它看起来像这样
{
loader: "svelte-loader",
options: {
customElement: true
}
}
有兴趣尝试 Svelte 吗?
以上任何一项都可以成为一篇很棒的博文。虽然我们可能只是触及了状态管理和 action 等内容的表面,但我们看到了 Svelte 的功能不仅与 React 相匹配,甚至可以模仿 React 的许多 API。这还是在我们简要提及 Svelte 的便利功能之前,例如内置动画(或转换)以及将 Svelte 组件转换为真正的 Web Components 的能力。
我希望我成功地激发了一些兴趣,如果我做到了,那么在深入探讨这些主题(以及更多主题)方面,文档、教程、在线课程等资源并不少。如果您在此过程中有任何疑问,请在评论中告诉我!
对于
const setNumber = cb => number = cb(number);
为什么它需要在一个函数中使用函数?为什么不使用
const setNumber = (newNumber) => number = newNumber;
您好!在这里,两种方法都可以。我这样写是为了帮助突出显示您可以在 Svelte 中使用更新函数,就像您可以在 React 中使用 useState 一样。
可能是因为您可能不希望直接传递数字,而是传递一个基于某些操作返回数字的函数。
这看起来很棒,但我认为我们永远不会为将 React 代码重构到新技术而付出代价,这非常耗时,并且几乎需要完全重写整个应用程序
我认为也不应该为了重构而重构。但问题是:对于一个新的项目,您会选择 Svelte 而不是 React 吗?
我目前使用 rust+wasm 和纯 css/js,但在去年的辉煌时光里,我曾使用 Svelte 开发前端,与 React、Angular 等相比,它非常轻松。
我喜欢这个教程,但我也不明白
这行代码。它让我感到困惑,几乎让我停止阅读这篇原本很棒的文章。我仍然不明白这段代码片段,例如,cb 的意义是什么,以及为什么要导入 Component3a?
查看下面的用法,有一个点击处理程序 prop
setNumber 获取一个回调函数(简称 cb)。
val => val + 1
就是此处的回调函数。然后它设置number = cb(number)
,调用回调函数以获取新值。没有必要这样操作,但这表明可以传递一个更新函数,类似于将更新函数传递给 React 的 useState setter。简单的“完成任务”方式可以是Svelte 现在实际上确实有片段:
<svelte:fragment>
。据我所知,唯一实际的用途是根据文档中所述使用命名插槽。哦哦——很好的提醒——谢谢!
我不知道您可以编写 fn?.(),第一次看到这种写法
是的!它是最近添加到 JS 中的,但所有现代浏览器都支持。
https://mdn.org.cn/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining
https://caniuse.cn/mdn-javascript_operators_optional_chaining
行。它让我感到困惑,几乎让我停止阅读一篇本来很棒的文章。例如,我仍然不明白那段代码片段,cb 的意义是什么,为什么导入 Component3a?
写得很好!您对 Svelte 中 TypeScript 支持的现状有什么见解吗?
实际上,是的 :)
Svelte 几乎表明 React 中那些丑陋的东西是没有必要的 :)
好文章!
一个小小的建议:我认为
useReduce
和useState
函数可以进一步简化。一个Writable
存储可以通过在一个新对象中公开subscribe
方法,简单地变成一个只读的Readable
存储。所以,与其使用
我们可以简单地
我在这里一分钱也没赚到,但由于您有一个前端大师横幅,所以很高兴知道他们有一个来自 Rich Harris 的课程,他是 Svelte 的创建者,所以没有什么比向大师学习更好的了。感谢您的帖子,我不是 React 开发人员,并且非常喜欢了解 React 开发人员对 Svelte 的看法,这证实了我许多的想法。
很棒的文章!我肯定会在我新的项目(小而快)中采用 Svelte。不过,那些
{#if}
、{#else}
和on:click
对我来说看起来很丑。但是一个组件中的错误会阻止所有组件。只有刷新才能解决 :(