Preface#
Hi, this is Tomoko Misaki, a member of the Vue core team. The release of Vue 3.3 mainly aims to improve DX (developer experience), introducing some syntactic sugar and macros, as well as improvements in TypeScript.
- Generic components
- Importing external TS types in SFC (Single File Components)
defineSlots
to define the types of slotsdefineEmits
with more convenient syntaxdefineOptions
to define component options- (Experimental) Reactive props destructuring
- (Experimental)
defineModel
syntactic sugar - Deprecated Reactivity Transform
During the time I just joined Vue last year, I was contributing PRs for Vue 3, but it was only recently that they finally landed in Vue 3.3. Vue 3.3 has absorbed five or six features from Vue Macros, and today I will briefly discuss the parts I contributed.
defineOptions Macro#
- PR: https://github.com/vuejs/core/pull/5738
- RFC: https://github.com/vuejs/rfcs/discussions/430
- Development record: https://www.bilibili.com/video/BV1uu411y7WE
Background#
Think about the time before we had <script setup>
. If we wanted to define props
and emits
, we could easily add an attribute at the same level as setup
. However, after using <script setup>
, we can no longer do that— the setup
attribute is gone, and naturally, we cannot add attributes at the same level. To solve this problem, we introduced the two macros defineProps
and defineEmits
.
But this only solved the props
and emits
attributes. If we want to define the component's name
, inheritAttrs
, or other custom attributes, we still have to revert to the original usage—adding a regular <script>
tag. This would lead to having two <script>
tags, which is unacceptable for me.
- Two
script
tags may cause unexpected issues with ESLint plugins or Volar; - If both
script
tags contain import statements, Vue will perform some strange special handling; - For DX (developer experience), it is not only troublesome but also hard to understand.
Current Situation#
So we introduced the defineOptions
macro in Vue 3.3. As the name suggests, it is mainly used to define options for the Options API. We can use defineOptions
to define any options, except for props
, emits
, expose
, and slots
(because these can be done using defineXXX). We can even write the render function render
directly in defineOptions
using the h function or JSX without a <template>
tag (though this usage is not recommended).
🌰 Example#
<script setup>
defineOptions({
name: 'Foo',
inheritAttrs: false,
// ... more custom attributes
})
</script>
The Story Behind#
The origin of this feature came from refactoring the components of Element Plus to <script setup>
. For component libraries, we want to customize the component name (name
attribute) instead of using the default file name. But I didn't want to revert to the original writing style, so I wrote a plugin (the beginning of a dream 🤣) unplugin-vue-define-options
. After several modifications, we finalized and implemented the current defineOptions
macro in Vue 3.3.
Enhancing Static Constants#
- PR: https://github.com/vuejs/core/pull/5752
- Development record: https://www.bilibili.com/video/BV1st4y1G7sG
This feature is an optimization for the SFC (Single File Component) compiler. A new hoistStatic
option has been added to the script
section.
hoistStatic
in Templates#
The hoistStatic
under template
is fundamentally similar. The Vue compiler has an optimization—it can hoist static element nodes to the top-level scope. This means it is executed only once when the code is loaded, rather than being repeatedly executed every time the render
function is called (which may have its downsides in extreme cases).
Let's look at an example 🌰.
<template>
<div id="title">Hello World</div>
</template>
The above code will be compiled into the following JavaScript (non-key code omitted)
const _hoisted_1 = { id: 'title' }
function render(_ctx, _cache) {
return _openBlock(), _createElementBlock('div', _hoisted_1, 'Hello World')
}
We can see that the _hoisted_1
variable is deliberately hoisted to the top level by the compiler. If this feature is turned off, it will be inside the render function.
hoistStatic
in Script Tags#
Before Vue 3.3, there was only the above feature. In Vue 3.3, we also made a similar optimization—if the value of a constant is a primitive value (basic types, namely string, number, boolean, bigint, symbol, null, undefined), then the declaration of that constant will be hoisted to the top level. (Note: symbol has not been implemented yet)
Since these types of constants cannot be changed, their effect is the same regardless of where they are declared.
Purpose#
This feature, in addition to performance optimization, has another useful aspect. Before Vue 3.3, when using macros, we could not pass variables defined in the <script setup>
block. Let's look at the following example.
<script setup>
const name = 'Foo'
defineOptions({
name,
})
</script>
We would get an error.
[@vue/compiler-sfc] `defineOptions()` in <script setup> cannot reference locally declared variables because it will be hoisted outside of the setup() function. If your component options require initialization in the module scope, use a separate normal <script> to export the options instead.
This is due to the reason mentioned earlier; defineProps
adds a props
attribute at the same level as setup
, while the name
constant is declared inside the setup
function. We cannot reference a variable declared inside a function from outside because that variable has not been initialized yet. The following code is undoubtedly incorrect.
const __sfc__ = {
props: [propName],
setup(__props) {
const propName = 'foo'
},
}
After Vue 3.3, the propName
on line 4 will be hoisted to line 1, making the code completely logical. This feature is enabled by default. In general, developers do not need to be aware of the existence of this feature.
The Story Behind#
The reason for developing this feature is similar to the previous one. It was because Element Plus needed to throw an exception under certain conditions after setting the name
with defineOptions
. The exception needed to carry the component's name to facilitate user debugging.
<script setup>
const name = 'ElButton'
defineOptions({
name,
})
// ...
if (condition) {
throw new Error(`${name}: something went wrong.`)
}
</script>
So I wanted to avoid duplicate code by extracting the component name into a constant and referencing it in both defineOptions
and when throwing the exception.
defineModel Macro#
- PR: https://github.com/vuejs/core/pull/8018
- RFC: https://github.com/vuejs/rfcs/discussions/503
- Development record: Part 1, Part 2
- Twitter: https://twitter.com/sanxiaozhizi/status/1644564064931307522
Motivation#
This macro is purely syntactic sugar. Before Vue 3.3, defining a two-way bound prop was quite a hassle.
<script setup lang="ts">
const props = defineProps<{
modelValue: number
}>()
const emit = defineEmits<{
(evt: 'update:modelValue', value: number): void
}>()
// Update value
emit('update:modelValue', props.modelValue + 1)
</script>
We need to define props
first, then define emits
. There is a lot of repetitive code. If we need to modify this value, we also have to manually call the emit
function.
I thought: why not encapsulate a function (macro) to simplify this step? Thus, the defineModel
macro was born.
🌰 Example#
<script setup>
const modelValue = defineModel()
modelValue.value++
</script>
The above cumbersome 7 lines of code can now be simplified to two lines in Vue 3.3!
Difference from useVModel
#
There is also a useVModel
function in VueUse that can achieve similar effects. Why do we still need to introduce this macro?
This is because VueUse merely combines props
and the emit
function into a Ref
, and it cannot define props
and emits
for the component. This means developers still need to manually call defineProps
and defineEmits
.
The Story Behind#
😛 There isn't much of a story behind this; it was purely because I found it cumbersome to write. Initially, I created the defineModel
macro in Vue Macros (now renamed to defineModels
to distinguish it from the official version), and it worked quite well.
Importing External Types in SFC#
Background#
Since the release of Vue 3.2, there has been a highly discussed issue—how to use external types on defineProps.
To solve this problem, we had two paths ahead of us.
- The Vue SFC compiler calls the TypeScript compiler to compute the final types and determine what types it actually includes (
String
,Number
,Boolean
,Function
, etc.). - We implement a simple TypeScript analyzer ourselves to handle most scenarios.
As we all know, TypeScript's type gymnastics can be terrifying. If we really want to perfectly solve this issue, the Vue SFC compiler would need to parse and compute all types like the TypeScript compiler. The first solution sounds good, but it has a huge drawback: it inevitably requires relying on TypeScript's large and bloated compiler, which would greatly slow down build times.
In the end, I chose the second solution in Vue Macros. Therefore, it is currently not suitable to pass complex types in macros. However, this will be gradually improved in the future.
Vue 3.3 and Vue Macros#
After Vue Macros implemented this simple analyzer, Vue core also did a similar implementation.
However, there are still differences at present. The iteration speed of Vue Macros is far faster than that of Vue core itself, and Vue Macros is currently supporting more quirky syntax, while some syntax in Vue 3.3 is still unsupported.
So in the future, if you encounter types that Vue cannot parse, you might want to try Vue Macros. If Vue Macros still doesn't work, you can try providing a minimal reproducible code and opening an issue in the Vue Macros repository. Or try using a simpler syntax that avoids second inference.
defineSlots Macro#
- PR: https://github.com/vuejs/core/pull/7982
- Twitter: https://twitter.com/sanxiaozhizi/status/1641378248448937984
Background#
Vue 3.3 introduced the defineSlots
macro. We can use defineSlots
to define the types of our slots. This macro is hardly used in simple components but is very useful for more complex components, especially when this feature is used with generic components. Or when Volar cannot correctly infer types, we can specify them manually.
🌰 Example#
<script setup lang="ts">
const slots = defineSlots<{
default(props: { foo: string; bar: number }): any
}>()
</script>
We manually defined the type of the slot scope for the default
component.
🌰 Real Example#
For instance, we have a paginator component, and we can control how to render specific items
through slots.
<script setup lang="ts" generic="T">
// Child component Paginator
defineProps<{
data: T[]
}>()
defineSlots<{
default(props: { item: T }): any
}>()
</script>
<template>
<!-- Parent component -->
<Paginator :data="[1, 2, 3]">
<template #default="{ item }">{{ item }}</template>
</Paginator>
</template>
We passed the data
parameter, which is number[]
, so item
will also be inferred as number
. The type of item
will change according to the type passed to the data
parameter.
More Convenient Syntax for defineEmits
#
- PR: https://github.com/vuejs/core/pull/7992
- Twitter: https://twitter.com/youyuxi/status/1641403989026820098
This feature is also purely syntactic sugar.
Example#
<script setup lang="ts">
const emits = defineEmits<{
(evt: 'update:modelValue', value: string): void
(evt: 'change'): void
}>()
// ⬇️ After Vue 3.3
const emits = defineEmits<{
'update:modelValue': [value: string],
'change': []
}>()
</script>
Before Vue 3.3, we needed to write a few more characters, but now we can save some.
Deprecated Reactivity Transform Syntax#
Earlier this year, the Vue team announced the deprecation of Reactivity Transform syntax. Specifically, it will be deprecated in Vue 3.3, with warnings during use; it will be completely removed in Vue 3.4.
Personally, I believe Reactivity Transform still has its place.
Although it has been deprecated by the official team, the functionality has been moved to Vue Macros. This means you can still use the plugin without rushing to migrate to the old syntax, and you can continue to use it along with future bug fixes.
For specific reasons for removal, you can read this comment.
Postscript#
Overall, I am very happy to see Vue willing to accept suggestions and proposals from the community. This is probably my contribution to Vue 3.3. For more features, you can read the article on the Vue blog.
P.S. If anyone is willing to translate this article into English, I would greatly appreciate it if you could submit the content to the sxzz/articles repository after translating!
About Vue Macros#
Vue Macros is currently an independent project separate from the official Vue. Unlike the official Vue, its purpose is to explore some different possibilities.
I would prefer to see some more radical ideas, even if those ideas are not so mature. We can experiment further in Vue Macros and try to merge them into the official Vue repository once they mature.
Currently, Vue Macros is maintained by me alone, and I hope to attract more community members to participate in its development! 💕