本文内容基于:https://cn.vuejs.org/
一、SFC: Single-File Component
1.1 介绍
Vue 的单文件组件 (即 *.vue
文件,英文 Single-File Component,简称 SFC) 是一种特殊的文件格式,使我们能够将一个 Vue 组件的模板、逻辑与样式封装在单个文件中。
Vue 的单文件组件是网页开发中 HTML、CSS 和 JavaScript 三种语言经典组合的自然延伸。<template>
、<script>
和 <style>
三个块在同一个文件中封装、组合了组件的视图、逻辑和样式。
1.2 为什么 Vue 推荐使用 SFC
1. 优势
使用 SFC 必须使用构建工具,但作为回报带来了以下优点:
- 使用熟悉的 HTML、CSS 和 JavaScript 语法编写模块化的组件
- 让本来就强相关的关注点自然内聚
- 支持快速设置组件作用域的 CSS
- 预编译模板,避免运行时的编译开销
- 通过交叉分析模板和逻辑代码能进行更多编译时优化
2. 关注点自然内聚与关注点分离
在上述阐述里,关于第 2 点:一些有着传统 Web 开发背景的用户可能会因为 SFC 将不同的关注点集合在一处而有所顾虑,觉得 HTML/CSS/JS 应当是分离开的!
要回答这个问题,我们必须对这一点达成共识:前端开发的关注点不是完全基于文件类型分离的。前端工程化的最终目的都是为了能够更好地维护代码。关注点分离不应该是教条式地将其视为文件类型的区别和分离,仅仅这样并不够帮我们在日益复杂的前端应用的背景下提高开发效率。
在现代的 UI 开发中,我们发现划分为松散耦合的组件,再按需组合起来是一个更好的模式。因为在一个组件中,其模板、逻辑和样式本就是有内在联系的、是耦合的,将它们放在一起,实际上使组件更有内聚性和可维护性。
当然你不喜欢单文件组件这样的形式,也可以选择拆分单独的 JavaScript 和 CSS 文件:
<template src="./template.html"></template>
<style src="./style.css"></style>
<script src="./script.js"></script>
在上述代码中,我们将 *.vue
组件分散到多个文件中,并且使用 src
这个 attribute 来导入一个外部文件。
3. 预编译模版优化
在上述阐述里,关于第 4 点:SFC 的预编译模板指的是在 Vue 组件中使用模板时,模板会在构建时被预先编译成渲染函数,而不是在运行时动态编译。这样做的目的是为了避免运行时的编译开销。
这一点是相比于传统的 Vue 2.x 版本,在 Vue 2 中如果使用了 Vue 的模板语法,例如 template 标签中的内容,Vue 在运行时会将模板编译成渲染函数。这意味着每次组件被渲染时,都需要进行模板的编译,这会带来一定的性能开销,特别是在组件较多或者嵌套层次较深的情况下。
而在 Vue 3.x 中,通过使用 SFC 的预编译模板,Vue 在构建时会将模板预先编译成渲染函数,并将这些渲染函数与组件逻辑分开存储。这样,在组件被实例化和渲染时,就不需要再进行模板的动态编译,而是直接执行预先编译好的渲染函数,从而避免了运行时的编译开销,提升了组件的渲染性能。
4. 支持交叉分析模板和逻辑代码编译时优化
在上述阐述里,关于第 5 点:SFC 通过交叉分析模板和逻辑代码能进行更多编译时优化,指的是在 Vue 组件中,编译器可以同时分析模板部分和 JavaScript 逻辑部分,并结合两者的信息进行更多的优化操作。
具体来说,编译器可以利用模板和逻辑代码之间的交叉信息来进行以下方面的优化:
-
静态提升(Static Hoisting):编译器可以检测到模板中哪些部分是静态的(不会根据数据变化而改变),并将这些静态部分提升到渲染函数的外部,从而避免在每次重新渲染时重新计算。
-
条件代码消除(Conditional Code Elimination):如果模板中的条件逻辑在编译时就可以确定,编译器可以将不满足条件的代码从渲染函数中移除,以减少运行时的性能开销。
-
事件监听器优化(Event Listener Optimization):编译器可以分析模板中的事件监听器,并根据逻辑代码的情况来优化事件监听器的注册方式,减少不必要的事件监听器或优化事件监听器的绑定方式。
-
动态属性提升(Dynamic Attribute Hoisting):如果模板中的动态属性是通过逻辑代码计算得出的,编译器可以将这些动态属性的计算提升到渲染函数的外部,并将结果缓存起来,从而减少渲染时的计算量。
总的来说,SFC 通过交叉分析模板和逻辑代码能进行更多编译时优化,可以帮助提升 Vue 组件的性能和效率。
1.3 SFC 如何工作(重点)
Vue SFC 是一个框架指定的文件格式,因此必须交由 @vue/compiler-sfc
编译为标准的 JavaScript 和 CSS,一个编译后的 SFC 是一个标准的 JavaScript(ES) 模块,这也意味着在构建配置正确的前提下,你可以像导入其他 ES 模块一样导入 SFC:
import MyComponent from './MyComponent.vue'
export default {
components: {
MyComponent
}
}
注意,SFC 中的 <style>
标签一般会在开发时注入成原生的 <style>
标签以支持热更新,而生产环境下它们会被抽取、合并成单独的 CSS 文件。
如下 vue sfc 示例:
<script setup>
import { ref } from 'vue'
const msg = ref('Hello World!')
</script>
<template>
<h1>{{ msg }}</h1>
<input v-model="msg" />
</template>
它会被编译成:
/* Analyzed bindings: {
"ref": "setup-const",
"msg": "setup-ref"
} */
import { ref } from 'vue'
const __sfc__ = {
__name: 'App',
setup(__props, { expose: __expose }) {
__expose();
const msg = ref('Hello World!')
const __returned__ = { msg, ref }
Object.defineProperty(__returned__, '__isScriptSetup', { enumerable: false, value: true })
return __returned__
}
};
import { toDisplayString as _toDisplayString, createElementVNode as _createElementVNode, vModelText as _vModelText, withDirectives as _withDirectives, Fragment as _Fragment, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock(_Fragment, null, [
_createElementVNode("h1", null, _toDisplayString($setup.msg), 1 /* TEXT */),
_withDirectives(_createElementVNode("input", {
"onUpdate:modelValue": _cache[0] || (_cache[0] = $event => (($setup.msg) = $event))
}, null, 512 /* NEED_PATCH */), [
[_vModelText, $setup.msg]
])
], 64 /* STABLE_FRAGMENT */))
}
__sfc__.render = render
__sfc__.__file = "src/App.vue"
export default __sfc__
这段代码包含了组件的配置对象和渲染函数。
首先,我们来看配置对象 __sfc__
,它包含了组件的名称、setup
函数以及一些其他信息。在 setup
函数中,我们可以看到组件的逻辑代码,其中包含了一个响应式数据 msg
,它使用 ref
函数创建了一个响应式引用。setup
函数最后返回了一个包含了 msg
和 ref
的对象,并通过 Object.defineProperty
方法将属性 __isScriptSetup
设置为 true
,用于标识这个对象是由 setup
函数返回的。
接下来,我们看到了一个名为 render
的函数,它是组件的渲染函数,用于生成虚拟 DOM。在渲染函数中,我们首先调用 _openBlock
函数开启一个块级作用域,然后使用 _createElementBlock
创建了一个根节点。在根节点中,包含了一个 <h1>
标签用于展示 msg
的值,并且使用了 _withDirectives
函数来绑定了一个双向绑定的输入框。
最后,整个 SFC 文件通过 export default
导出了 __sfc__
对象,使得这个组件可以在其他地方被引用和使用。
总的来说,这段代码是一个 Vue 3 单文件组件的编译结果,它包含了组件的配置对象和渲染函数,用于实现组件的逻辑和渲染。
在实际项目中,我们一般会使用集成了 SFC 编译器的构建工具,比如 Vite 或者 Vue CLI (基于 webpack),Vue 官方也提供了脚手架工具来帮助你尽可能快速地上手开发 SFC。
二、组合式 API
2.1 什么是 组合式 API
Vue3 分为:
- 全局 API(包括应用实例、通用)
- 组合式 API(包括 setup、响应式核心、响应式工具、响应式进阶、生命周期钩子、依赖注入)
- 选项式 API、内置内容(包括指令、组件、特殊元素、特殊 Attributes)
- 单文件组件
- 进阶 API(包含渲染函数、服务端渲染、Typescript 工具类型、自定义渲染、编译时标志)
具体可以阅读:https://cn.vuejs.org/api/。
在日常使用中,可以根据需要阅读 Vue3 api 文档。今天接下来这部分主要介绍组合式 API。
组合式 API (Composition API) 是一系列 API 的集合,使我们可以使用函数而不是声明选项的方式书写 Vue 组件。
它是一个概括性的术语,涵盖了以下方面的 API:
-
响应式 API:包括三类,核心api、工具api、进阶api。例如
ref()
和reactive()
,使我们可以直接创建响应式状态、计算属性和侦听器。 -
生命周期钩子:例如
onMounted()
和onUnmounted()
,使我们可以在组件各个生命周期阶段添加逻辑。 -
依赖注入:例如
provide()
和inject()
,使我们可以在使用响应式 API 时,利用 Vue 的依赖注入系统。
下面是一个使用组合式 API 的组件示例:
<script setup>
import { ref, onMounted } from 'vue'
// 响应式状态
const count = ref(0)
// 更改状态、触发更新的函数
function increment() {
count.value++
}
// 生命周期钩子
onMounted(() => {
console.log(`计数器初始值为 ${count.value}。`)
})
</script>
<template>
<button @click="increment">点击了:{{ count }} 次</button>
</template>
2.2 组合式 API 与函数式编程
虽然这套 API 的风格是基于函数的组合,但组合式 API 并不是函数式编程。组合式 API 是以 Vue 中数据可变的、细粒度的响应性系统为基础的,而函数式编程通常强调数据不可变。
2.2 基于函数的组合:组合式函数
1. 有状态逻辑复用
在 Vue 应用的概念中,“组合式函数”(Composables) 是一个利用 Vue 的组合式 API 来封装和复用有状态逻辑
的函数。
注意,是有状态逻辑。
当构建前端应用时,我们常常需要复用公共任务的逻辑。例如为了在不同地方格式化时间,我们可能会抽取一个可复用的日期格式化函数。这个函数封装了无状态的逻辑:它在接收一些输入后立刻返回所期望的输出。复用无状态逻辑的库有很多,比如你可能已经用过的 lodash 或是 date-fns。
相比之下,有状态逻辑负责管理会随时间而变化的状态。
一个简单的例子是跟踪当前鼠标在页面中的位置。在实际应用中,也可能是像触摸手势或与数据库的连接状态这样的更复杂的逻辑。
<script setup>
import { useMouse } from './mouse.js'
const { x, y } = useMouse()
</script>
<template>Mouse position is at: {{ x }}, {{ y }}</template>
上述示例中,通过组合式函数,将公共任务逻辑移到一个外部函数中去,并返回需要暴露的状态。而且和在组件中一样,你可以在组合式函数中使用所有的组合式 API。
2. 嵌套组合式函数
你还可以嵌套多个组合式函数:一个组合式函数可以调用一个或多个其他的组合式函数。
这使得我们可以像使用多个组件组合成整个应用一样,用多个较小且逻辑独立的单元来组合形成复杂的逻辑。实际上,这正是为什么我们决定将实现了这一设计模式的 API 集合命名为组合式 API。
3. 关于组合式函数的最佳实践和使用规范
可以直接阅读官方文档内容。
其中关于输入参数和返回值可以注意一下。
- 对于输入参数
如果你正在编写一个可能被其他开发者使用的组合式函数,最好处理一下输入参数是 ref 或 getter 而非原始值的情况。可以利用 toValue()
工具函数来实现:
import { toValue } from 'vue'
function useFeature(maybeRefOrGetter) {
// 如果 maybeRefOrGetter 是一个 ref 或 getter,将返回它的规范化值。否则原样返回。
const value = toValue(maybeRefOrGetter)
}
以一个异步数据请求 useFetch()
为例:
如果我们想要在 URL 改变时重新 fetch?为了实现这一点,我们需要将响应式状态传入组合式函数,并让它基于传入的状态来创建执行操作的侦听器:
// useFetch() 应该能够接收一个 ref
const url = ref('/initial-url')
const { data, error } = useFetch(url)
url.value = '/new-url'
// 或者接收一个 getter 函数
const { data, error } = useFetch(() => `/posts/${props.id}`)
// useFetch.js
import { ref, watchEffect, toValue } from 'vue'
export function useFetch(url) {
const data = ref(null)
const error = ref(null)
const fetchData = () => {
// reset state before fetching..
data.value = null
error.value = null
fetch(toValue(url))
.then((res) => res.json())
.then((json) => (data.value = json))
.catch((err) => (error.value = err))
}
watchEffect(() => {
fetchData()
})
return { data, error }
}
toValue()
是一个在 3.3 版本中新增的 API。它的设计目的是将 ref 或 getter 规范化为值。如果参数是 ref,它会返回 ref 的值;如果参数是函数,它会调用函数并返回其返回值。否则,它会原样返回参数。它的工作方式类似于 unref()
,但额外对函数有特殊处理。
注意,上面代码中 toValue(url)
是在 watchEffect 回调函数的内部调用的,这确保了在 toValue()
规范化期间访问的任何响应式依赖项都会被侦听器跟踪。
- 对于返回值
我们在编写组合式函数时,往往会使用 ref()
而不是 reactive()
。
我们推荐的约定是:组合式函数始终返回一个包含多个 ref 的普通的非响应式对象,这样该对象在组件中被解构为 ref 之后仍可以保持响应性,比如:
// x 和 y 是两个 ref
const { x, y } = useMouse()
从组合式函数返回一个响应式对象会导致在对象解构过程中丢失与组合式函数内状态的响应性连接。用 ref 则可以维持这一响应性连接。
更多关于解构过程中丢失响应性连接丢失(如props、reactive等场景)我们将在后面的系列会重点介绍。
如果你更希望以对象属性的形式来使用组合式函数中返回的状态,你可以将返回的对象用 reactive()
包装一次,这样其中的 ref 类型的属性会被自动解包,使得你可以直接访问属性的值而不需要使用 .value
。例如:
const mouse = reactive(useMouse())
// mouse.x 链接到了原来的 x ref
console.log(mouse.x) // 而不需要 mouse.x.value
// Mouse position is at: {{ mouse.x }}, {{ mouse.y }}
这里涉及到 reactive 底层实现。当你使用 reactive()
将一个对象进行响应式封装时,它会递归地遍历对象的所有属性,并将每个属性都转换为响应式的。对于 ref
属性来说,reactive()
会将它自动解包,并将其值作为响应式对象的属性值。
以下是一个简单的伪代码示例,用来解释 reactive()
封装 ref
属性的内部工作原理:
function reactive(obj) {
// 创建一个空的响应式对象
const observed = {};
// 遍历对象的所有属性
for (const key in obj) {
// 获取属性值
let value = obj[key];
// 如果属性值是 ref 类型(重点)
if (isRef(value)) {
// 自动解包 ref,并将其值作为响应式对象的属性值
observed[key] = value.value;
} else {
// 对于其他类型的属性,直接赋值给响应式对象
observed[key] = value;
}
}
// 返回响应式对象
return observed;
}
这段伪代码中,reactive()
函数接受一个对象作为参数,然后遍历这个对象的所有属性。对于每个属性,如果它的值是一个 ref
类型,那么就会自动解包,并将其值作为响应式对象的属性值;对于其他类型的属性,则直接赋值给响应式对象。这样,当你使用 reactive()
包装一个对象时,其中的 ref
属性会被自动解包,使得你可以直接访问属性的值而不需要手动解包。
4. vue 组合式函数和其他(如无渲染组件、React hooks)
- 和无渲染组件的对比
一些组件可能只包括了逻辑而不需要自己渲染内容,视图输出通过作用域插槽全权交给了消费者组件。我们将这种类型的组件称为无渲染组件。
这里有一个无渲染组件的例子,一个封装了追踪当前鼠标位置逻辑的组件:
<MouseTracker v-slot="{ x, y }">
Mouse is at: {{ x }}, {{ y }}
</MouseTracker>
虽然这个模式很有趣,但大部分能用无渲染组件实现的功能都可以通过组合式 API 以另一种更高效的方式实现,并且还不会带来额外组件嵌套的开销。
组合式函数相对于无渲染组件的主要优势就是:组合式函数不会产生额外的组件实例开销。当在整个应用中使用时,由无渲染组件产生的额外组件实例会带来无法忽视的性能开销。
推荐在纯逻辑复用时使用组合式函数,在需要同时复用逻辑和视图布局时使用无渲染组件。
- 与 React hooks 的对比
组合式 API 的一部分灵感正来自于 React hooks,Vue 的组合式函数也的确在逻辑组合能力上与 React hooks 相近。然而,Vue 的组合式函数是基于 Vue 细粒度的响应性系统,这和 React hooks 的执行模型有本质上的不同。
- 调用频率
React Hooks 在组件每次更新时都会重新调用。这就产生了一些即使是经验丰富的 React 开发者也会感到困惑的问题。这也
带来了一些性能问题,并且相当影响开发体验。
Vue 组合式 API 仅调用 setup()
或 <script setup>
中的代码一次。
- 调用顺序
Hooks 有严格的调用顺序,并不可以写在条件分支中。
Vue 组合式 API 并不限制调用顺序,还可以有条件地进行调用。
- 依赖收集
在 React 中,当你在 useEffect、useCallback、useMemo 等 Hook 函数中使用变量时,这些变量会被闭包捕获。如果你在依赖数组中传递了错误的依赖,可能会导致依赖不会被更新,发生“过期”行为,从而引发 bug。
当你在 React 组件中使用 useEffect
时,通常你会传递一个依赖数组,以指示在哪些依赖项发生变化时触发 effect。如果你没有正确地设置依赖数组,可能会导致意外的行为。
下面是一个示例,展示了正确和错误地使用依赖数组的区别:
import React, { useState, useEffect } from 'react';
function Component() {
const [count, setCount] = useState(0);
// 错误的使用方式:没有将 count 添加到依赖数组中
useEffect(() => {
console.log('Component rendered');
// 这里的 count 依赖被闭包捕获,但没有在依赖数组中声明
}, []);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
在这个例子中,我们没有将 count
变量添加到 useEffect
的依赖数组中。这意味着 useEffect
依赖的是闭包中的旧值,而不是最新的值,不会在 count
发生变化时重新运行。
为了避免这种问题,React 开发者会依赖 ESLint 规则来帮助他们检测并纠正错误的依赖数组。比如,eslint-plugin-react-hooks
提供了一系列规则,可以帮助开发者检测依赖数组中是否包含了正确的依赖,从而提高代码质量和可维护性。
而 Vue 的响应性系统运行时会自动收集计算属性和侦听器的依赖,因此无需手动声明依赖。
总之,React 中的 hooks 需要解决变量闭包
导致的问题,再结合并发功能,使得很难推理出一段钩子代码是什么时候运行的,并且很不好处理需要在多次渲染间保持引用 (通过 useRef) 的可变状态。
- 性能开销
React 中昂贵的计算需要使用 useMemo,这也需要传入正确的依赖数组。
另外在默认情况下,React 中传递给子组件的事件处理函数会导致子组件进行不必要的更新。子组件默认更新,并需要显式的调用 useCallback 作优化。这个优化同样需要正确的依赖数组,并且几乎在任何时候都需要。忽视这一点会导致默认情况下对应用进行过度渲染,并可能在不知不觉中导致性能问题。
下面是一个示例,展示了不使用 useCallback
和使用 useCallback
两种情况的对比:
import React, { useState } from 'react';
// 子组件
function ChildComponent({ onClick }) {
console.log('ChildComponent rendered');
return <button onClick={onClick}>Click me</button>;
}
// 父组件
function ParentComponent() {
const [count, setCount] = useState(0);
// 错误的使用方式:事件处理函数在每次渲染时都会重新创建
const handleClick = () => {
console.log('Button clicked');
};
// 正确的使用方式:使用 useCallback 包裹事件处理函数
const handleClickOptimized = useCallback(() => {
console.log('Button clicked');
}, []); // 依赖数组为空,表示事件处理函数不依赖于任何外部变量
return (
<div>
<p>Count: {count}</p>
{/* 错误的使用方式 */}
<ChildComponent onClick={handleClick} />
{/* 正确的使用方式 */}
<ChildComponent onClick={handleClickOptimized} />
</div>
);
}
在这个示例中,handleClick
是一个简单的事件处理函数,但它在每次渲染时都会被重新创建。这意味着当父组件重新渲染时,传递给子组件的 onClick
prop 会变化,导致子组件不必要地重新渲染。通过使用 useCallback
来包裹 handleClick
,可以确保它在依赖项不变的情况下保持稳定,避免不必要的子组件更新。
而 Vue 无需手动缓存回调函数来避免不必要的组件更新。Vue 细粒度的响应性系统能够确保在绝大部分情况下组件仅执行必要的更新。对 Vue 开发者来说几乎不怎么需要对子组件更新进行手动优化。
2.3 为什么要有组合式 API
1. 更好的逻辑复用
组合式 API 最基本的优势是它使我们能够通过组合函数来实现更加简洁高效的逻辑复用。在选项式 API 中我们主要的逻辑复用机制是 mixins,而组合式 API 解决了 mixins 的所有缺陷。
mixins 的缺陷:
- 不清晰的数据来源
- 命名空间冲突
- 隐式的跨 mixin 交流
更具体的可以阅读:https://cn.vuejs.org/guide/reusability/composables.html#vs-mixins
组合式 API 提供的逻辑复用能力孵化了一些非常棒的社区项目,比如 VueUse。
2. 更灵活的代码组织
放一张官方的图就理解了:
通过使用组合式 API,相比选项式 API,无需再为了一个逻辑关注点在不同的选项块间来回滚动切换。此外,我们现在可以很轻松地将这一组代码移动到一个外部文件中,不再需要为了抽象而重新组织代码,大大降低了重构成本,这在长期维护的大型项目中非常关键。
3. 更好的类型推导:Typescript
近几年来,越来越多的开发者开始使用 TypeScript 书写更健壮可靠的代码,TypeScript 还提供了非常好的 IDE 开发支持。
然而选项式 API 是在 2013 年被设计出来的,那时并没有把类型推导考虑进去,因此 vue 官方团队不得不做了一些复杂到夸张的类型体操才实现了对选项式 API 的类型推导。但尽管做了这么多的努力,选项式 API 的类型推导在处理 mixins 和依赖注入类型时依然不甚理想。
因此,很多想要搭配 TS 使用 Vue 的开发者采用了由 vue-class-component
提供的 Class API。然而,基于 Class 的 API 非常依赖 ES 装饰器,在 2019 年我们开始开发 Vue 3 时,它仍是一个仅处于 stage 2 的语言功能。我们认为基于一个不稳定的语言提案去设计框架的核心 API 风险实在太大了,因此没有继续向 Class API 的方向发展。在那之后装饰器提案果然又发生了很大的变动,在 2022 年才终于到达 stage 3。另一个问题是,基于 Class 的 API 和上面介绍的选项式 API 在逻辑复用和代码组织方面存在相同的限制。
相比之下,组合式 API 主要利用基本的变量和函数,它们本身就是类型友好的。用组合式 API 重写的代码可以享受到完整的类型推导,不需要书写太多类型标注。
大多数时候,用 TypeScript 书写的组合式 API 代码和用 JavaScript 写都差不太多!这也让许多纯 JavaScript 用户也能从 IDE 中享受到部分类型推导功能。
4. 更小的生产包体积:this 属性访问无法压缩问题
搭配 <script setup>
使用组合式 API 比等价情况下的选项式 API 更高效,对代码压缩也更友好。
这是由于 <script setup>
形式书写的组件模板被编译为了一个内联函数,和 <script setup>
中的代码位于同一作用域。不像选项式 API 需要依赖 this
上下文对象访问属性,被编译的模板可以直接访问 <script setup>
中定义的变量,无需从实例中代理。这对代码压缩更友好,因为本地变量的名字可以被压缩,但对象的属性名则不能。
三、sfc: script setup
在 Vue 3 中,组合式 API 基本上都会配合 <script setup>
语法在单文件组件中使用。
优势
<script setup>
是在单文件组件 (SFC) 中使用组合式 API 的编译时语法糖。相比于普通的 <script>
语法,它具有更多优势:
- 更少的样板内容,更简洁的代码。
- 能够使用纯 TypeScript 声明 props 和自定义事件。
- 更好的运行时性能 (其模板会被编译成同一作用域内的渲染函数,避免了渲染上下文代理对象)。
- 更好的 IDE 类型推导性能 (减少了语言服务器从代码中抽取类型的工作)。
对于第 4 点:这句话具体来说,使用传统的 <script>
语法编写 Vue 组件时,语言服务器(如 TypeScript 的服务端、VS Code 的 IntelliSense 等)需要对整个 <script>
部分进行分析,包括变量、函数、组件选项等,并提取其中的类型信息,以便在编辑器中提供准确的代码提示和类型检查。
而使用 <script setup>
语法后,由于代码结构更加简洁和明确,语言服务器可以更轻松地抽取到类型信息,因为所有的逻辑都封装在 setup
函数中,而不需要在顶层定义变量或函数。
这样一来,语言服务器可以更快速地分析代码并提供更好的代码提示和类型推导,从而提高 IDE 的性能和响应速度。下面我们通过代码来看可能更好理解。
当使用传统的 <script>
语法编写 Vue 组件时,我们通常会像下面这样定义组件的选项和逻辑:
<template>
<div>{{ message }}</div>
</template>
<script>
export default {
data() {
return {
message: 'Hello, Vue!'
};
},
methods: {
greet() {
alert(this.message);
}
}
};
</script>
在这个例子中,我们使用了 data
函数来定义响应式数据 message
,并且在 methods
中定义了一个方法 greet
。语言服务器需要从 <script>
部分中分析出 message
和 greet
的类型信息,以提供准确的代码提示和类型检查。
而使用 <script setup>
语法后,可以将代码重构为以下形式:
<template>
<div>{{ message }}</div>
</template>
<script setup>
const message = 'Hello, Vue!';
function greet() {
alert(message);
}
</script>
在这个例子中,我们直接在 <script setup>
中定义了变量 message
和函数 greet
,不需要再显式地导出组件选项。这样一来,语言服务器可以更轻松地从 setup
函数中抽取类型信息,因为所有的逻辑都集中在一个地方。这样,IDE 可以更快速地分析代码并提供更好的代码提示和类型推导,从而提高开发效率。
总的来说,Vue 3 中的 <script setup>
语法通过简化组件代码结构,减少语言服务器从代码中抽取类型的工作量,从而提高了 IDE 的类型推导性能,使开发者能够更流畅地编写和编辑 Vue 组件。
关于 LSP 更多内容,可以阅读 WHAT - Web 代码编辑器(含 LSP - Language Server Protocol)