大家好,很高兴又见面了,我是"高级前端进阶",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发!
高级前端进阶
前言今天一起来看看Svelte到底是什么?Svelte和React对比如何?
当我刚开始阅读 Svelte 文档时,发现它非常鼓舞人心,并打算在 Medium 上写一篇关于它的颂文。但是,在阅读了来自官方博客和社区的几篇文章后,我迟疑了,因为我注意到了 JavaScript 世界中一种常见修辞的迹象,一种让我非常不安的修辞。
嘿,还记得人类强大的头脑 30 年来一直试图解决的那个问题吗? 我刚刚找到了一个通用的解决方案! 为什么它还没有征服世界? 应该是显而易见的。 Facebook 的营销团队正在密谋反对我们。
在我看来,可以说您的工具与现有工具相比具有革命性的进步。 但是,人很难对自己的创作保持完全公正的态度。 举个正面的例子——与其他解决方案比起来,我觉得 Vue 实在是干得漂亮。没错,确实存在一些我不敢苟同的质疑声音,但这些声音都在传达一个建设性的信息:
我们的解决方案是怎样怎样的,还有别的一些现有的解决方案。而且我们坚信我们的方案更优秀,原因是什么什么。一些常见的反对论点是什么什么。
Svelte 的官方博客却正好相反,它通过只显露片面的事实来麻痹读者,甚至有时会宣扬一些关于 Web 技术和其他库(我会着重提到 React,只因我对它的理解更深一些)的不实言论。因此在本文中,我会对 Svelte 调侃一二,平衡一下官方吹斜的天平。话虽如此,我仍认为 Svelte 中还是有闪光点的,我会在文末告诉你原因。
1.什么是 Svelte?Svelte 是一种用于构建用户界面的工具,与流行的框架(如 React 和 Vue)利用虚拟 DOM 来更高效更新真实 DOM 不同,Svelte 使用静态分析在构建时创建 DOM 更新代码 。 下面是一个 Svelte 组件的示例(App.svelte):
<script>
import Thing from './Thing.svelte';
let things = [
{ id: 1, color: '#0d0887' },
{ id: 2, color: '#6a00a8' },
{ id: 3, color: '#b12a90' },
{ id: 4, color: '#e16462' },
{ id: 5, color: '#fca636' }
];
function handleClick() {
things = things.slice(1);
}
</script>
<button on:click={handleClick}>
Remove first thing
</button>
{#each things as thing}
<Thing color={thing.color}/>
{/each}
Thing.svelte 的内容如下:
<script>
export let color;
</script>
<p>
<span style="background-color: {color}">current</span>
</p>
<style>
span {
display: inline-block;
padding: 0.2em 0.5em;
margin: 0 0.2em 0.2em 0;
width: 4em;
text-align: center;
border-radius: 0.2em;
color: white;
}
</style>
等效的 React 组件代码如下:
import React, {usestate} from 'react'
import styled from 'styled-components';
const things = [
{ id: 1, color: '#0d0887' },
{ id: 2, color: '#6a00a8' },
{ id: 3, color: '#b12a90' },
{ id: 4, color: '#e16462' },
{ id: 5, color: '#fca636' }
];
const Block = styled.span`
display: inline-block;
padding: 0.2em 0.5em;
margin: 0 0.2em 0.2em 0;
width: 4em;
text-align: center;
border-radius: 0.2em;
color: white;
background-color: ${props => props.backgroundColor}
`;
const Thing = ({color}) => {
return (
<p>
<Block backgroundColor={color} />
</p>
);
}
export const App = () => {
const [things, setThings] = useState(things);
const removeFirstThing = () => setThings(things.slice(1))
return (
<>
<button onClick={removeFirstThing} />
{things.map(thing =>
<Thing key={thing.key} color={thing.color} />
}
</>
);
}
2.Svelte 不是一个框架——它是一种语言
它不只是通过 <script> 和 <style>标签 添加类似 Vue 的“单文件组件”,同时它为语言添加了一些结构,以解决 UI 开发中最复杂的问题之一,即状态管理。
Svelte 利用其作为编译器的特性,使响应式(reactivity)成为一种语言特性 。 Svelte 中有两种新的语言结构可用于此目的。
- $: 能够让该语句具有响应式的运算符,即每当该语句读取的变量有更新时,它都会被重新执行。一个语句可以是一次赋值(即“依赖变量”或“派生变量”)、一个代码块或者一个调用(即“effect”)。这有点类似 MobX 的方式,只不过是集成到语言中了。
- $ 运算符创建对 store(状态容器)的订阅,该订阅在组件卸载时自动取消
Svelte 的响应式概念允许使用常规 JS 变量存储状态,即不需要状态容器。
3.Svelte 的响应式React 的最初设计是您可以在每次状态更改时重新渲染整个应用程序而无需担心性能。 在实践中,可能会有一些问题,所以引入了类似 shouldComponentUpdate 这样的优化(这是一种告诉 React 何时可以安全地跳过组件的方式)。
所以,真正的问题是开发者在错误的时间、错误的地点花费太多时间来担心效率; 过早的优化是编程中的万恶之源(或至少是万恶之源)。首先要明确。 即使您的代码中没有任何一个 shouldComponentUpdate,React 也不会每次在状态更改时重新渲染整个应用程序,可以通过在 render 中打印一个 console 来证明。
在上面的例子中,除非 isAuthorized 状态发生变化,否则不会重新渲染 App。 任何子组件的更改都不会导致 App 组件重新渲染。 组件只有在它自己的状态发生变化,或者被 React 上下文触发时,或者在父组件重新渲染期间才会重新渲染。
后一种情况为所谓的无效渲染创造了空间,这种情况是事先知道父级重新渲染不会导致子级的 DOM 层次结构发生任何变化,但子级仍会重新渲染。 当子级 props 未更改或这种特殊类型的更改不应该影响屏幕上的可见内容时,就会发生这种情况。 为避免无效渲染,您可以定义 shouldComponentUpdate(或使用 React.memo 作为更现代的功能替代方案)。
4.默认开启优化不是最好的在绝大多数情况下,无效渲染并没有错。 它们占用的资源非常少,以至于人眼根本无法察觉。 事实上,将每个组件的 props 与其之前的 props 进行浅层比较可能比简单地重新渲染整个子树更耗费资源。 这就是 React 默认回退到 shouldComponentUpdate: () => true 的原因。 此外,React 团队甚至从开发工具中删除了“高亮更新(highlight updates)”功能,因为人们过去常常无缘无故地被无效渲染困扰。
这是一个非常危险的做法,因为每次优化都意味着要做假设。当你压缩一张图片时,你就会假设有些负载可以在不影响质量的前提下被削减,当你向后端增加缓存数据时,你就会假设 API 可能会返回相同的结果。恰当的假设能让你节省资源。而不恰当的假设就会给应用带来 Bug。这就是应该合理做优化的原因。
Svelte 选择了相反的处理方式。除非你用 $: 运算符做出了明确指定,否则组件代码不会在更新时重新运行。我可不想花上几十个小时来查找我哪个地方忘了加 $: 运算符,并试图搞清楚为何我的应用跑不起来,而这一切只为了用户能享受那快了 20 毫秒的重渲染。如果偶然遇到一个体积庞大的组件,我确实会优化它,但那是极其少见的情况了。在这一押注开发体验是没有意义的。
5.Svelte 的优化也不一定最优顺便说一句,Svelte 检查是否需要更新并不总是最佳的。 假设我有一个计算量非常大的组件,它接受具有以下的 prop:Array<{id: string, otherProps}>。 假设知道 ID 是唯一的并且数组项是不可变的,则可以使用以下代码来确定是否需要更新:
const shouldUpdate = (prevArr, nextArr) => {
if (prevArr.length !== nextArr.length) return true;
return nextArr.some((item, index) => item.id !== prevArr[index].id);
};
在 Svelte 中无法指定自定义响应式比较器(reaction comparators),它会回退到下面方法比较数组:
export function safe_not_equal(a, b) {
return a != a
? b == b
: a !== b || (a && typeof a === 'object') || typeof a === 'function';
}
虽然可以在 Svelte 的比较器之上使用一些第三方记忆工具,但是“开箱即用”的优化往往有局限性。
6.状态更新(Inexpressive state updates)每当你需要在 React 中更新状态时,你必须调用 setState。 为了在 Svelte 中更新您的状态,更新变量的名称必须出现在赋值运算符的左侧。Svelte 添加了对触发反应的内部运行时无效函数的调用,这带来了一些很反常规的用法。
const foo = obj.foo;
foo.bar = 'baz';
obj = obj;
// 如果没有这一行代码,更新不会生效
使用 push 或其他方法更新数组也不会自动触发组件更新,所以你必须使用数组或对象的 spread 操作符:
arr = [...arr, newItem];
obj = { ...obj, updatedValue: newValue };
这一点与 React 基本相同,除了在 React 中你需要进行函数调用,比如 setState 并将更新后的状态传递给该方法,而在 Svelte 中你有一种错觉,觉得是在使用常规可变变量。
7.虚拟 DOMVirtual DOM 很有价值,它允许您在构建应用程序时无需考虑状态转换,而且性能通常足够好。Svelte 博客中的几乎每篇文章都声称虚拟 DOM 是一种不必要的开销,而且开销相当高,可以轻松地替换为预生成的 DOM 更新程序。 但这种说法正确吗? 只能说一部分对!