三咲智子 Kevin Deng

三咲智子 Kevin Deng

Gen Z | Full Stack Developer 🏳️‍🌈
github
twitter
twitter

Detailed Explanation of the Main New Features of Vue 3.3

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 slots
  • defineEmits with more convenient syntax
  • defineOptions 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#

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>

Vue SFC Playground

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#

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>

Vue SFC Playground

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>

Vue SFC Playground

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#

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#

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#

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! 💕

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.