Vue3.0 has been updated and optimized since the first One Piece version was released in September 20; In addition to the rendering of the server, most of the other work has been completed, and the Chinese version of the official documents have been released; As end users, let's take a look at the new functions and features of vue3.
You dada shared several highlights of Vue3.0 during the live broadcast of station B:
-
Performance: Performance Optimization
-
Tree shaking support: supports tree shaking optimization
-
Composition API: composition API
-
Fragment, report, suspend: new components
-
Better TypeScript support: better TypeScript support
-
Custom Renderer API: Custom renderer
In terms of performance, compared with Vue2.x, the performance is improved by about 1.3 ~ 2 times; The volume after packaging is also smaller. If you only write a HelloWorld for packaging, it is only 13.5kb; With all runtime features, it is only 22.5kb.
So what is the difference between us as end users and Vue2.x in development? Talk is heap, let's look at the code.
Tree-shaking
One of the most important changes in Vue3 is the introduction of tree shaking. It is obvious that tree shaking brings smaller bundles. In version 2.x, many functions are mounted on global Vue objects, such as, set and other functions. Therefore, although we may not use them, as long as Vue is introduced during packaging, these global functions will still be packaged into the bundle.
In Vue3, all APIs are introduced through ES6 modularization, so that packaging tools such as webpack or rollup can eliminate unused APIs during packaging and minimize the volume of the bundle; We can find such changes in main.js:
//src/main.js import { createApp } from "vue"; import App from "./App.vue"; import router from "./router"; const app = createApp(App); app.use(router).mount("#app");
The method of creating app instances is changed from new Vue() to createApp function; However, some core functions, such as virtual DOM update algorithm and responsive system, will be packaged anyway; The change brought about by this is that the previously globally configured components (Vue.component), instructions (Vue.directive), mixing (Vue.mixin) and plug-ins (Vue.use) are directly mounted on the instance; We call through the created instance. The advantage is that an application can have multiple Vue instances, and the configurations between different instances will not affect each other:
const app = createApp(App) app.use(/* ... */) app.mixin(/* ... */) app.component(/* ... */) app.directive(/* ... */)
Therefore, the following global API s of Vue2.x also need to be introduced in ES6 Modularization:
-
Vue.nextTick
-
Vue.observable is no longer supported and changed to reactive
-
Vue.version
-
Vue.compile (full build only)
-
Vue.set (compatible build only)
-
Vue.delete (compatible build only)
In addition, vuex and Vue router also use tree shaking for improvement, but the syntax of the api has not changed much:
//src/store/index.js import { createStore } from "vuex"; export default createStore({ state: {}, mutations: {}, actions: {}, modules: {}, }); //src/router/index.js import { createRouter, createWebHistory } from "vue-router"; const router = createRouter({ history: createWebHistory(process.env.BASE_URL), routes, });
More information about the use of tree shaking can be viewed in Webpack configuration full parsing.
Life cycle function
As we all know, there are 8 lifecycle functions in Vue2.x:
-
beforeCreate
-
created
-
beforeMount
-
mounted
-
beforeUpdate
-
updated
-
beforeDestroy
-
destroyed
In vue3, a new setup life cycle function is added. The setup execution time is before the beforeCreate life function. Therefore, instances cannot be obtained through this function; At the same time, in order to unify the naming, beforeDestroy is renamed beforeUnmount and destroyed is renamed unmounted. Therefore, vue3 there are the following life cycle functions:
-
beforeCreate (it is recommended to use setup instead)
-
created (setup is recommended instead)
-
setup
-
beforeMount
-
mounted
-
beforeUpdate
-
updated
-
beforeUnmount
-
unmounted
At the same time, vue3 adds a life cycle hook. We can access the life cycle of components by adding on before the life cycle function. We can use the following life cycle hooks:
-
onBeforeMount
-
onMounted
-
onBeforeUpdate
-
onUpdated
-
onBeforeUnmount
-
onUnmounted
-
onErrorCaptured
-
onRenderTracked
-
onRenderTriggered
So how do these hook functions be called? We mount the life cycle hook in setup. When the corresponding life cycle is executed, we call the corresponding hook function:
import { onBeforeMount, onMounted } from "vue"; export default { setup() { console.log("----setup----"); onBeforeMount(() => { // beforeMount code execution }); onMounted(() => { // mounted code execution }); }, }
New features
After finishing the life cycle, here are the new features we expect from Vue3.
Responsive API
After in-depth study of Object.defineProperty and Proxy, we explained the advantages of Proxy and why Vue3 uses Proxy to implement responsive. At the same time, Vue3 also separates some responsive API s for better code reuse.
We can use reactive to create a responsive state for JS objects:
import { reactive, toRefs } from "vue"; const user = reactive({ name: 'Vue2', age: 18, }); user.name = 'Vue3'
reactive is equivalent to Vue.observable in Vue2.x.
The reactive function only receives complex data types such as object and array.
For some basic data types, such as strings and values, we want to make them responsive. Of course, we can also create objects through the reactive function, but Vue3 provides another function ref:
import { ref } from "vue"; const num = ref(0); const str = ref(""); const male = ref(true); num.value++; console.log(num.value); str.value = "new val"; console.log(str.value); male.value = false; console.log(male.value);
The response object returned by ref is a RefImpl object that contains only one parameter named value. It is obtained and modified in js through its value attribute; However, when rendered in the template, the internal values are automatically expanded, so there is no need to append. Value to the template.
<template> <div> <span>{{ count }}</span> <button @click="count ++">Increment count</button> </div> </template> <script> import { ref } from 'vue' export default { setup() { const count = ref(0) return { count } } } </script>
reactive is mainly responsible for complex data structures, while ref mainly deals with basic data structures; However, many children's shoes misunderstand that REF can only handle basic data, and ref itself can also handle objects and arrays:
import { ref } from "vue"; const obj = ref({ name: "qwe", age: 1, }); setTimeout(() => { obj.value.name = "asd"; }, 1000); const list = ref([1, 2, 3, 4, 6]); setTimeout(() => { list.value.push(7); }, 2000);
When dealing with the properties of some large responsive objects, we hope to use the deconstruction of ES6 to obtain the values we want:
let book = reactive({ name: 'Learn Vue', year: 2020, title: 'Chapter one' }) let { name, } = book name = 'new Learn' // Learn Vue console.log(book.name);
Unfortunately, this will eliminate its responsiveness; In this case, we can convert the responsive object into a set of ref s, which will retain the responsive association with the source object:
let book = reactive({ name: 'Learn Vue', year: 2020, title: 'Chapter one' }) let { name, } = toRefs(book) // Note that the deconstructed name here is a ref object // Value assignment through value is required name.value = 'new Learn' // new Learn console.log(book.name);
For some read-only data, we want to prevent any changes from happening through readonly To create a read-only object:
import { reactive, readonly } from "vue"; let book = reactive({ name: 'Learn Vue', year: 2020, title: 'Chapter one' }) const copy = readonly(book); //Set operation on key "name" failed: target is readonly. copy.name = "new copy";
Sometimes the value we need depends on the state of other values. In vue2.x, we use the calculated function to calculate the attribute. In vue3, we extract the calculated function. It accepts a getter function and creates an "immutable" responsive ref object for the value returned by the getter:
const num = ref(0); const double = computed(() => num.value * 2); num.value++; // 2 console.log(double.value); // Warning: computed value is readonly double.value = 4
Alternatively, we can use the get and set functions to create a read-write ref object:
const num = ref(0); const double = computed({ get: () => num.value * 2, set: (val) => (num.value = val / 2), }); num.value++; // 2 console.log(double.value); double.value = 8 // 4 console.log(num.value);
Responsive listening
Corresponding to computed is watch. Computed is a many to one relationship, while watch is a one to many relationship; vue3 also provides two functions to listen for changes in the data source: watch and watchEffect.
Let's first look at the usage of watch, which is exactly the same as that of the watch option of the component. It needs to listen to a data source and then execute a specific callback function. Let's first look at its usage of listening to a single data source:
import { reactive, ref, watch } from "vue"; const state = reactive({ count: 0, }); //Return value getter function when listening watch( () => state.count, (count, prevCount) => { // 1 0 console.log(count, prevCount); } ); state.count++; const count = ref(0); //Listen directly to ref watch(count, (count, prevCount) => { // 2 0 console.log(count, prevCount, "watch"); }); count.value = 2;
We can also put multiple values into an array for listening, and the last value is also returned in the form of an array:
const state = reactive({ count: 1, }); const count = ref(2); watch([() => state.count, count], (newVal, oldVal) => { //[3, 2] [1, 2] //[3, 4] [3, 2] console.log(newVal, oldVal); }); state.count = 3; count.value = 4;
If we want to listen for the property changes of a deeply nested object, we need to set deep:true:
const deepObj = reactive({ a: { b: { c: "hello", }, }, }); watch( () => deepObj, (val, old) => { // new hello new hello console.log(val.a.b.c, old.a.b.c); }, { deep: true } ); deepObj.a.b.c = "new hello";
It can be found that the final print results are all changed values. This is because listening to a responsive object always returns the reference of the object. Therefore, we need to make a deep copy of the value:
import _ from "lodash"; const deepObj = reactive({ a: { b: { c: "hello", }, }, }); watch( () => _.cloneDeep(deepObj), (val, old) => { // new hello new hello console.log(val.a.b.c, old.a.b.c); }, { deep: true } ); deepObj.a.b.c = "new hello";
Generally, listening will stop automatically when the component is destroyed, but sometimes we want to stop manually before the component is destroyed. We can call the stop function returned by watch to stop:
const count = ref(0); const stop = watch(count, (count, prevCount) => { // Do not execute console.log(count, prevCount); }); setTimeout(()=>{ count.value = 2; }, 1000); // Stop watch stop();
Another function, watchEffect, can also be used to listen, but there is already a watch. What is the difference between this watchEffect and watch? Their usage is mainly different in the following points:
-
watchEffect does not need to manually pass in dependencies
-
At each initialization, watchEffect executes a callback function to automatically obtain dependencies
-
watchEffect cannot get the original value, but only the changed value
import { reactive, ref, watch, watchEffect } from "vue"; const count = ref(0); const state = reactive({ year: 2021, }); watchEffect(() => { console.log(count.value); console.log(state.year); }); setInterval(() => { count.value++; state.year++; }, 1000);
watchEffect will automatically execute once when the page is loaded to track the responsive dependency; After loading, when the timer executes every 1s, watchEffect will listen to the change of data and execute automatically. Each execution is to obtain the changed value.
Composite API
The Composition API is also the most important function in Vue3. The previous version 2.x used the Options API, that is, the writing methods are officially defined: data, calculated and methods. You can write wherever you need to write. The problem is that with the increase of functions, the code becomes more and more complex. We see that the code needs to jump up and down repeatedly:
Composition API comparison
In the figure above, a color represents a function. We can see that the function codes of the Options API are scattered; Composition API can organize the logic of the same function into a function for maintenance.
Let's first look at the previous writing of Options API:
export default { components: {}, data() {}, computed: {}, watch: {}, mounted() {}, }
The Options API is to put the same type of things in the same option. When we have less data, this organization method is relatively clear; However, with the increase of data, the function points we maintain will involve multiple data and methods, but we can't perceive which data and methods need to be involved. We often need to switch back and forth to find, and even need to understand the logic of other functions, which also makes it difficult for components to understand and read.
What the Composition API does is to maintain the codes of the same function together, so that when we need to maintain a function point, we don't need to care about other logic, but only focus on the current function; The Composition API organizes code through the setup option:
export default { setup(props, context) {} };
We can see here that it receives two parameters props and context. Props is some data passed in by the parent component. Context is a context object and some attributes exposed from 2.x:
-
attrs
-
slots
-
emit
Note: props data also needs to be deconstructed through toRefs, otherwise the responsive data will become invalid.
Let's look at the specific usage of setup through a Button button:
Take a chestnut
<template> <div>{{ state.count }} * 2 = {{ double }}</div> <div>{{ num }}</div> <div @click="add">Add</div> </template> <script> import { reactive, computed, ref } from "vue"; export default { name: "Button", setup() { const state = reactive({ count: 1, }); const num = ref(2); function add() { state.count++; num.value += 10; } const double = computed(() => state.count * 2); return { state, double, num, add, }; }, }; </script>
Many children's shoes may have doubts. It's no different from what I wrote in data and methods. Isn't it just putting them together? We can extract and divide the functions in setup into one independent function, and each function can also be logically reused in different components:
export default { setup() { const { networkState } = useNetworkState(); const { user } = userDeatil(); const { list } = tableData(); return { networkState, user, list, }; }, }; function useNetworkState() {} function userDeatil() {} function tableData() {}
Fragment
The so-called Fragment is a Fragment; In vue2.x, each template must have a root node, so our code should be written as follows:
<template> <div> <span></span> <span></span> </div> </template>
Or Vue fragments library can be introduced into Vue2.x to replace div with a virtual fragment; In React, the solution is to create a virtual element through a React.Fragment tag; In Vue3, we do not need the root node directly:
<template> <span>hello</span> <span>world</span> </template>
In this way, there are many meaningless div elements.
Teleport
Teleport translates to transmission and long-distance transmission; As the name suggests, it can transfer elements or components in the slot to other locations on the page:
In React, you can use the createPortal function to create nodes that need to be transmitted; Originally, I wanted to call it Portal, but the H5 native Portal tag is also planned. Although there are some security problems, in order to avoid duplicate names, it is changed to Teleport.
A common usage scenario for Teleport is to transfer the position of the modal box in some deeply nested components. Although the modal box logically belongs to this component, in terms of style and DOM structure, it is not conducive to maintenance when the nesting level is deep (z-index and other problems); Therefore, we need to peel it off:
<template> <button @click="showDialog = true">Open modal box</button> <teleport to="body"> <div class="modal" v-if="showDialog" style="position: fixed"> I am a modal box <button @click="showDialog = false">close</button> <child-component :msg="msg"></child-component> </div> </teleport> </template> <script> export default { data() { return { showDialog: false, msg: "hello" }; }, }; </script>
Here, the modal div in the report is transferred to the bottom of the body; Although rendering is performed in different places, the elements and components in the report belong to the logical sub components of the parent component, and can still communicate with the parent component. Report receives two parameters, to and disabled:
-
to - string: must be a valid query selector or HTMLElement, and can be id or class selector.
-
disabled - boolean: if true, it means that the function of teleport is disabled, and its slot content will not be moved to any location. By default, false is not disabled.
Suspense
Suspend is a built-in component launched by Vue3. It allows our program to render some backup content while waiting for asynchronous components, which allows us to create a smooth user experience; The asynchronous component loaded in Vue actually exists in Vue2.x. the routing component loaded in Vue router we use is also an asynchronous component:
export default { name: "Home", components: { AsyncButton: () => import("../components/AsyncButton"), }, }
Redefine in Vue3. Asynchronous components need to be defined by defining asynccomponent:
// Globally define asynchronous components //src/main.js import { defineAsyncComponent } from "vue"; const AsyncButton = defineAsyncComponent(() => import("./components/AsyncButton.vue") ); app.component("AsyncButton", AsyncButton); // Define asynchronous components within components // src/views/Home.vue import { defineAsyncComponent } from "vue"; export default { components: { AsyncButton: defineAsyncComponent(() => import("../components/AsyncButton") ), }, };
At the same time, the of asynchronous components can be more finely managed:
export default { components: { AsyncButton: defineAsyncComponent({ delay: 100, timeout: 3000, loader: () => import("../components/AsyncButton"), errorComponent: ErrorComponent, onError(error, retry, fail, attempts) { if (attempts <= 3) { retry(); } else { fail(); } }, }), }, };
In this way, we can control the loading of asynchronous components, and reload or display the abnormal state in case of loading failure:
Asynchronous component loading failed
Let's go back to suspend. As mentioned above, it mainly renders some backup contents during component loading. It provides two slot slots, one is the default, and the other is the state of fallback loading:
<template> <div> <button @click="showButton">Show asynchronous components</button> <template v-if="isShowButton"> <Suspense> <template #default> <AsyncButton></AsyncButton> </template> <template #fallback> <div>Component loading...</div> </template> </Suspense> </template> </div> </template> <script> export default { setup() { const isShowButton = ref(false); function showButton() { isShowButton.value = true; } return { isShowButton, showButton, }; }, } </script>
Asynchronous component loading display placeholder
Incompatible features
Incompatible functions mainly include some syntax that has changed greatly from Vue2.x, and there may be compatibility problems on Vue3.
data, mixin and filter
In Vue2.x, we can define data as object or function, but we know that if data is an object in the component, data will affect each other, because object is a reference data type;
In Vue3, data only accepts function type and returns objects through function; At the same time, the merging behavior of mixin has also changed. When mixin merges with data in the base class, shallow copy merging will be performed:
const Mixin = { data() { return { user: { name: 'Jack', id: 1, address: { prov: 2, city: 3, }, } } } } const Component = { mixins: [Mixin], data() { return { user: { id: 2, address: { prov: 4, }, } } } } // vue2 results: { id: 2, name: 'Jack', address: { prov: 4, city: 3 } } // vue3 results: user: { id: 2, address: { prov: 4, }, }
We can see the result of the final merge. vue2.x will make a deep copy to merge and copy the data in data; vue3 only makes shallow copies. If it is found that the data in data already exists, it will not merge the copies.
In vue2.x, we can also process the display of some text content through the filter:
<template> <div>{{ status | statusText }}</div> </template> <script> export default { props: { status: { type: Number, default: 1 } }, filters: { statusText(value){ if(value === 1){ return 'Order not placed' } else if(value === 2){ return 'Order to be paid' } else if(value === 3){ return 'Order completed' } } } } </script>
The most common is to deal with the copywriting display of some orders; However, in vue3, the filter has been deleted and is no longer supported. It is officially recommended to use method call or calculated attribute instead.
v-model
In Vue2.x, v-model is equivalent to binding value attribute and input event. In essence, it is also a syntax sugar:
<child-component v-model="msg"></child-component> <!-- amount to --> <child-component :value="msg" @input="msg=$event"></child-component>
In some cases, we need to bind multiple values in two directions, and other values need to be displayed. Use the callback function to change:
<child-component v-model="msg" :msg1="msg1" @change1="msg1=$event" :msg2="msg2" @change2="msg2=$event"> </child-component>
The. sync modifier is introduced in vue2.3.0 +, which is essentially a syntax sugar. It binds the @ update:propName callback on the component, and the syntax is more concise:
<child-component :msg1.sync="msg1" :msg2.sync="msg2"> </child-component> <!-- amount to --> <child-component :msg1="msg1" @update:msg1="msg1=$event" :msg2="msg2" @update:msg2="msg2=$event"> </child-component>
Vue3 integrates the functions of v-model and. sync, abandoning. sync, which means that multiple two-way bound value values can be directly passed through multiple v-models; At the same time, the default prop name passed by v-model is changed from value to modelValue:
<child-component v-model="msg"> </child-component> <!-- amount to --> <child-component :modelValue="msg" @update:modelValue="msg = $event"> </child-component>
If we want to pass multiple values through the v-model, we can pass an argument to the v-model:
<child-component v-model.msg1="msg1" v-model.msg2="msg2"> </child-component> <!-- amount to --> <child-component :msg1="msg1" @update:msg1="msg1=$event" :msg2="msg2" @update:msg2="msg2=$event"> </child-component>
v-for and key
In Vue2.x, we all know that each cycle of v-for requires a unique key for each child node, which cannot be bound to the template tag,
<template v-for="item in list"> <div :key="item.id">...</div> <span :key="item.id">...</span> </template>
In Vue3, the key value should be placed on the template tag, so we don't have to set it for each child node:
<template v-for="item in list" :key="item.id"> <div>...</div> <span>...</span> </template>
v-bind merge
In vue2.x, if an element defines both v-bind="object" and the same separate attribute, the separate attribute will override the binding in the object:
<div id="red" v-bind="{ id: 'blue' }"></div> <div v-bind="{ id: 'blue' }" id="red"></div> <!-- The final results are the same --> <div id="red"></div>
However, in vue3, if an element defines both v-bind="object" and the same individual attribute, the order of declaring binding determines the final result (the latter overrides the former):
<!-- template --> <div id="red" v-bind="{ id: 'blue' }"></div> <!-- result --> <div id="blue"></div> <!-- template --> <div v-bind="{ id: 'blue' }" id="red"></div> <!-- result --> <div id="red"></div>
ref in v-for
In vue2.x, use the ref attribute on v-for. Through this.$refs, you will get an array:
<template <div v-for="item in list" :ref="setItemRef"></div> </template> <script> export default { data(){ list: [1, 2] }, mounted () { // [div, div] console.log(this.$refs.setItemRef) } } </script>
But this may not be the result we want; Therefore, vue3 no longer automatically creates an array, but changes the processing method of ref to a function, which is passed into the node by default:
<template <div v-for="item in 3" :ref="setItemRef"></div> </template> <script> import { reactive, onUpdated } from 'vue' export default { setup() { let itemRefs = reactive([]) const setItemRef = el => { itemRefs.push(el) } onUpdated(() => { console.log(itemRefs) }) return { itemRefs, setItemRef } } } </script>
v-for and v-if priority
In vue2.x, v-for and v-if are used on one element at the same time. v-for has a higher priority. Therefore, an important point for performance optimization in vue2.x is that v-for and v-if cannot be placed on the same element.
In vue3, v-if has a higher priority than v-for. Therefore, the following code can run normally in vue2.x, but there is no item variable when v-if takes effect in vue3, so an error will be reported:
<template> <div v-for="item in list" v-if="item % 2 === 0" :key="item">{{ item }}</div> </template> <script> export default { data() { return { list: [1, 2, 3, 4, 5], }; }, }; </script>
summary
The above are some new features and functions that we may involve in Vue3.0 as a terminal. In fact, Vue3.0 still has many changes, which will not be carried out one by one due to space reasons. You can consult the official documents by yourself and look forward to Vue3 bringing us a more convenient and friendly development experience.