曾几何时,React、Angular和Ember站在网络开发的最前沿,争相成为最好的JavaScript框架,后来Vue大行其道,将Ember踢到了一边,取而代之。
快进到2022年,历史似乎正在重演,因为Svelte,一个相对较新的框架,正在以指数级的速度流行起来。我们不禁要问自己,Svelte是否具备成为顶级竞争者的条件。
Svelte似乎借鉴了Vue的玩法,实现并改进了让我们一开始就喜欢Vue的东西,比如高性能、轻量级和熟悉的模板语法,以及简单的学习曲线。
在这篇文章中,我们将研究Svelte和Vue在语法上的差异,比较它们在引擎盖下的工作方式。
目录
Vue的基本原理
Vue是一个渐进的、开源的JavaScript框架,用于构建用户界面和单页应用程序。
Vue被设计成渐进式集成,这意味着你可以将Vue添加到现有前端项目的任何部分,而不需要重新构建整个项目。Vue只依赖于JavaScript,不像Svelte这样的框架,它使用了编译器。
要用Vue构建一个单页应用程序,你必须使用它的CLI,这是一个命令行实用工具,可以用不同的构建系统快速搭建Vue模板项目的支架。
Vue的核心是一些从其竞争对手Angular和React那里继承的概念。首先,Vue利用了Angular的反应式双向数据绑定,在模型和视图之间建立了反应式连接。另一个是React的Virtual DOM diffing,它可以防止Vue在每次有变化时都要动手术更新DOM。
部分受到MVVM设计模式的启发,Vue的重点在于视图层,或模板。然而,Vue应用程序中的每个组件实例都被称为ViewModel,或vm 变量。vm 是连接视图和模型-视图层的数据绑定系统。
Svelte的基本原理
Svelte是一个开源的、用于创建交互式UI的前端框架。与Vue不同,Svelte是一个编译器,可以将声明性的状态驱动组件转换为直接更新DOM的命令式JavaScript代码。
通过虚拟DOM差异化技术,单体框架在运行时编译声明性代码。虽然这种方法高效快速,但它要求浏览器在渲染网页之前执行额外的任务,产生影响应用程序性能的开销。
通过解析并将其声明性代码编译成浏览器可以在构建时使用的JavaScript代码,Svelte避免了这种性能开销,使其比使用虚拟DOM并在运行时编译的框架快2倍。
开始使用
在我们让这两个框架相互竞争之前,让我们为每个框架建立一个样本应用程序,以便我们在整个文章中参考。首先,创建两个新文件夹,每个框架一个。打开你的机器的命令行工具。cd 进入每个文件夹,然后在各自的文件夹中运行下面的命令。
Vue
下面的命令将在你的机器上安装Vue CLI。
npm install -g @vue/cli
# OR
yarn global add @vue/cli
安装完成后,运行下面的命令来搭建一个Vue项目的支架。
vue create vue-sample-app
要运行该应用程序,cd 到vue-sample-app 文件夹。
cd vue-sample-app
然后,启动开发服务器,如下所示。
npm run serve
Svelte
用下面的命令打开一个新的Svelte项目。
npx degit sveltejs/template
运行下面的命令来安装所需的依赖项。
npm install
现在,启动一个新的开发服务器。
npm run dev
虽然我们可以为我们的应用程序的源代码创建单独的组件,并将其导入项目中的高级组件,App.svelte 和App.vue ,但为了简单起见,我们将使用高级组件来代替。
让我们清理一下App.svelte 和App.vue 组件,并将下面的代码块放在它们各自的文件中。
//VUE
<template>
<label>City </label>
<input type="text" v-model="city"/>
<label>Town </label>
<input type="text" v-model="town"/>
<div class="location">Location: {{location}} </div>
<button v-on:click="handleReset">Reset</button>
</template>
<script>
export default {
name: 'App',
data(){
return{
city: "",
town: ""
}
},
methods: {
handleReset(){
this.city = ""
this.town = ""
}
},
computed: {
location(){
return this.city + " " + this.town
}
}
}
</script>
<style scoped>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
input{
display: block;
margin-bottom: 15px;
width: 200px;
height: 35px;
}
button{
width: 60px;
height: 30px;
}
.location{
margin-bottom: 10px;
}
</style>
//SVELTE
<script>
let city = "";
let town = "";
$: location = "Location: " + city + ' ' + town;
const reset = () => {
city = "";
town = "";
}
</script>
<main>
<div>
<label>City</label>
<input type="text" bind:value={city}>
<label>Town</label>
<input type="text" bind:value={town}>
<button on:click={reset}>Reset</button>
</div>
<div>
{location}
</div>
</main>
<style>
main{
background-color: white;
}
</style>
每个应用程序包括两个反应式变量,city 和town ,两个与反应式变量绑定的输入字段,以及一个带有事件处理程序的按钮,在触发时重置反应式变量。
注意:因为我们的应用程序只用于比较的目的,所以造型是没有必要的。
组件结构和语法比较
你可能已经注意到,Svelte和Vue在模板结构和句法元素方面有很多共同的特点。两者都使用单文件组件,这是一种特殊的文件格式,将组件的模板、逻辑和样式封装在一个.svelte 或.vue 文件中。
一个单文件组件由三部分组成。
- 模板。
<template></template部分包含组件的纯HTML标记。 - 脚本。
<script></script>部分包含组件中所有的JavaScript逻辑。 - 样式。
<style></style>部分包含了该组件的本地范围内的样式。
Svelte并没有像Vue的<template> ,有专门的模板标签来封装组件中的标记。HTML标记可以直接在组件内以任何顺序定义。
声明式渲染
两种模板之间最明显的区别是向模板引用声明性数据属性的方法。Svelte使用单大括号{ } 。
<div>
{location}
</div>
另一方面,Vue使用双大括号{{ }} ,也就是所谓的小胡子语法。
<div>
{{location}}
</div>
数据绑定
为了将数据从模型中绑定到模板中,Svelte和Vue使用了特殊的属性,称为指令。一个指令的工作是在其表达式的值发生变化时,对DOM进行反应性的副作用。
bind 是Vue和Svelte中用于绑定数据的指令。bind 指令接受用冒号表示的参数,以及将被绑定到它所定义的元素上的数据属性或反应式变量。
下面的代码包含了我们的Svelte应用样本中的一个输入栏元素。bind 指令被分配了一个value 参数和一个反应式变量,city 。
<input type="text" bind:value={city}>
在上面的代码中,bind 指令将输入字段的值与反应式变量city 。当输入字段的状态发生变化时,反应式变量会根据变化情况进行更新。
Vue为其指令添加了一个v- 的前缀,因此,bind 指令在Vue中的定义如下。
v-bind:value
在Vue应用中,我们把v-bind 指令换成了v-model 。在Vue中,v-bind 只能单向绑定数据,而v-model 则创建双向绑定。
现在,假设我们使用v-bind ,将我们的输入字段与模型中的数据属性绑定。只有输入字段的状态变化才会触发反应性。但是用v-model ,数据属性和输入元素的状态的变化都会触发反应性。
计算的属性
在我们的Svelte应用程序中,另一个奇特的语法是美元符号$: 标签,它被用来定义一个计算值,将其前缀到顶层赋值,或不在代码块或函数内的赋值。
一个计算值或属性是一个接受反应式变量作为赋值的反应式声明。当反应式变量发生变化时,计算值会做出反应。基本上,一个计算值是一个依赖于其他状态的状态。
直接出现在$: 块内的值将成为反应式语句的依赖项。
在我们的Svelte应用程序中,location 值是一个依赖于反应式变量city 和town 的计算值。我们将反应式变量分配给计算值,并在模板中引用了结果。
$: location = "Location: " + city + ' ' + town;
每当city 或town 发生变化时,location 将会反应性地重新计算并更新DOM,以对应这一变化。
在Vue中,计算的属性被定义在component 对象中,并被分配了命名的函数,这些函数基于数据模型返回一个代码表达式。
export default {
name: 'App',
data(){
return{
city: "",
town: ""
}
},
computed: {
location(){
return this.city + " " + this.town
}
}
}
在这个Vue示例应用程序的节选中,我们创建了一个计算属性并在其中定义了一个location 函数。然后,我们返回了一个表达式,将函数中的city 和town 数据属性连接起来。每当这些数据属性改变时,计算属性将重新计算并更新其在DOM中的值。
注意:
this关键字是用来引用数据属性的。
事件
两个框架都用类似的方式处理事件,使用on 指令,它接受一个由冒号表示的参数,就像bind 指令。
//Svelte
On:click
//Vue
v-on:click
传统上,一个事件处理指令会唤起一个函数,在触发时返回一个表达式。在Svelte中,我们可以像其他函数一样定义这个函数。
const reset = () => {
city = "";
town = "";
}
然而,在Vue中,我们必须在组件模型中创建一个method 属性,就像我们为computed 属性所做的那样,并给它分配一个函数,在事件被触发时返回一个表达式。
<template>
<button ="handleReset">Reset</button>
</template>
...
methods: {
handleReset(){
this.city = ""
this.town = ""
}
}
在上面的例子中,我们创建了一个method 属性,并定义了一个函数,在data 函数中重置了反应式属性,city 和town 。然后,我们将函数名handleReset 赋予了按钮上的事件处理指令。
在Vue中,表达式和函数名是用引号定义或分配给指令的。
<input type="text" v-model="city"/>
像大多数框架一样,函数和代码表达式在Svelte中是用大括号分配给指令的{ } 。
<input type="text" bind:value={town}>
Reactivity
反应性是一种允许我们根据变化进行声明性调整的编程风格,它在JavaScript中并不是开箱即用的。如果我们存储两个变量的总和,然后重新分配这些变量,原来的总和就不会改变。
let a = 10;
let b = 5;
let sum = a + b;
console.log(sum); // sum is 15
b = 15;
console.log(sum); // sum is still 15
要在JavaScript中实现反应性,我们必须检测其中一个值是否有变化,跟踪改变它的函数,并触发该函数,使其能够更新最终值。
大多数JavaScript框架提供了特殊的API函数来处理这个逻辑。例如,为了创建反应式变量,Vue在选项API中使用data 对象,在Composition API中使用ref() 方法。另一方面,React使用useState() Hook,Angular使用detectChanges() 方法。
然而,Svelte并没有使用特殊的API来创建反应性。默认情况下,反应性变量是通过赋值创建的,每一个用let 关键字声明的变量都是自动反应性的。
让我们来看看Vue和Svelte是如何在运行时和构建时分别处理反应性的。
构建时的反应性
要了解Svelte如何在内部处理反应性,我们首先需要了解Svelte编译器的工作原理。
在将Svelte组件编译为命令式JavaScript时,编译器会经历一个流水线过程,就像虚拟DOM一样。唯一的区别是,虚拟DOM在运行时操作,而Svelte编译器在构建时操作。让我们回顾一下整个过程。
在编译过程中,Svelte实现了一个解析器,能够解析HTML元素、逻辑块和条件,但不能解析CSS内容或组件的style 和script 标签中的JavaScript表达式。
每当解析器遇到script 标签或大括号内的任何代码表达式时,它就会把这个操作交给Acorn JavaScript解析器。同样,当解析器遇到style 标签时,Svelte将把操作交给CSS树来处理解析工作。
接下来,编译器会将解析后的代码分解成更小的片段,称为标记。然后,编译器从标记列表中创建一个树状结构,称为抽象语法树(AST)。AST是输入代码的代表。
基于AST,编译器将生成一个代码输出。在静态分析和渲染器阶段,代码输出将被分析并用于生成JavaScript代码。
Svelte静态分析阶段
为了分析创建的AST,编译器将创建一个组件实例,这是一个存储Svelte组件信息的组件类,如反应式变量、编译选项等。这被称为静态分析阶段。
首先,组件类将遍历脚本的AST,并寻找组件中正在声明的所有变量和函数。在我们的例子中,它将发现city 变量、town 变量、reset 函数和location 的计算值。
每当这些变量发生变化时,组件类将把它们标记为reassigned 。接下来,它将遍历模板AST,查找之前收集的变量和函数,将它们标记为references 。
在模板中没有被引用的变量不需要被反应。此外,模板中变量被引用的元素将把这些变量作为依赖关系保留。因此,只要它们在运行时发生变化,这些元素就需要被更新。
最后,Svelte将遍历CSS AST,更新CSS选择器,使其成为组件范围,并对任何未使用的选择器发出警告。
Svelte的渲染阶段
为了生成一个输出代码,Svelte将根据compile 选项创建一个渲染器实例。它将创建一个用于客户端渲染的DOM渲染器或一个用于服务器端渲染的SSR渲染器。
我们的示例应用程序不是SSR应用程序,所以Svelte将使用DOM渲染器来生成运行时代码输出。
要看到运行时的代码,打开你的浏览器的DevTool,导航到 source 标签,或Firefox中的调试器标签。你应该在侧边栏中看到一个build 文件夹。在该文件夹中,有一个bundle.js 文件,其中包含了由编译器生成的所有运行时JavaScript代码。
如果你看一下bundle.js 文件,你会发现它充满了跨越数行的代码块。我们对$$instance 代码块感兴趣,它是包装我们应用程序上下文的函数。
function instance($$self, $$props, $$invalidate) {
let location;
let { $$slots: slots = {}, $$scope } = $$props;
validate_slots('App', slots, []);
let city = "";
let town = "";
const reset = () => {
$$invalidate(0, city = "");
$$invalidate(1, town = "");
};
const writable_props = [];
Object.keys($$props).forEach(key => {
if (!~writable_props.indexOf(key) && key.slice(0, 2) !== '$$' && key !== 'slot') console.warn(`<App> was created with unknown prop '${key}'`);
});
function input0_input_handler() {
city = this.value;
$$invalidate(0, city);
}
function input1_input_handler() {
town = this.value;
$$invalidate(1, town);
}
$$self.$capture_state = () => ({ city, town, reset, location });
$$self.$inject_state = $$props => {
if ('city' in $$props) $$invalidate(0, city = $$props.city);
if ('town' in $$props) $$invalidate(1, town = $$props.town);
if ('location' in $$props) $$invalidate(2, location = $$props.location);
};
if ($$props && "$$inject" in $$props) {
$$self.$inject_state($$props.$$inject);
}
$$self.$$.update = () => {
if ($$self.$$.dirty & /*city, town*/ 3) {
$$invalidate(2, location = "Location: " + city + ' ' + town);
}
};
return [city, town, location, reset, input0_input_handler, input1_input_handler];
}
$$invalidate 方法用于包装instance 函数的不同部分的反应式赋值,当赋值发生变化时通知Svelte。
$$invalidate 方法是一个内部函数,用于促进Svelte的反应性。它将每个变量的值改变后标记为dirty ,然后安排一次更新。Svelte调用update 函数来进行检查,并确定哪个变量需要被更新。
$$self.$$.update = () => {
if ($$self.$$.dirty & /*city, town*/ 3) {
$$invalidate(2, location = "Location: " + city + ' ' + town);
}
};
$$invalidate 方法在reset 和input_handler 函数中被调用,这两个函数是我们应用程序中发生变化的部分。前者是我们在应用程序中创建的用于重置反应式变量的函数,而后者是由Svelte创建的用于将输入字段的值绑定到反应式变量的函数。
const reset = () => {
$$invalidate(0, city = "");
$$invalidate(1, town = "");
};
input_handler 函数是由我们分配给模板中的输入字段的bind 指令创建的。
bind:value = {city}
上面的指令将被编译成以下代码。
function input0_input_handler(){
city = this.value;
}
Vue运行时的反应性
Vue由三个核心模块组成,它们促进了框架的功能,反应性模块、装载模块和渲染器模块。
反应性模块
反应性模块允许我们创建反应性的JavaScript对象,我们可以观察其变化。我们可以在运行依赖这些对象的代码时对其进行跟踪,这样当反应性对象发生变化时就可以在以后重新运行。
挂载模块
mount模块将HTML模板编译成渲染函数。一个渲染函数可以看起来像下面的代码。
render() {
return h(
'h' + this.level, // tag name
{}, // props/attributes
this.$slots.default() // array of children
)
}
渲染器模块
渲染器模块将组件渲染并更新到网页上。这个过程被分成三个不同的阶段。
在渲染阶段,渲染器模块调用渲染函数,它返回一个虚拟的DOM节点,或者叫VNode,它用JavaScript对象表示DOM元素。例如,HTML: <div>logRocket</div> ,可以用VNode表示,如下所示。
{
tag: "div",
children: [
{
text: "logRocket"
}
]
}
在挂载阶段,渲染器接收VNode并进行DOM JavaScript调用,以创建一个网页。
最后,在patch阶段,呈现器获取新旧VNode,对两者进行比较,并只更新网页中发生变化的部分。当一个反应式对象发生变化时,修补阶段由反应式模块触发。
通过使用虚拟DOM,Vue确保这些功能在大型组件树上是可执行的。
为了满足我们前面所说的核心反应性要求,反应性模块将首先根据data 函数中的数据属性创建JavaScript对象。然后,它将把该对象包裹在一个proxy 中,并将其存储为this.$data ,这意味着我们的示例应用程序中的数据属性this.city 和this.town 将变成this.$data.city 和this.$data.town 。前者的数据属性是后者的别名。
Proxy是一个ES6的JavaScript对象,它封装了另一个对象,并允许你拦截与该对象的任何交互。接下来,Proxy将把计算属性location 封装在一个effect 函数中,每当计算属性被访问时就会运行这个函数。该效果将运行location 函数内的表达式,例如,将city 和town 数据属性连接起来。
在这个操作过程中,代理将调用一个handler 函数。在handler 函数内的get 处理程序中使用track 函数,handler 函数将跟踪并记录当前正在运行的效果。
Vue知道将数据属性city 和town 标记为效果的dep ,这意味着location 依赖于city 和town ,因此它们是deps ,依赖关系,location 。
最后,该模块将在handler 函数内创建一个set 处理程序,每当deps 发生变化时,该处理程序将重新运行该效果。在set 处理程序内有一个trigger 函数,它将查找依赖属性的效果,然后启动重新运行的过程。
总结
尽管Svelte带来了很多好处,但很多开发者还没有准备好迁移到它,主要是因为它缺乏灵活性和小的社区支持。
毫无疑问,Svelte是一个高性能的框架,但Vue已经有多年的时间和大量的支持来实现和改进这些方面。Svelte是一个相对较新的框架,仍在努力寻找其在生态系统中的立足点。
希望Svelte的创造者Rich Harris和Vercel之间的合作能将Svelte推向它应有的高度。我希望你喜欢这篇文章!
The postSvelte vs Vue:框架内部的比较首次出现在LogRocket博客上。