Words: 7887, Reading time: 40 minutes, click on read the original text
It has been almost eight years since the first version was released on December 8, 2013. Do you know the meaning of each version's name?
version number | name | Paraphrase | time |
---|---|---|---|
V0.9 | Animatrix | The Matrix Animation | 2014.2.25 |
V0.10 | Blade Runner | Blade Runner | 2014.3.23 |
V0.11 | Cowboy Bebop | Star Cowboy | 2014.11.7 |
V0.12 | Dragon Ball | Dragon Ball | 2015.6.12 |
V1.0 | Evangelion | Evangelion | 2015.10.27 |
V2.0 | Ghost in the Shell | Ghost in the Shell | 2016.9.30 |
V2.1 | Hunter X Hunter | Full-time hunter | 2016.11.22 |
V2.2 | Initial D | Initial D | 2017.2.26 |
V2.3 | JoJo's Bizarre Adventure | JoJo's Bizarre Adventure | 2017.4.2 |
V2.4 | Kill la Kill | Kill the girl | 2017.7.13 |
V2.5 | Level E | Supernatural E touch | 2017.10.13 |
V2.6 | Macross | Macross | 2019.2.4 |
V3.0 | One Piece | One Piece | 2020.9.18 |
V3.1 | Pluto | The strongest robot on the ground | 2021.6.8 |
V3.2 | Quintessential Quintuplets | Quintuplets | 2021.8.10 |
It turns out that the name of each version is named after a manga, so how many of these anime have you watched?
Then we will focus on Vue3.0.
origin
The emergence of a new tool must be to solve the problems of existing tools. We often hear that Vue is not suitable for developing large and complex projects. One fundamental reason is that Vue's existing API forces us to organize code through options, but sometimes it makes more sense to organize code through logical relationships. Another reason is the lack of a simple and low-cost mechanism to extract and reuse the logic between multiple components.
Then let's take a look at the 2.0 problem and how Vue3 solves it.
Defects of Option Organization Code
The options type organization code, the same function is scattered in each option, which causes the data, methods, computed and other options to jump horizontally during development.
Vue3 launched CompositionApi, the purpose is to solve this problem, it combines the logic scattered in each option, let's compare it below:
Mixin problem
For complex functions, we might think of using Mixin to extract separate files. But Mixin has some usage problems, such as naming conflicts and unclear attribute sources.
Vue3 proposes the Hooks method, which can extract each function to hooks. A hook is an independent function, so the above problems will no longer occur.
TypeScript support is not sound
Now large projects will be equipped with TypeScript as standard. Vue's current API has encountered a lot of trouble when integrating TypeScript. The main reason is that Vue relies on a simple this
context to expose property. this
is more subtle. (Such methods
functions in the option this
is to a component instance, instead of the methods
object).
In other words, Vue's existing API did not take care of type deduction at the beginning of its design, which complicates the adaptation of TypeScript.
Currently, most Vue developers who use TypeScript are writing components as TypeScript classes (with the help of decorator) vue-class-component
It must rely on decorator-a very unstable stage 2 proposal with many unknowns in its implementation details. Based on it is extremely risky.
The scheme proposed in Vue3 makes more use of common variables and functions that are naturally type-friendly, enjoys type deduction perfectly, and does not need to make too many additional type annotations.
This also means that the JavaScript code you write is almost TypeScript code. Even non-TypeScript developers will benefit from better IDE type support.
Better responsiveness and performance
As we all know, Vue2's response getter
and setter
an existing property of the object Object.defineProperty
, so it can only monitor the change of this property value, but not the addition and deletion of the object property. In the implementation of Vue 2, when the data becomes responsive during the component initialization phase, when the sub-attribute is still an object, it will recursively execute Object.defineProperty
define the responsive type of the sub-object, and there will be some performance problems. And there is a common problem that is to modify the array through the index and directly add properties to the object, and it will not trigger the responsive update mechanism.
In Vue3, Proxy is used to achieve responsiveness. In fact, it is not that the performance of Proxy itself is better than Object.defineProperty
, in fact, it is the opposite. So why choose Proxy?
Because Proxy
is essentially hijacking an object, so it can not only monitor the change of an attribute value of the object, but also monitor the addition and deletion of object attributes. Moreover, when implementing the responsive style, a delayed processing method is adopted. When a deeply nested object is used, the responsive style of the attribute will be processed only when its attribute is accessed, and there will be a certain improvement in performance.
Support global API Treeshaking
Vue3 reconstructs the global and local APIs, both use ESModule's named export access, support tree-shaking, and only package the functions that are used. Users only pay for the functions that are actually used. At the same time, the reduction in package size also means performance. promote.
// vue2
import Vue from 'vue'
Vue.nextTick(() => {
// 一些和DOM有关的东西
})
// vue3
import { nextTick } from 'vue'
nextTick(() => {
// 一些和DOM有关的东西
})
The above are the main changes in Vue, so let's take a look at the new features.
New features and changes
Next, we mainly look at some non-compatible major changes:
Global API
Support multiple application root instances to prevent global configuration pollution
// vue2 // 这会影响两个根实例 Vue.mixin({ /* ... */ }) const app1 = new Vue({ el: '#app-1' }) const app2 = new Vue({ el: '#app-2' })
// vue3
import { createApp } from 'vue'
const app = createApp({})
app.mixin({
/* ... */
})
Global API details of some other global API changes.
Global API refactored to be Treeshaking
import { nextTick } from 'vue' nextTick(() => { // 一些和DOM有关的东西 }) // **** 受影响的API // Vue.nextTick // Vue.observable (用 Vue.reactive 替换) // Vue.version // Vue.compile (仅完整构建版本) // Vue.set (仅兼容构建版本) // Vue.delete (仅兼容构建版本)
Template and instruction related
- Better to use
v-model
Replace the original v-model
and v-bind.sync
modifiers, and support the use of multiple v-model
for two-way binding through the parameter form.
<!-- vue2 -->
<ChildComponent v-model="pageTitle" :title.sync="title"/>
<!-- 完整 -->
<ChildComponent :value="pageTitle" @input="(title)=> (pageTitle=title)" :title="title" @update:title="(title)=> (title=title)"/>
<!-- vue3 -->
<ChildComponent v-model="pageTitle" v-modle:title="title"/>
<!-- 完整 -->
<ChildComponent :model-value="pageTitle" @update:modelValue="(title)=> (pageTitle=title)" :title="title" @update:title="(title)=> (title=title)"/>
<template v-for>
changes
<!-- vue2 -->
<template v-for="item in list">
<div :key="'heading-' + item.id">...</div>
<span :key="'content-' + item.id">...</span>
</template>
<!-- vue 3 -->
<template v-for="item in list" :key="item.id">
<div>...</div>
<span>...</span>
</template>
v-bind
merge order change
<!-- vue2 -->
<div id="red" v-bind="{ id: 'blue' }"></div>
<!-- result -->
<div id="red"></div>
<!-- vue3 -->
<div id="red" v-bind="{ id: 'blue' }"></div>
<!-- result -->
<div id="blue"></div>
Remove
v-on.native
modifierIn previous versions, to add a native DOM listener to the root element of a child component, you can use the
.native
modifier.
<!-- vue2 -->
<my-component
v-on:close="handleComponentEvent"
v-on:click.native="handleNativeClickEvent"
/>
In vue3, assembly not is defined as all events listener component triggered, Vue now will add them as a native event listener to the root element subassembly (unless you have set the option subassembly inheritAttrs: false
)
<!-- vue3 -->
<my-component
v-on:close="handleComponentEvent"
v-on:click="handleNativeClickEvent"
/>
<script>
export default {
emits: ['close']
}
</script>
- Support fragments (multiple root nodes)
<!-- vue2 -->
<template>
<div>
<header>...</header>
<main>...</main>
<footer>...</footer>
</div>
</template>
In vue2, components must be contained within an element, and multiple root nodes are not supported. This sometimes brings troubles to us in writing styles, so multiple root nodes are supported in vue3.
<!-- vue3 -->
<template>
<header>...</header>
<main v-bind="$attrs">...</main>
<footer>...</footer>
</template>
- Added Teleport portal
The core of vue application development is component writing, which encapsulates UI and related behaviors into components to build UI. But sometimes part of the component template logically belongs to the component, and from a technical point of view, it is best to move this part of the template to a location other than the Vue app in the DOM.
For example, the most common modal window, we hope that the logic of the modal window exists in the component, but on the UI, it is best to mount the element to the DOM root node (such as body) to facilitate our css positioning.
<body>
<div style="position: relative;">
<h3>Tooltips with Vue 3 Teleport</h3>
<div>
<modal-button></modal-button>
</div>
</div>
</body>
const app = Vue.createApp({});
app.component('modal-button', {
template: `
<button @click="modalOpen = true">
Open full screen modal!
</button>
<div v-if="modalOpen" class="modal">
<div>
I'm a modal!
<button @click="modalOpen = false">
Close
</button>
</div>
</div>
`,
data() {
return {
modalOpen: false
}
}
})
In the above example, we can see a problem - the frame mode is deeply nested div
rendered, whereas the modal box position:absolute
to the parent positioned opposite div
as a reference, the final results will be affected by the parent The impact of level positioning, this may not be the result we expect.
Teleport provides a clean method that allows us to control which parent node in the DOM renders HTML without having to resort to global state or split it into two components.
app.component('modal-button', {
template: `
<button @click="modalOpen = true">
Open full screen modal! (With teleport!)
</button>
<teleport to="body">
<div v-if="modalOpen" class="modal">
<div>
I'm a teleported modal!
(My parent is "body")
<button @click="modalOpen = false">
Close
</button>
</div>
</div>
</teleport>
`,
data() {
return {
modalOpen: false
}
}
})
Component
- Functional component
In vue2, we may use functional components because of the performance and the needs of multiple root nodes. When in vue3, the performance of ordinary components is optimized, which is almost the same as the performance of functional components, and it also supports multiple root nodes, so functional The usage scenarios of components are not very necessary, so some adjustments have been made to functional components:
<!-- Vue 2 函数式组件示例 -->
<script>
export default {
functional: true,
props: ['level'],
render(h, { props, data, children }) {
return h(`h${props.level}`, data, children)
}
}
</script>
<!-- Vue 2 函数式组件示例使用 <template> -->
<template functional>
<component
:is="`h${props.level}`"
v-bind="attrs"
v-on="listeners"
/>
</template>
<script>
export default {
props: ['level']
}
</script>
Deleted In vue3 functional option
and functional attribute
, the above two methods can not be used in vue3.
In vue3, a functional component is an ordinary function, which receives two parameters: props
and context
.
// vue3
import { h } from 'vue'
const DynamicHeading = (props, context) => {
return h(`h${props.level}`, context.attrs, context.slots)
}
DynamicHeading.props = ['level']
export default DynamicHeading
- Create asynchronous components
It used to be possible to define asynchronous components by returning a Promise function:
// vue2
const asyncModal = () => import('./Modal.vue');
// 或者带上配置
const asyncModal = {
component: () => import('./Modal.vue'),
delay: 200,
timeout: 3000,
error: ErrorComponent,
loading: LoadingComponent
}
In vue3, a new api (defineAsyncComponent) is added to display and define asynchronous components.
// vue3
import { defineAsyncComponent } from 'vue'
import ErrorComponent from './components/ErrorComponent.vue'
import LoadingComponent from './components/LoadingComponent.vue'
// 不带选项的异步组件
const asyncModal = defineAsyncComponent(() => import('./Modal.vue'))
// 带选项的异步组件
const asyncModalWithOptions = defineAsyncComponent({
loader: () => import('./Modal.vue'),
delay: 200,
timeout: 3000,
errorComponent: ErrorComponent,
loadingComponent: LoadingComponent
})
- Add
emits
option to define and verify the custom event sent
<!-- vue2 -->
<template>
<div>
<p>{{ text }}</p>
<button v-on:click="$emit('accepted')">OK</button>
</div>
</template>
<script>
export default {
props: ['text']
}
</script>
The emits option is added to vue3 to display the custom events of the defined component. emits
will be counted in the $attrs
the component and bound to the root node of the component.
<!-- vue3 -->
<template>
<div>
<p>{{ text }}</p>
<button v-on:click="$emit('accepted')">OK</button>
</div>
</template>
<script>
export default {
props: ['text'],
emits: ['accepted']
}
</script>
Emits can also support the verification of custom events, just change it to an object form.
emits: {
// 没有验证函数
click: null,
// 带有验证函数
submit: payload => {
if (payload.email && payload.password) {
return true
} else {
console.warn(`Invalid submit event payload!`)
return false
}
}
}
It is strongly recommended to use emits
record all events triggered by each component, and the recorded events will have code prompts.
Rendering function
Unified Slot API
Previously, there were two different
this.$scopedSlots
(061317aadd0dd7 andthis.$slots
) to obtain slots in the component. Now,this.$slots
.- Integrate
$listeners
, class, style to$attrs
In vue2, we can access attribute
and the event listener in the following way:
<!-- vue3 -->
<template>
<label>
<input type="text" v-bind="$attrs" v-on="$listeners" />
</label>
</template>
<script>
export default {
inheritAttrs: false
}
</script>
In the virtual DOM of Vue 3, the event listener is now just an attribute prefixed on
, which becomes part of the $attrs
$listeners
is removed.
<!-- vue3 -->
<template>
<label>
<input type="text" v-bind="$attrs" />
</label>
</template>
<script>
export default {
inheritAttrs: false
}
</script>
There are some special treatments class
and style
attributes in the virtual DOM implementation of Vue 2. Thus, they not contained in $attrs
in, Vue3 simplifies this part of the process, with $attrs
comprising all attribute, including class
and style
.
Custom element
is
prop can only be used in the<component>
element
is
attribute cannot be used in ordinary components and elements, but can only be used in the component
built-in components.
other
Life cycle changes
destroyed
life cycle option was renamed tounmounted
beforeDestroy
life cycle option was renamed tobeforeUnmount
Custom instruction life cycle adjustment, unified with component life cycle
- created-new! Called before the element's attribute or event listener is applied.
- bind → beforeMount
- inserted → mounted
- beforeUpdate : New! This is called before the element itself is updated, much like a component lifecycle hook.
- update → remove! There are too many similarities to update, so this is redundant, please use
updated
instead. - componentUpdated → updated
- beforeUnmount : New! Similar to the component lifecycle hook, it will be called before the element is unloaded.
- unbind -> unmounted
- Mixin merge behavior changes
data()
and its mixin or extends base class from the component shallow level will now be merged.
- Transitional class name change
The transition class name v-enter
modified to v-enter-from
, and the transition class name v-leave
modified to v-leave-from
.
VNode life cycle event changes
<!-- vue2 --> <template> <child-component @hook:updated="onUpdated"> </template>
<!-- vue3 -->
<template>
<child-component @vnode-updated="onUpdated">
</template>
Obsolete API
keyCode
asv-on
modifier and theconfig.keyCodes
configuration.
<!-- 键码版本(废弃) -->
<input v-on:keyup.13="submit" />
<!-- 别名版本 -->
<input v-on:keyup.enter="submit" />
<script>
Vue.config.keyCodes = { // 废弃
f1: 112
}
</script>
$on
,$off
and$once
instance methods have been removed, component instances no longer implement the event trigger interface
In vue2, we can implement component communication through EventBus:
// eventBus.js
const eventBus = new Vue()
export default eventBus
// ChildComponent.vue
import eventBus from './eventBus'
export default {
mounted() {
// 添加 eventBus 监听器
eventBus.$on('custom-event', () => {
console.log('Custom event triggered!')
})
},
beforeDestroy() {
// 移除 eventBus 监听器
eventBus.$off('custom-event')
}
}
// ParentComponent.vue
import eventBus from './eventBus'
export default {
methods: {
callGlobalCustomEvent() {
eventBus.$emit('custom-event') // 当 ChildComponent 被挂载,控制台中将显示一条消息
}
}
}
In vue3, this method is no longer valid because the $on
, $off
and $once
methods are completely removed. If necessary, you can use some external libraries that implement the event trigger interface, or use Provide, and just go to Vuex for complicated ones.
- Filters are no longer supported
<!-- vue2 -->
<template>
<h1>Bank Account Balance</h1>
<p>{{ accountBalance | currencyUSD }}</p>
</template>
<script>
export default {
props: {
accountBalance: {
type: Number,
required: true
}
},
filters: {
currencyUSD(value) {
return '$' + value
}
}
}
</script>
In vue3, you can use methods or calculated properties instead:
<!-- vue3 -->
<template>
<h1>Bank Account Balance</h1>
<p>{{ accountInUSD }}</p>
</template>
<script>
export default {
props: {
accountBalance: {
type: Number,
required: true
}
},
computed: {
accountInUSD() {
return '$' + this.accountBalance
}
}
}
</script>
Delete
$children
property$children
property has been removed and is no longer supported. If you need to access the sub-component instance, we recommend using $refs .- Global functions
set
anddelete
and instance methods$set
and$delete
. They are no longer needed for agent-based change detection.
Of course, the above are just appetizers, and the next ones are our most noteworthy new features.
Combined Api
In order to solve the problems of logic reuse and code organization we mentioned earlier, vue3 introduced a new code writing method. This most important feature of vue3 is also the main trend of writing vue in the future.
The following is a view showing the warehouse list of a certain user, with search and filtering functions. The pseudo code is as follows:
// src/components/UserRepositories.vue
export default {
components: { RepositoriesFilters, RepositoriesSortBy, RepositoriesList },
props: {
user: {
type: String,
required: true
}
},
data () {
return {
repositories: [], // 1
filters: { ... }, // 3
searchQuery: '' // 2
}
},
computed: {
filteredRepositories () { ... }, // 3
repositoriesMatchingSearchQuery () { ... }, // 2
},
watch: {
user: 'getUserRepositories' // 1
},
methods: {
getUserRepositories () {
// 使用 `this.user` 获取用户仓库
}, // 1
updateFilters () { ... }, // 3
},
mounted () {
this.getUserRepositories() // 1
}
}
It can be seen that the code is organized by option, and the functional logic points are fragmented and scattered in each component option. Especially when you encounter some components with more content, you need to jump repeatedly in each option. Reading and writing code will be the same. A very painful thing, greatly reducing the maintainability of the component.
In fact, when developing and reading the component code, we pay more attention to the functional points, rather than to the options that are used. This is the problem that the combined api solves.
// src/composables/useUserRepositories.js
import { fetchUserRepositories } from '@/api/repositories'
import { ref, onMounted, watch } from 'vue'
export default function useUserRepositories(user) {
const repositories = ref([])
const getUserRepositories = async () => {
repositories.value = await fetchUserRepositories(user.value)
}
onMounted(getUserRepositories)
watch(user, getUserRepositories)
return {
repositories,
getUserRepositories
}
}
// src/composables/useRepositoryNameSearch.js
import { ref, computed } from 'vue'
export default function useRepositoryNameSearch(repositories) {
const searchQuery = ref('')
const repositoriesMatchingSearchQuery = computed(() => {
return repositories.value.filter(repository => {
return repository.name.includes(searchQuery.value)
})
})
return {
searchQuery,
repositoriesMatchingSearchQuery
}
}
// src/components/UserRepositories.vue
import { toRefs } from 'vue'
import useUserRepositories from '@/composables/useUserRepositories'
import useRepositoryNameSearch from '@/composables/useRepositoryNameSearch'
import useRepositoryFilters from '@/composables/useRepositoryFilters'
export default {
components: { RepositoriesFilters, RepositoriesSortBy, RepositoriesList },
props: {
user: {
type: String,
required: true
}
},
setup(props) {
const { user } = toRefs(props)
const { repositories, getUserRepositories } = useUserRepositories(user)
const {
searchQuery,
repositoriesMatchingSearchQuery
} = useRepositoryNameSearch(repositories)
const {
filters,
updateFilters,
filteredRepositories
} = useRepositoryFilters(repositoriesMatchingSearchQuery)
return {
// 因为我们并不关心未经过滤的仓库
// 我们可以在 `repositories` 名称下暴露过滤后的结果
repositories: filteredRepositories,
getUserRepositories,
searchQuery,
filters,
updateFilters
}
}
}
The combined API separates the logical concerns of the components, is more organized, and the code is more readable and maintainable. Moreover, the reusable logic can be separated into hooks, which has better reusability.
Due to the particularity of combined APIs, new APIs need to be used, so let's take a look at these APIs next.
setup
setup is the entrance modular API, all the content needs to be included in them, it only created in the component before execution once , this is not so in this case points to the current component instance.
setup(props,context){
const { attrs, slots, emit } = context;
// ...
}
parameter
{Data} props
: The received props data is responsive.{SetupContext} context
: An object that contains the context information needed by the component, includingattrs
,slots
,emit
.
return value
- If it returns an object, then the property of the object and passed to
setup
ofprops
parameter property will have access to the template.
<!-- MyBook.vue -->
<template>
<div>{{ collectionName }}: {{ readersNumber }} {{ book.title }}</div>
</template>
<script>
import { ref, reactive } from 'vue'
export default {
props: {
collectionName: String
},
setup(props) {
const readersNumber = ref(0)
const book = reactive({ title: 'Vue 3 Guide' })
// 暴露给 template
return {
readersNumber,
book
}
}
}
</script>
- If a rendering function is returned, the function can directly use the reactive state declared in the same scope.
// MyBook.vue
import { h, ref, reactive } from 'vue'
export default {
setup() {
const readersNumber = ref(0)
const book = reactive({ title: 'Vue 3 Guide' })
// 请注意这里我们需要显式调用 ref 的 value
return () => h('div', [readersNumber.value, book.title])
}
}
Lifecycle hook
In order to make the function of the combined API as complete as the optional API, we also need a setup
to register the lifecycle hook in 061317aadd1e9e. The lifecycle hook on the combined API has the same name as the optional API, but the prefix is on
: that is, mounted
will look like onMounted
.
import { onMounted, onUpdated, onUnmounted } from 'vue'
const MyComponent = {
setup() {
onMounted(() => {
console.log('mounted!')
})
onUpdated(() => {
console.log('updated!')
})
onUnmounted(() => {
console.log('unmounted!')
})
}
}
setup replaces beforeCreate
and created
, the comparison is as follows:
Option API | Hook inside setup |
---|---|
beforeCreate | Not needed* |
created | Not needed* |
beforeMount | onBeforeMount |
mounted | onMounted |
beforeUpdate | onBeforeUpdate |
updated | onUpdated |
beforeUnmount | onBeforeUnmount |
unmounted | onUnmounted |
errorCaptured | onErrorCaptured |
renderTracked | onRenderTracked |
renderTriggered | onRenderTriggered |
activated | onActivated |
deactivated | onDeactivated |
Responsive
In vue3 using the Proxy
instead Object.defineProperty
, so that in response Vue. 3 avoids some problems present in earlier versions of the Vue.
When we data
function of a component, Vue will wrap the object in a Proxy get
and set
.
Give me 🌰:
const dinner = {
meal: 'tacos'
}
const handler = {
get(target, property, receiver) { // 捕捉器
track(target, property) // 跟踪property读取,收集依赖
return Reflect.get(...arguments) // Reflect将this绑定到Proxy
},
set(target, property, value, receiver) {
trigger(target, property) // 执行副作用依赖项
return Reflect.set(...arguments)
}
}
const proxy = new Proxy(dinner, handler)
console.log(proxy.meal)
// tacos
- track when a value is read : the Proxy
get
handlertrack
function records the current property and side effects. - when a certain value changes. : Calls the
set
processing function on the proxy. - re-run the code to read the original value :
trigger
function to find which side effects depend on the property and execute them.
The proxied objects are invisible to the user, but internally, they enable Vue to perform dependency tracking and change notifications when the value of the property is accessed or modified.
So how does the component make the rendering respond to data changes?
The component template will be compiled into a render
function, which is used to create VNodes , describing how the component should be rendered. This render function is wrapped in a side effect, allowing Vue to track the property that is "touched" at runtime. When the property changes, the corresponding side effect will be executed, thereby executing render re-rendering. Of course, the rendering will not be completely re-rendered. Here are some optimization methods. There are many online materials, so I won't go into it here.
Next we look at a few commonly used reactive APIs.
ref
interface Ref<T> {
value: T
}
function ref<T>(value: T): Ref<T>
Accepts an internal value and returns a responsive and variable ref object. The ref object has a single property .value
that points to an internal value.
import { ref } from 'vue'
const counter = ref<number>(0)
console.log(counter) // { value: 0 }
console.log(counter.value) // 0
counter.value++
console.log(counter.value) // 1
Because in JavaScript, Number
or String
are passed by value instead of by reference, there is an encapsulated object around any value, so that we can safely pass it throughout the application without worrying about being somewhere Lose its responsiveness.
Note: If ref is nested in a reactive object (such as reactive, readonly) or used in a template, will be automatically unpacked.
reactive
function reactive<T extends object>(target: T): UnwrapNestedRefs<T>
Return a responsive copy of the object, that is, a proxy object that is deeply recursively transformed.
import { reactive } from 'vue'
interface IState{
count:number
}
// state 现在是一个响应式的状态
const state = reactive<IState>({
count: 0,
})
ref and reactive :
- General basic data types use ref, objects use reactive
- If the object is assigned as a ref value, the reactive method makes the object highly responsive.
readonly
Accept an object (responsive or pure object) or ref and return the read-only proxy of the original object. The read-only proxy is deep: any nested property that is accessed is also read-only.
const original = reactive({ count: 0 })
const copy = readonly(original)
watchEffect(() => {
// 用于响应性追踪
console.log(copy.count)
})
// 变更 original 会触发依赖于副本的侦听器
original.count++
// 变更副本将失败并导致警告
copy.count++ // 警告!
unref
If the parameter is a ref
, the internal value is returned, otherwise the parameter itself is returned. This is the syntactic sugar function val = isRef(val) ? val.value : val
function useFoo(x: number | Ref<number>) {
const unwrapped = unref(x) // unwrapped 现在一定是数字类型
}
toRef
ref
for a certain property on the source responsive object, and it will maintain a responsive connection to the source property.
const state = reactive({
foo: 1,
bar: 2
})
const fooRef = toRef(state, 'foo')
fooRef.value++
console.log(state.foo) // 2
state.foo++
console.log(fooRef.value) // 3
toRefs
Convert the responsive object into an ordinary object, where each property of the result object is ref
that points to the corresponding property of the original object.
function useFeatureX() {
const state = reactive({
foo: 1,
bar: 2
})
// 操作 state 的逻辑
// 返回时转换为ref
return toRefs(state)
}
export default {
setup() {
// 可以在不失去响应性的情况下解构
const { foo, bar } = useFeatureX()
return {
foo,
bar
}
}
}
To identify whether the data has been processed using the above APIs, you can use these APIs: isRef 、
isProxy 、
isReactive 、
.
computed
// 只读的
function computed<T>(
getter: () => T,
debuggerOptions?: DebuggerOptions
): Readonly<Ref<Readonly<T>>>
// 可写的
function computed<T>(
options: {
get: () => T
set: (value: T) => void
},
debuggerOptions?: DebuggerOptions
): Ref<T>
interface DebuggerOptions {
onTrack?: (event: DebuggerEvent) => void
onTrigger?: (event: DebuggerEvent) => void
}
interface DebuggerEvent {
effect: ReactiveEffect
target: any
type: OperationTypes
key: string | symbol | undefined
}
- Accept a getter function and return an immutable response type ref object according to the return value of the getter.
const count = ref(1)
const plusOne = computed(() => count.value + 1)
console.log(plusOne.value) // 2
plusOne.value++ // 错误
- Accept an
get
andset
to create a writable ref object.
const count = ref(1)
const plusOne = computed({
get: () => count.value + 1,
set: val => {
count.value = val - 1
}
})
plusOne.value = 1
console.log(count.value) // 0
watchEffect
function watchEffect(
effect: (onInvalidate: InvalidateCbRegistrator) => void,
options?: WatchEffectOptions
): StopHandle
interface WatchEffectOptions {
flush?: 'pre' | 'post' | 'sync' // 默认:'pre'
onTrack?: (event: DebuggerEvent) => void
onTrigger?: (event: DebuggerEvent) => void
}
interface DebuggerEvent {
effect: ReactiveEffect
target: any
type: OperationTypes
key: string | symbol | undefined
}
type InvalidateCbRegistrator = (invalidate: () => void) => void
type StopHandle = () => void
Execute a function passed in immediately, track its dependencies responsively, and re-run the function when its dependencies change.
const count = ref(0)
watchEffect(() => console.log(count.value))
// -> logs 0
setTimeout(() => {
count.value++
// -> logs 1
}, 100)
- Stop listening
When watchEffect
in the component's setup() function or the life cycle hook , the listener will be linked to the life cycle of the component and will automatically stop when the component is uninstalled. Of course, you can also call the return value explicitly to stop listening:
const stop = watchEffect(() => {
/* ... */
})
// later
stop()
- Clear side effects
Sometimes the side-effect function executes some asynchronous side-effects, and these responses need to be cleared when they fail. So the function that listens to the incoming side effects can receive a onInvalidate
function as an input parameter to register the callback when the cleanup fails. This invalidation callback will be triggered when the following situations occur:
- When side effects are about to be re-executed
- Listener is stopped (if
setup()
using a hook function in the life cycle orwatchEffect
, when the assembly is unloaded)
watchEffect(onInvalidate => {
const token = performAsyncOperation(id.value)
onInvalidate(() => {
// id has changed or watcher is stopped.
// invalidate previously pending async operation
token.cancel()
})
})
In addition, you can use the flush option or watchPostEffect and watchSyncEffect to adjust the refresh timing.
watch
// 侦听单一源
function watch<T>(
source: WatcherSource<T>,
callback: (
value: T,
oldValue: T,
onInvalidate: InvalidateCbRegistrator
) => void,
options?: WatchOptions
): StopHandle
// 侦听多个源
function watch<T extends WatcherSource<unknown>[]>(
sources: T
callback: (
values: MapSources<T>,
oldValues: MapSources<T>,
onInvalidate: InvalidateCbRegistrator
) => void,
options? : WatchOptions
): StopHandle
type WatcherSource<T> = Ref<T> | (() => T)
type MapSources<T> = {
[K in keyof T]: T[K] extends WatcherSource<infer V> ? V : never
}
// 参见 `watchEffect` 共享选项的类型声明
interface WatchOptions extends WatchEffectOptions {
immediate?: boolean // 默认:false
deep?: boolean
}
watch
needs to listen to a specific data source and execute side effects in a separate callback function. By default, it is also lazy—that is, the callback is only called when the listening source changes.
Compared with watchEffect ,
watch
allows us to:- Execute side effects lazily;
- More specifically specify the state in which the listener should be triggered to re-run;
- Access the previous and current values of the listened state.
// 侦听一个 getter
const state = reactive({ count: 0 })
watch(
() => state.count,
(count, prevCount) => {
/* ... */
}
)
// 直接侦听一个 ref
const count = ref(0)
watch(count, (count, prevCount) => {
/* ... */
})
watch([fooRef, barRef], ([foo, bar], [prevFoo, prevBar]) => {
/* ... */
})
Of course, I just introduced the commonly used APIs. For more information, please see Responsive API .
Disadvantages
Of course, the combined API is not a silver bullet, at least not for now, there are still some problems.
- Ref's mental burden
Reading and writing ref must bring .value, the syntax is redundant, there is no definite solution to solve this problem. However, Youda gave the refSuger2 proposal, depending on how the follow-up community recognition is.
<script setup>
// declaring a variable that compiles to a ref
let count = $ref(1)
console.log(count) // 1
function inc() {
// the variable can be used like a plain value
count++
}
</script>
<template>
<button @click="inc">{{ count }}</button>
</template>
ugly and lengthy return statement
setup()
becomes lengthy, like repetitive work, and there are still questions about the code jumping up and down.
In vue3.2, SetupScript syntax sugar is provided, so there is no such problem.
- Need more self-restraint
Although the combined API provides more flexibility in code organization, it also requires more self-restraint by developers to "get it right." Some people worry that the API will allow inexperienced people to write noodle code. In other words, although the combined API raises the upper limit of code quality, it also lowers the lower limit.
We need to think more about how to organize the code reasonably. It is recommended to decompose the program into functions and modules to organize it according to logical concerns.
SetupScript
<script setup>
is compile-time syntactic sugar for using combined API in single file component (SFC). Compared with the ordinary <script>
syntax, it has more advantages:
- Less boilerplate content, more concise code.
- Ability to declare props and emit events using pure Typescript.
- Better runtime performance (the template will be compiled into a rendering function in the same scope as it, without any intermediate proxy).
- Better IDE type inference performance (reduce the work of the language server to extract types from the code).
<script setup>
// 导入
import { capitalize } from './helpers'
// 组件
import MyComponent from './MyComponent.vue'
// 变量
const msg = 'Hello!'
// 函数
function log() {
console.log(msg)
}
</script>
<template>
<div @click="log">{{ msg }}</div>
<div>{{ capitalize('hello') }}</div>
<MyComponent />
</template>
The above <script setup>
will be compiled into setup()
function, the difference is <script setup>
will be executed every time the component instance is created. And all the top level of (including variables, function declarations, and content introduced by import) will be exposed to the template and can be used directly, even the components do not need to be manually registered.
Before continuing the following content, let's look at a word compiler macro , they do not need to be imported, and will be compiled and processed when <script setup>
<script setup>
provides the following compiler macros:
- defineProps
- defineEmits
- defineExpose
- withDefaults
Next, take a look at the unique API of <script setup>
defineProps
declares Props and receives the same value as theprops
const props = defineProps({
foo: {
type:String,
default:''
}
})
If you use TypeScript, you can also use pure type declarations to declare Props.
// 普通
const props = defineProps<{
foo: string
bar?: number
}>()
// 默认值
interface Props {
msg?: string
labels?: string[]
}
const props = withDefaults(defineProps<Props>(), {
msg: 'hello',
labels: () => ['one', 'two']
})
defineEmits
declares emerges and receives the same value as theemits
// 普通
const emit = defineEmits(['change', 'delete'])
// TS类型声明
const emit = defineEmits<{
(e: 'change', id: number): void
(e: 'update', value: string): void
}>()
defineExpose
declares the exposed binding
Use <script setup>
component is off by default , i.e. template or ref $parent
disclosed acquired Examples of chain assembly, not exposed in any <script setup>
binding declaration. Developers need to clearly declare the exposed attributes.
<script setup>
import { ref } from 'vue'
const a = 1
const b = ref(2)
defineExpose({
a,
b
})
</script>
useSlots
anduseAttrs
correspondsetupContext.slots
andsetupContext.attrs
, can also be used in a conventional combined API.
<script setup>
import { useSlots, useAttrs } from 'vue'
const slots = useSlots()
const attrs = useAttrs()
</script>
There are still some things that <script setup>
can't do, and they need to be used together with <script>
<script>
// 普通 <script>, 在模块范围下执行(只执行一次)
runSideEffectOnce()
// 声明额外的选项
export default {
inheritAttrs: false,
customOptions: {}
}
</script>
<script setup>
// 在 setup() 作用域中执行 (对每个实例皆如此)
</script>
For more information, please see SetupScript .
other
- Selector
/* 深度选择器 */ .a :deep(.b) { /* ... */ } /* 插槽选择器 */ :slotted(div) { color: red; } /* 全局选择器 */ :global(.red) { color: red; }
<style module>
<template>
<p :class="$style.red">
This should be red
</p>
</template>
<style module>
.red {
color: red;
}
</style>
You can also customize the name of the injection:
<template>
<p :class="classes.red">red</p>
</template>
<style module="classes">
.red {
color: red;
}
</style>
useCssModule
in the combined API:
// 默认, 返回 <style module> 中的类
useCssModule()
// 命名, 返回 <style module="classes"> 中的类
useCssModule('classes')
Use state-driven dynamic CSS:
<script setup>
const theme = {
color: 'red'
}
</script>
<template>
<p>hello</p>
</template>
<style scoped>
p {
color: v-bind('theme.color');
}
</style>
- Follow RFCS , look back on history and gain insight into the future
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。