1. 新的单文件组件功能
<script setup>
是一种编译时语法糖,可在SFC (单文件组件,也就是我们常说的.vue 文件) 内使用 Composition API 时极大地提升工作效率。<style> v-bind
在 SFC 标签中启用组件状态驱动的动态 CSS 值。<style>
<template>
<div class="button" @click="color = color==='red'?'green':'red'">
color is:{{color}}
</div>
</template>
<script setup lang="ts">
import {ref} from 'vue'
const color = ref('red')
</script>
<style lang="less" scoped>
.button{
color:v-bind(color)
}
</style>
1. <script setup>
不再需要 return
任何变量或者方法,定义后可直接在模板中使用
2. <script setup>
引入的子组件不再需要注册,可以直接在模板中使用
<script setup>
是普通的 <script>
的语法糖,编译的过程:
编译结果:
1. <script setup> 语法糖最终编译成了普通的 <script>
2. <script setup> 的内容都编译成了组件 setup() 函数的内容
3. 模板内容都编译成了 VNode 结构并在 setup() 函数中导出,并且对模板中的值进行了引用
<script setup>
语法糖和普通的 <script>
相比又有哪些优势呢?
1. 普通的 <script> 只在组件被首次引入的时候仅执行一次不同,<script setup> 中的代码会在每次组件实例被创建的时候执行。这一点非常的重要,也就是写在 <script setup> 中的代码,例如初始化的赋值等在组件每次实例创建时都重新执行一次。
2. 当使用 <script setup> 的时候,任何在 <script setup> 声明的顶层的绑定 (包括声明的变量,函数声明,以及 import 引入的内容) 都能在模板中直接使用,不再需要使用 return 导出。
在一个 SFC 是支持 <script setup>
与 <script>
同时存在的,普通的 <script>
的内容会和 <script setup>
的内容进行 merge.
原理探究
以下为上述 SFC 编译后的 JS 代码。
可以看到,编译后 <script setup> 语法糖变成了 <script> 并导出了模板 VNode 结构的函数,并且将模板中用到的值进行了引用和自动解包。这就是为什么不再需要显示 return 的原因了。感兴趣可在 Vue SFC Playground 中测试。
/* Analyzed bindings: {
"ref": "setup-const",
"color": "setup-ref"
} */
import { useCssVars as _useCssVars, defineComponent as _defineComponent } from 'vue'
import { toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
import {ref} from 'vue'
const __sfc__ = /*#__PURE__*/_defineComponent({
setup(__props) { // 编译成了普通的 script
_useCssVars(_ctx => ({
"f13b4d11-color": (color.value)
}))
const color = ref('red')
// 可以看到,编译后 `<script setup>` 语法糖变成了<script> 并导出了模板 VNode 结构的函数,并且将模板中用到的值进行了自动解包
return (_ctx,_cache) => {
return (_openBlock(), _createElementBlock("div", {
class: "button",
onClick: _cache[0] || (_cache[0] = ($event) => (color.value = color.value==='red'?'green':'red'))
}, " color is:" + _toDisplayString(color.value), 1 /* TEXT */))
}
}
})
__sfc__.__scopeId = "data-v-f13b4d11"
__sfc__.__file = "App.vue"
export default __sfc__
可以看到编译后的结果中包含了大量的 vue 内置方法和导出 sfc,这属于框架内部的执行,所以所有的 SFC 编译时都会有这些代码。
import { useCssVars as _useCssVars, defineComponent as _defineComponent } from 'vue'
import { toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
__sfc__.__file = "App.vue"
export default __sfc__
2 Web Components
Vue 3.2 引入了一个新的 defineCustomElement 方法,可以使用 Vue 组件 API 轻松创建原生自定义元素:
import { defineCustomElement } from 'vue'
const MyVueElement = defineCustomElement({
// 正常的 vue 组件写法
props: {},
emits: {},
template: `...`,
// 只用于 defineCustomElement:注入到 shadow root 中的 CSS
styles: [`/* inlined css */`]
})
// 注册自定义元素.
// 注册后,页面上所有的 `<my-vue-element>` 标签都会更新
customElements.define('my-vue-element', MyVueElement)
这个 API 允许开发者创建由 Vue 驱动的 UI 组件库,这个 UI 库可以与任何框架一起使用,或者根本没有框架,因为最终会编译成原生的 Web Components.
3 性能提升
由于 @basvanmeurs 的出色工作,3.2 对 Vue 的响应式系统进行了一些重大的性能改进。 具体来说:
1. 更高效的 ref 实现(提高约 260% 的读取速度/约 50% 的写入速度)
2. 提高约 40% 的依赖跟踪速度
3. 内存使用量减少约 17%
模板编译器也得到了一些改进:
1.创建普通元素 VNode 的速度提高了约 200%
2. 其它的提升 [1] [2]
最后,还引入了一个新的 v-memo 指令,它提供了记忆一部分模板树的能力。 v-memo 指令使得这部分模板可以跳过虚拟 DOM 的 diff 比较,同时还完全跳过新 VNode 的创建。 虽然很少需要,但它提供了一种在某些情况下想要得到最大性能的方案,例如大型 v-for 列表。
直接单行添加 v-memo 即可生效,这也使得 Vue 成为 js-framework-benchmark 中最快的主流框架
4 服务端渲染
3.2 版本中的 @vue/server-renderer 提供了一个 ES 模块构建包,它与 Node.js 内置模块分离。 这使得在非 Node.js 运行时环境中构建和使用 @vue/server-renderer 称为可能,(例如在 CloudFlare Workers 或 Service Workers)。
我们还改进了流式渲染 API (streaming render APIs),提供了用于渲染到 Web Streams API 的新方法。 查看 @vue/server-renderer 的文档以获取更多详细信息。
5 Effect 作用域 API
3.2版本还 、引入了一个新的 Effect Scope API,用于直接控制响应式 API 的(computed and watchers)执行时机。 它可以使得更轻松地在组件上下文之外使用 Vue 的响应式 API,同时还包括了组件内部的一些高级用例。
Effect 作用域是一个高阶的 API,主要服务于库作者,因此建议阅读该功能的 RFC 以了解此功能的动机和用例。
props 的使用——defineProps
为了在 <script setup> 中 声明 props ,必须使用 defineProps API,这是一个宏命令,不需要导入,直接可在 <script setup> 使用且只能在 <script setup> 中使用,有两种方式可以使用这个宏命令类声明 props,运行时声明和类型声明式,不同的方式下使用这个宏命令后 props 将具备不同的类型推断。
使用运行时声明(runtime declaration)
defineProps 运行时声明的基本用法如下,仅支持运行时的校验。
<script setup lang='ts'>
const props = defineProps({
foo: String,
bar: {
type: Number,
required: true
}
})
</script>
类型声明(type declaration)
defineProps 类型声明的基本用法如下,完美的支持 IDE 的类型推断和检查。
<script setup lang='ts'>
const props = defineProps<{
foo?: string
bar: number
}>()
</script>
编译后的结果如下:
const __sfc__ = /*#__PURE__*/_defineComponent({
props: {
foo: { type: String, required: false },
bar: { type: Number, required: true }
},
setup(__props) {
const props = __props
return () => {}
}
})
从编译后的结果可以看到,两种方式最终都编译成了普通的 <script> 下的 props 模式,并且结果几乎完全一致。不同的在于是否完美的支持 IDE 的类型推断和检查。
需要注意的点
不能同时使用运行时声明和类型声明
defineProps 只能是要么使用运行时声明,要么使用类型声明。同时使用两种声明方式会导致编译报错
使用类型声明的时候,静态分析(也就是约束的类型) 会自动生成等效的运行时声明,以确保正确的运行时行为
截至目前,类型声明参数必须是以下内容之一,以确保正确的静态分析:
1. 类型字面量,如 string, number, boolean 等
2. 在同一文件中的 interface 或类型字面量的引用
说得更通俗一些就是,props 的 ts 接口只能写在本文件中,如下所示
<script setup lang="ts">
// 暂不支持引入,因为 setup 语法糖会将 List 编译成一个变量,因此只能在文件内写
// import { List } from "./type";
interface List {
id: number,
content: string,
isDone: boolean,
};
const props = defineProps<{
title: string,
list: List[], // ts 接口
}>();
</script>
现在暂时还不支持复杂的类型和从其它文件进行类型导入。理论上来说,将来是可能实现类型导入的。
但是,因为 ts 会自动扫描项目中的 types 来自动导入类型,因此可以将 interface 通过 namespace 的方式来实现自动导入,这样就不需要在文件中引入,直接就可以使用了
示例如下,
types 文件夹下的 list.ts 文件
declare namespace List {
export interface Basic {
id: number,
content: string,
isDone: boolean,
}
}
// 这样是可以支持的
const props = defineProps<{
title: string,
list: List.Basic[],
}>();
在开发环境下, IDE 会试着从类型声明来推断对应的运行时声明。
例如这里从 foo: string 类型中推断出 foo: String。但如果类型声明使用的是对导入类型的引用(例如自定义的 interface),这里的推断结果会是 foo: null (与 any 类型相等),因为 IDE 没有外部文件的信息。因此,使用导入类型的引用的类型声明运行时是没有校验的,推断成 null 了
在生产模式下,IDE 会生成数组格式的声明来减少打包体积 (这里的 props 会被编译成 ['foo', 'bar'])。
生成的代码仍然是有着类型的 ts 代码,它会在后续的流程中被其它工具处理。
2.4.5 运行时声明和类型声明的比较
类型 | 优势 | 劣势 |
运行时声明 | 不使用 ts 的情况下能够对 props 进行一定的、运行时的类型校验 | 1. 运行时校验 2. 只能进行基本类型的校验 3. 编码时无任何提示 |
类型声明 | 完美的支持类型的校验,包括props 的完美类型约束、父组件在传 props 时的提示以及子组件在使用 props 的提示 | 目前 ts 的接口暂时只支持写在文件内,未来应该会实现可从外部导入的,但目前可通过ts自动扫描types来解决 |
因此,强烈推荐使用类型声明的 defineProps。
2.5 props 的默认值 —— widthDefaults
defineProps 使用类型声明时的不足之处在于,它没有可以给 props 提供默认值的方式。为了解决这个问题,提供了 withDefaults 宏命令。
2.5.1 基本用法
<script setup lang="ts">
const props = withDefaults(defineProps<{
title?: string,
list?: List.Basic[],
}>(), {
title: 'Hello withDefaults',
list: () => [{ id: 3, content: '3', isDone: false }],
});
上面代码会被编译为等价的运行时 props 的 default 选项,如下所示。此外,withDefaults 辅助函数提供了对默认值的类型检查,并确保返回的 props 的类型删除了已声明默认值的属性的可选标志。
const __sfc__ = /*#__PURE__*/_defineComponent({
props: {
title: { type: String, required: false, default: 'Hello withDefaults' },
list: { type: Array, required: false, default: () => [{ id: 3, content: '3', isDone: false }] }
},
setup(__props) {
const props = __props
return () => {}
}
})
注意点
widthDefaults 是为了给 defineProps 使用类型声明时提供添加默认值的的方法,因此,需要注意这仅仅适用于 <script setup lang='ts'> 且 defineProps 使用类型声明。
自定义事件 —— defineEmits
在 <script setup> 中 声明 emit ,必须使用 defineEmits API,这也是一个宏命令。同样可采用运行时声明和类型声明式,在类型声明下 emit 将具备完美的类型推断。
运行时声明
<script setup lang="ts">
// 这样是没有任何的类型检查的
const emit = defineEmits(['handleClick', 'handleChange']);
const handleClick = () => emit('handleClick', Date.now()+'');
const handleChange = () => emit('handleChange', Date.now());
</script>
类型声明式
<script setup lang="ts">
interface Click {
id: string,
val: number,
}
// 完美的类型检查
// List.Basic 是基于 ts 自动扫描 types 文件夹以及 delcare namespace 自动导入的
const emit = defineEmits<{
(e: 'handleClickWithTypeDeclaration', data: Click): void,
(e: 'handleChangeWithTypeDeclaration', data: List.Basic): void,
}>();
const handleClickWithTypeDeclaration = () => emit('handleClickWithTypeDeclaration', { id: '1', val: Date.now() });
const handleChangeWithTypeDeclaration = () => emit('handleChangeWithTypeDeclaration', {
id: 1,
content: 'change',
isDone: false,
});
</script>
跟 defineProps 一样,运行时声明和类型声明式同样不可同时使用,且类型声明只能用于在 ts 环境下。
显示的暴露 —— defineExpose
基本使用
官方文档指出默认情况下使用 <script setup> 的组件是默认关闭的,也就是说通过模板 ref 或者 $parent 链获取到的组件的实例,并不会暴露任何在 <script setup> 中声明的绑定(变量,函数)。
为了在 <script setup> 组件中明确要暴露出去的属性,那么就需要使用 defineExpose 这个宏命令。
<script setup>
import { ref } from 'vue'
const a = 1
const b = ref(2)
defineExpose({
a,
b
})
</script>
当父组件通过模板 ref 的方式获取到当前组件的实例,获取到的实例会像这样 { a: number, b: number } (ref 会和在普通实例中一样被自动解包)
7.2 遇到的有趣的地方
在我的实验过程中,我居然发现,不管有没有显示暴露,都可以拿到!!很奇怪!!
不知道大家能不能猜到是因为什么原因,我在 <script setup> 中并没有显示暴露,但是却能获取到呢?
可以看看 DefineExposeWithNormalScript.vue 文件便知道了。
原来是因为我不仅仅使用了 <script setup>,同时为了给组件命名,还添加了一个普通的 <script> 标签,我们都知道,当仅仅使用 <script> 时,是需要在 <setup> 函数中显示得使用 return 才能暴露的,但是这里为何添加了一个 普通的 <script> 标签后就全量的暴露了,我也检查了编译后的代码,暂未找出具体的原因,恐怕需要查看源码才能真正的看透其中奥秘了。
首先,使用 ref 等方式去获取组件实例的方法或者组件属性本身就不是推荐的(vue react 官方都有提及,ref 的方式并不推荐),所以 defineExpose 使用的频率不高
即便是要向外暴露方法或者属性,那么也并不需要暴露太多,所以 defineExpose 完全能满足业务需求
不过,这个实验给我们提供了一个办法,那就是当需要向外暴露很多数据或者方法时(当然,这种情况很少见,就当是钻vue 中一个有趣的空子玩玩好了),使用 defineExpose 这种标准的暴露方式当然是可行的,但是过多了我们又不想写,那么可以采取这种”巧妙的办法“来全量暴露,但至于这种暴露方式是否有什么缺陷,还有待验证。
useSlots 和 useAttrs
在 <script setup> 使用 slots 和 attrs 的情况应该是很罕见的,因为可以在模板中直接可通过 $slots 和 $attrs 来访问它们。
在那些罕见的需要使用它们的场景中,可以分别用 useSlots 和 useAttrs 两个函数来获取到对应的信息:
<script setup lang="ts">
import { useSlots, useAttrs } from "vue";
const slot = useSlots();
console.log('TestUseSlots', slot.header && slot.header()); // 获取到使用插槽的具体信息
const attrs = useAttrs();
console.log('TestUseAttrs', attrs); // 获取到使用组件时传递的 attributes
</script>
<template>
<h1> Here is slots test!!</h1>
<slot name="header"></slot>
</template>
useSlots 和 useAttrs 是真实的运行时函数,它会返回与 setupContext.slots 和 setupContext.attrs 等价的值,同样也能在普通的 composition API 中使用。
与普通的 < script > 一起使用
<script setup> 可以和普通的 <script> 一起使用。普通的 <script> 在有这些需要的情况下或许会被使用到:无法在 <script setup> 声明的选项,例如 inheritAttrs 或通过插件启用的自定义的选项。
显示定义组件的名称。
运行副作用或者创建只需要执行一次的对象。
<script>
// 普通 <script>, 在模块范围下执行(只执行一次)
runSideEffectOnce()
// 声明额外的选项
export default {
inheritAttrs: false,
customOptions: {}
}
</script>
<script setup>
// 在 setup() 作用域中执行 (对每个实例皆如此)
</script>
实验得知,如果同时使用 <script setup> 和 <script> ,那么将打破 <script setup> 的默认关闭(即外部无法获取组件内部的属性和方法),此时,组件内部的属性和方法都将在外部可获取到,如 ref.xxx
顶层 await
await 的使用必须是要在async 语法糖的包裹下,否者将无法执行,为了更简化代码, <script setup> 中可以使用顶层 await。
<script setup>
const post = await fetch(`/api/post/1`).then(r => r.json())
</script>
上述代码编译后的结果如下,可以看到编译后的结果不再是 setup 了,而是带有 async 的 setup(), 因此便可以直接在 <script setup> 中使用顶层的 await 了:
const __sfc__ = {
async setup(__props) { // 不再是 setup, 而是 async setup
let __temp, __restore
const post = (([__temp,__restore]=_withAsyncContext(()=>(fetch(`/api/post/1`).then(r => r.json())))),__temp=await __temp,__restore(),__temp)
return () => {}
}
}
另外,await 的表达式会自动编译成在 await 之后保留当前组件实例上下文的格式。
注意: async setup() 必须与 Suspense 组合使用,Suspense 目前还是处于实验阶段的特性。vue 官方提到,在将来的某个发布版本中将开发完成并提供文档 - 如果你现在感兴趣,可以参照 tests 看它是如何工作的。
如果你了解 React 的话,一定知道 React 中有一个<Suspense> 内置组件, 这个组件主要是在组件完成前实现 loading 效果,因为有的组件是需要等待异步结果才渲染的,所以需要一个 loading 过程,那么 vue 这里提到的 "async setup() 必须与 Suspense 组合使用" ,其思想应该是一致的,因为默认情况下 vue 会认为 async setup() 中一定存在顶层的 await 异步,为了更好的交互体验,强制添加一个 Suspense 组件以显示 loading。
所以,思想才是关键,做法是次要的
限制使用src 导入
SFC 的三个模块都可以通过 src 的方式进行导入,如下所示:
<template src="./template.html"></template>
<style src="./style.css"></style>
<script src="./script.js"></script>
但是在 <script setup>下强烈建议不使用 Src 导入。
由于模块执行语义的差异,<script setup> 中的代码依赖单文件组件的上下文。当将其移动到外部的 .js 或者 .ts 文件中的时候,对于开发者和工具来说都会感到混乱。因而 <script setup> 不能和 src attribute 一起使用。
< style > v-bind 新特性
scoped 跟 vue2.x 的设计和使用完全是一样的,因此不再赘述。
style module
设计和使用上跟 Vue2.x 是一致的,因此也不多赘述。
唯一新的点是使用 <script setup> 时,可以使用 useCssModule API 获取到 css module 对象。
<script setup lang="ts">
import { useCssModule } from "vue";
const css = useCssModule();
console.log(css); // { blue: "_blue_13cse_5", red: "_red_13cse_2"}
</script>
<style module>
.red {
color: red;
}
.blue {
color: blue;
}
</style>
3.2 状态驱动的动态 CSS
3.2.1 基本使用
单文件组件的 <style> 标签可以通过 v-bind 这一 CSS 函数将 CSS 的值关联到动态的组件状态上,有了这一特性,可以将大量的动态样式通过状态来驱动了,而不是写动态的 calss 类名或者获取 dom 来动态设置了。
<script setup lang="ts">
import { ref } from "vue";
const color = ref('red');
setTimeout(() => color.value = 'blue' , 2000);
</script>
<template>
<p>hello</p>
</template>
<style scoped>
p {
color: v-bind(color);
}
</style>
实际的值会被编译成 hash 的 CSS 自定义 property,CSS 本身仍然是静态的。自定义 property 会通过内联样式的方式应用到组件的根元素上,并且在源值变更的时候响应式更新。
上述代码编译后的结果如下,可以看到,编译后的代码会维护一份 hash 值和源值的映射,hash 值用于 css var 函数获取自定义属性,hash 映射源值并保留响应式:
不知道你是否有使用或者听说过 css 的 var 函数,小生我是没有的,因此会在下一小节简单的描述一下
编译后的 css
p[data-v-f13b4d11] {
color: var(--f13b4d11-color); /* 通过 css 的 var 函数去获取到自定义属性的值 */
}
编译后的 js
const __sfc__ = /*#__PURE__*/_defineComponent({
setup(__props) {
_useCssVars(_ctx => ({
"f13b4d11-color": (color.value) // 可以看到,编译后的值 和 一个 hash 值映射,并且具备响应式, css 的 var 便可以获取到这个 hash 映射的值
}))
const color = ref('red');
setTimeout(() => color.value = 'blue' , 2000);
return (_ctx,_cache) => {
return (_openBlock(), _createElementBlock("p", null, "hello"))
}
}
})
小解析——css var 函数探究
css in js这一新特性
如上所述,在 style 中使用 v-bind 来使用状态以达到动态 css 的目的,最终编译的结果是 vue 维护了 一份 hash 值和源值的映射,hash 值用于 css var 函数获取自定义属性,这个通过代码一眼就能够理解,不太理解的就是这个 css var 函数,先给出总结: var 函数能够将参数替换成预先定义的值。
下面来简单描述一下,相信看完后你就能完全的理解状态驱动的动态 css 的原理
什么是var 函数
**var()函数可以代替元素中任何属性中的值的任何部分。var()**函数不能作为属性名、选择器或者其他除了属性值之外的值(这样做通常会产生无效的语法或者一个没有关联到变量的值)。
语法
方法的第一个参数是要替换的自定义属性的名称。函数的可选第二个参数用作回退值。如果第一个参数引用的自定义属性无效,则该函数将使用第二个值。
var( <custom-property-name> , <declaration-value>? )
注意:自定义属性的回退值允许使用逗号。例如, var(--foo, red, blue) 将red, blue同时指定为回退值;即是说任何在第一个逗号之后到函数结尾前的值都会被考虑为回退值
值
<custom-property-name> 自定义属性名
在实际应用中它被定义为以两个破折号开始的任何有效标识符。 自定义属性仅供作者和用户使用; CSS 将永远不会给他们超出这里表达的意义。
<declaration-value> 声明值(后备值)
回退值被用来在自定义属性值无效的情况下保证函数有值。回退值可以包含任何字符,但是部分有特殊含义的字符除外,例如换行符、不匹配的右括号(如)、``]或``})、感叹号以及顶层分号(不被任何非var**()**的括号包裹的分号,例如var(--bg-color, --bs**;**color)是不合法的,而var(--bg-color, --value**(**bs;color**)**)是合法的)。
示例
在 :root 上定义,然后使用它
:root {
--main-bg-color: pink;
}
body {
background-color: var(--main-bg-color);
}
当第一个值未定义,回退值生效
/* 后备值 */
/* 在父元素样式中定义一个值 */
.component {
--text-color: #080; /* header-color 并没有被设定 */
}
/* 在 component 的样式中使用它: */
.component .text {
color: var(--text-color, black); /* 此处 color 正常取值 --text-color */
}
.component .header {
color: var(--header-color, blue); /* 此处 color 被回退到 blue */
}
兼容性
在 caniuse 上查看结果如下,整体来说兼容性还是不错的。虽然已经明确不再支持 IE,但相信我们的尤大大肯定还是有做兼容处理的.