preface
vue is a framework for data-driven view update. In our usual development, we will split different modules of the page into vue components. Therefore, for vue, the data communication between components is very important, so how to conduct data communication between components?
First of all, we need to know what kind of relationship exists between components in vue, so that it is easier to understand their communication mode.
Generally, we have the following relationships:
- Communication between parent and child components
- Communication between non parent and child components (brother components, intergenerational relationship components, etc.)
props / $emit
The parent component passes data to the child component through props, and the child component can communicate with the parent component through $emit.
- Parent component passes value to child component
<!-- Parent component --> <template> <div class="section"> <child :msg="articleList"></child> </div> </template> <script> import child from './child.vue' export default { name: 'HelloWorld', components: { comArticle }, data() { return { msg: 'Aliwang' } } } </script> Copy code
<!-- Subcomponents child.vue --> <template> <div> {{ msg }} </div> </template> <script> export default { props: { msg: String } } </script> Copy code
be careful:
Prop can only be passed from the upper level component to the lower level component (parent-child component), that is, the so-called one-way data flow. Moreover, prop is read-only and cannot be modified. All modifications will be invalidated and warned.
- First, the prop should not be changed inside a sub component, which will destroy the one-way data binding and make the data flow difficult to understand. If there is such a need, you can receive it through the data attribute or use the computed attribute for conversion.
- Second, if props passes a reference type (object or array), changing this object or array in the sub component will also update the status of the parent component accordingly. Using this, we can realize the "two-way binding" of the parent and child component data. Although this implementation can save code, it will sacrifice the simplicity of the data flow, which is difficult to understand. It is best not to do so.
- To realize the data "two-way binding" of parent and child components, you can use v-model or sync.
- Child component passes value to parent component
The principle of using $emit to transmit data to the parent component is as follows: the parent component listens to the function and receives parameters through v-on in the child component, and the vue framework listens to your fn event function of v-on="fn" in the child component. Using $emit in the child component can trigger it. Here is an example.
<!-- Parent component --> <template> <div class="section"> <child :msg="articleList" @changMsg="changMsg"></child> </div> </template> <script> import child from './child.vue' export default { name: 'HelloWorld', components: { comArticle }, data() { return { msg: 'Aliwang' } }, methods:{ changMsg(msg) { this.msg = msg } } } </script> Copy code
<!-- Subcomponents child.vue --> <template> <div> {{ msg }} <button @click="change">Change string</button> </div> </template> <script> export default { props: { msg: String }, methods: { change(){ this.$emit('changMsg', 'A Li Wang takes you to learn the front end') } } } </script> Copy code
v-model instruction
v-model is used to create two-way binding on form controls or components. Its essence is the syntax sugar of v-bind and v-on. Using v-model on a component will bind prop named value and event named input for the component by default.
When a prop in our component needs to implement the above-mentioned "two-way binding", v-model can play a big role. With it, you don't need to manually bind on the component to listen to the custom events on the current instance, which will make the code more concise.
Next, the application of v-model is introduced with the core code of an input component.
<!--Parent component--> <template> <base-input v-model="inputValue"></base-input> </template> <script> export default { data() { return { input: '' } }, } </script> Copy code
<!--Subcomponents--> <template> <input type="text" :value="currentValue" @input="handleInput"> </template> <script> export default { data() { return { currentValue: this.value === undefined || this.value === null ? '' } }, props: { value: [String, Number], // Key 1 }, methods: { handleInput(event) { const value = event.target.value; this.$emit('input', value); // Key 2 }, }, } </script> Copy code
As shown in the above example, the essence of v-model="inputValue" is the syntax sugar of v-bind and v-on. By default, the parent component binds the attribute named: value="inputValue" and the @input= "(V) = > {this. Inputvalue = V}" event. The child component passes this$ Emit ('input', value) notifies the parent component
Therefore, his principle also uses the method of pass parameter props / $emit of parent-child components mentioned above to realize two-way binding
Sometimes, in some specific controls, the attribute named value has a special meaning. At this time, this conflict can be avoided through the v-model option.
. sync modifier
- . Sync modifier in Vue 1 X version has been provided, 1 In the X version, when the subcomponent changes a with The value of sync's prop will be synchronized to the value in the parent component. This is very convenient to use, but the problem is also very obvious. This destroys the one-way data flow. When the application is complex, the cost of debug ging will be very high.
- As a result, it was removed in vue 2.0 sync. But in practical applications Sync has its application scenarios, so in Vue version 2.3, it ushers in a new one sync.
- new. The sync modifier is no longer a real two-way binding. Its essence is similar to v-model, but an abbreviation.
Examples of normally packaged components:
<text-document v-bind:title="doc.title" v-on:update:title="doc.title = $event" /> Copy code
The above code, use sync can be written as
<text-document v-bind:title.sync="doc.title" /> Copy code
In this way, in the sub component, you can re assign the prop through the following code.
this.$emit('update:title', newTitle) Copy code
See here, is it found The sync modifier is very similar to v-model and is also a syntax sugar, v-bind:title Sync is equivalent to v-bind:title="doc.title" v-on:update:title="doc.title = $event"
v-model and sync comparison
. sync is very similar to v-model in function. It is to realize the "two-way binding" of data. In essence, it is not a real two-way binding, but a syntax sugar.
In contrast sync is more flexible. It can be used by multiple prop s, while v-model can only have one in a component.
Semantically, the value of v-model binding refers to the binding value of this component, such as input component, select component, date and time selection component, color selector component. It is more appropriate to use v-model for the binding value of these components. In other cases, there is no such semantics, and I think it is used sync is better.
parent/parent / parent/children
You can access the instance of the component through $parent and $children. What does the instance represent? Represents all methods and data that can access this component. The following are the following:
<!-- Parent component --> <template> <div class="hello_world"> <div>{{msg}}</div> <com-a></com-a> <button @click="changeA">Click to change the sub component value</button> </div> </template> <script> import ComA from './test/comA.vue' export default { name: 'HelloWorld', components: { ComA }, data() { return { msg: 'Welcome' } }, methods: { changeA() { // Get sub component A this.$children[0].messageA = 'this is new value' } } } </script> Copy code
<!-- Subcomponents --> <template> <div class="com_a"> <span>{{messageA}}</span> <p>Get the value of the parent component as: {{parentVal}}</p> </div> </template> <script> export default { data() { return { messageA: 'this is old' } }, computed:{ parentVal(){ return this.$parent.msg; } } } </script> Copy code
Pay attention to the boundary condition. For example, taking $parent on \app gets an instance of new Vue(), taking $parent on this instance gets undefined, and taking $children on the bottom sub component is an empty array. Also note that the values of parent and parent are different from those of parent and children. The value of $children is an array, and $parent is an object
props $emit and $parent $children are used for the communication between parent and child components, and it is more common to use props for the communication between parent and child components. Neither of them can be used for the communication between non parent and child components.
provide / inject
provide / inject is a new api in vue2.2.0. In short, it means that variables are provided in the parent component through provide, and then injected into the child component through inject.
Official description : this pair of options need to be used together to allow an ancestor component to inject a dependency into all its descendants, no matter how deep the component level is, and it will always take effect when its upstream and downstream relationships are established
The provide option should be
- An object or a function that returns an object. This object contains attributes that can be injected into its descendants. You can use ES2015 Symbols as the key in this object, but symbols and reflect are only supported natively It can work in the environment of ownkeys.
The inject option should be:
- An array of strings
- An object (click for details here)
Basic Usage
// Ancestor component provides foo //First kind export default { name: "father", provide() { return { foo: 'hello' } }, } //The second kind export default { name: "father", provide: { foo:'hello~~~~' }, } Copy code
//The descendant component is injected into foo and directly treated as this foo to use export default { inject:['foo'], } Copy code
Is there any difference between the above two usages?
- If you just pass a string, like hello above, there is no difference. Future generations can read it.
- If you need the value of this object attribute (the code shown below), then the second kind cannot be passed on, and the descendant component cannot get the data. So it is suggested to write only the first one
//When you pass objects to offspring provide() { return { test: this.msg } }, Copy code
Note: once some data is injected, such as foo in the above example, the data of foo can no longer be declared in this component, because it has been occupied by the parent.
provide and inject bindings are not responsive.
This is deliberately done. However, if you pass in a listening object, its object properties are still responsive. Because the object is a reference type.
Let's start with an example of value type data (that is, string), which will not respond
provide(){ return{ test:this.msg } }, data() { return { msg: "Welcome to Your Vue.js App", } } mounted(){ setTimeout(()=>{ this.msg = "halo world"; console.log(this._provided.msg) //log: Welcome to Your Vue.js App },3000) }, Copy code
As shown above, it's impossible to do this. Print it out_ The data in provided has not changed, and the value obtained by the sub component has not changed.
You can even give it directly to this_ provided.msg assignment, but even_ provided. The value in MSG has changed, and the value of the sub component remains unchanged.
When your parameter is an object, you can respond as follows:
provide(){ return{ test:this.activeData } }, data() { return { activeData:{name:'halo'}, } } mounted(){ setTimeout(()=>{ this.activeData.name = 'world'; },3000) } Copy code
This is what vue officially says: the attributes of objects are responsive
provide/inject to implement global variables
Isn't provide/inject only passed from ancestors to descendants? Yes, but if we bind to the top-level component app Vue, whether all descendants receive it, is used as a global variable.
//app.vue export default { name: 'App', provide(){ return{ app:this } }, data(){ return{ text:"it's hard to tell the night time from the day" } }, methods:{ say(){ console.log("Desperado, why don't you come to your senses?") } } } Copy code
//All other sub components that need global variables only need to be injected into the app on demand export default { inject:['foo','app'], mounted(){ console.log(this.app.text); // Get variables in app this.app.say(); // You can execute the methods in the app and become a global method! } } Copy code
provide/inject to refresh the page without flashing
- Reroute to the current page with Vue router, and the page will not be refreshed
- Adopt window Reload(), or router When go (0) is refreshed, the entire browser is reloaded, flashing, and the experience is poor
So what do we do?
Similar to the above principle, we only write a function in the component that controls routing (use v-if to control the display and hiding of router view, and the principle here will not be repeated), then pass this function to descendants, and then call this method in the descendant component to refresh the route.
//app.vue <router-view v-if="isShowRouter"/> export default { name: 'App', provide() { return { reload: this.reload } }, data() { return { isShowRouter: true, } }, methods:{ reload() { this.isShowRouter = false; this.$nextTick(() => { this.isShowRouter = true; }) } } } Copy code
//Descendant component export default { inject: ['reload'], } Copy code
Here, provide uses a function to pass to descendants, and then descendants call this function. This idea is also the idea that children and descendants can pass parameters to parent components for communication. The principle here is very similar to that of event subscription and publishing
ref / $refs
Ref: if it is used on ordinary DOM elements, the reference refers to DOM elements; If it is used on a sub component, the reference points to the component instance. You can directly call the component's methods or access data through the instance. Let's take an example of ref to access the component:
// Sub assembly A.vue export default { data () { return { name: 'Vue.js' } }, methods: { sayHello () { console.log('hello') } } } Copy code
// Parent component app vue <template> <component-a ref="comA"></component-a> </template> <script> export default { mounted () { const comA = this.$refs.comA; console.log(comA.name); // Vue.js comA.sayHello(); // hello } } </script> Copy code
ref this way is to get the instance of the child component, and then directly access the method of the child component and the data of the operation data. It is a way for the parent component to control the child component. If the child component wants to pass parameters or operations to the parent component, it can only do so in other ways
eventBus
In fact, the principle of eventBus is event subscription and publishing. eventBus is also called event bus. It can be used as a concept of communication bridge in vue, just like all components share the same event center, which can register to send events or receive events, so components can notify other components.
Here we can directly use the event monitoring provided by vue, that is, emitmit emiton. Let's simply encapsulate:
- First, you need to create an event bus and export it so that other modules can use or listen to it
Create a new event bus JS file
// event-bus.js import Vue from 'vue' export const EventBus = new Vue() Copy code
- Occurrence of events
Suppose you have two components: additionNum and showNum, which can be brother components or parent-child components; Here we take brother components as an example:
<template> <div> <show-num-com></show-num-com> <addition-num-com></addition-num-com> </div> </template> <script> import showNumCom from './showNum.vue' import additionNumCom from './additionNum.vue' export default { components: { showNumCom, additionNumCom } } </script> Copy code
// addtionNum. Send events in Vue <template> <div> <button @click="additionHandle">+adder</button> </div> </template> <script> import { EventBus } from './event-bus.js' console.log(EventBus) export default { data() { return { num: 1 } }, methods: { additionHandle() { EventBus.$emit('addition', { num: this.num++ }) } } } </script> Copy code
- Receive events
// showNum. Receive events in Vue <template> <div>Calculation and: {{count}}</div> </template> <script> import { EventBus } from './event-bus.js' export default { data() { return { count: 0 } }, mounted() { EventBus.$on('addition', param => { this.count = this.count + param.num; }) } } </script> Copy code
In this way, the addionnum Click the Add button in Vue and click shownum In Vue, the sum result is displayed by using the passed num
- Remove event listener
If you want to remove event listening, you can do the following:
import { eventBus } from 'event-bus.js' EventBus.$off('addition') Copy code
Package a set of eventBus by yourself
It's also OK to use your own package of eventBus here, which is convenient for you to do whatever you want. Here's a package for you
/* eslint-disable no-console */ // Event mapping table let eventMap = {} /** * Listen for events * @param {string} eventName Event name * @param {function} listener Callback function * @param {object} instance Register instances of events */ function on(eventName, listener, instance) { eventMap[eventName] = eventMap[eventName] || [] eventMap[eventName].push({ listener, instance, }) } // Listen to the event and execute it only once function once(eventName, listener, instance) { eventMap[eventName] = eventMap[eventName] || [] eventMap[eventName].push({ listener, instance, once: true, }) } // Disable event listening function off(eventName, listener) { // Disable all event listening if (!eventName) { eventMap = {} return } // No corresponding event if (!eventMap[eventName]) { return } // Disable listening for an event eventMap[eventName].forEach((currentEvent, index) => { if (currentEvent.listener === listener) { eventMap[eventName].splice(index, 1) } }) } // Send the event and execute the corresponding response function function emit(eventName, ...args) { if (!eventMap[eventName]) { return } eventMap[eventName].forEach((currentEvent, index) => { currentEvent.listener(...args) if (currentEvent.once) { eventMap[eventName].splice(index, 1) } }) } // Display the currently registered events, which are used in code optimization function showEventMap(targetEventName) { if (targetEventName) { // View the listening condition of a specific event eventMap[targetEventName].forEach(eventItem => { console.log(targetEventName, eventItem.instance, eventItem.listener) }) } else { // Check the monitoring of all events Object.keys(eventMap).forEach(eventName => { eventMap[eventName].forEach(eventItem => { console.log(eventName, eventItem.instance, eventItem.listener) }) }) } } // Provide vue mixin method to automatically log off event listening in beforeDestroy export const mixin = { created() { // Overload the on function to collect the events monitored by this component. When it is eliminated, destroy the event monitoring this.$eventListenerList = [] this.$event = { off, once, emit, showEventMap } this.$event.on = (eventName, listener) => { this.$eventListenerList.push({ eventName, listener }) on(eventName, listener) } }, // Automatically destroy event listening when eliminating components beforeDestroy() { this.$eventListenerList.forEach(currentEvent => { off(currentEvent.eventName, currentEvent.listener) }) }, } export default { on, off, once, emit, showEventMap } Copy code
How to use it? Just in the main JS, and then vue Mixin is enough, as follows:
// main.js import Vue from 'vue' import { mixin as eventMixin } from '@/event/index' Vue.mixin(eventMixin) Copy code
In other files of vue project, you can directly this$ event. on this.$ event.$ Emit is as follows:
this.$event.on('test', (v) => { console.log(v) }) this.$event.$emit('test', 1) Copy code
By the way, it also encapsulates a mixin. The advantage is that after the vue page listens for events and the page is destroyed, the event listener is also automatically destroyed
Vuex
Vuex introduction
-
Vuex is a specially designed for vue JS application development state management mode. It uses centralized storage to manage the state of all components of the application, and uses corresponding rules to ensure that the state changes in a predictable way
-
Vuex solves the problem that multiple views depend on the same state and the behavior from different views needs to change the same state, and focuses the developer's energy on the update of data rather than the transfer of data between components
Vuex modules
- state: used for data storage. It is the only data source in the store
- getters: like the calculation attribute in vue, the secondary packaging based on state data is often used for data filtering and correlation calculation of multiple data
- mutations: similar functions, the only way to change state data, and cannot be used to process asynchronous events
- actions: similar to mutation, it is used to submit a mutation to change the state without directly changing the state. It can include any asynchronous operation
- modules: similar to namespaces, it is used to define and operate the status of each module separately in the project, which is convenient for maintenance
Vuex instance application
Here, we first create a new store folder to encapsulate Vuex
Add index JS file
// index.js // Automatically mount the store under the specified directory import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) let modules = {} // @/The files in the store/module directory are automatically mounted as store modules const subModuleList = require.context('@/store/modules', false, /.js$/) subModuleList.keys().forEach(subRouter => { const moduleName = subRouter.substring(2, subRouter.length - 3) modules[moduleName] = subModuleList(subRouter).default }) export default new Vuex.Store({ state: {}, mutations: {}, actions: {}, modules }) Copy code
Add the module folder under the store folder, and create a new user in the module folder JS file
// user.js import user from '@/utils/user.js' import userApi from '@/apis/user' import { OPEN_ACCOUNT_STAGE, STAGE_STATUS } from '@/constant' let getUserPromise = null export default { namespaced: true, state() { return { userInfo: null, // User information isLogined: !!user.getToken(), // Have you logged in } }, mutations: { // Update user information updateUser(state, payload) { state.isLogined = !!payload state.userInfo = payload }, }, actions: { // Get current user information async getUserInfo(context, payload) { // forceUpdate indicates whether to force the update if (context.state.userInfo && !payload?.forceUpdate) { return context.state.userInfo } if (!getUserPromise || payload?.forceUpdate) { getUserPromise = userApi.getUserInfo() } // Get user information try { const userInfo = await getUserPromise context.commit('updateUser', userInfo) } finally { getUserPromise = null } return context.state.userInfo }, // Logout async logout(context, payload = {}) { // Whether to exit manually const { manual } = payload if (manual) { await userApi.postLogout() } user.clearToken() context.commit('updateUser', null) }, } } Copy code
Then in the main JS file
import Vue from 'vue' import App from '@/app.vue' import { router } from '@/router' import store from '@/store/index' const vue = new Vue({ el: '#app', name: 'root', router, store, render: h => h(App), }) Copy code
The packaging is very pleasant, and then it can be operated normally.
this.$store.state.user.isLogined this.$store.state.user.userInfo this.$store.commit('user/updateUser', {}) await this.$store.dispatch('user/logout', { manual: true }) Copy code
localStorage / sessionStorage
This kind of communication is relatively simple, but the disadvantage is that the data and status are chaotic and not easy to maintain.
- Through window localStorage. Getitem (key) get data
- Through window localStorage. Setitem (key, value) stores data
Pay attention to json parse() / JSON. Stringify() does data format conversion. localStorage / sessionStorage can be combined with vuex to achieve persistent storage of data. At the same time, vuex can be used to solve the problem of data and state confusion
Implement simple Store mode by yourself
For small projects, communication is very simple. At this time, using Vuex will appear redundant and cumbersome. In this case, it is best not to use Vuex, and you can implement a simple Store in the project yourself.
// store.js const store = { debug: true, state: { author: 'yushihu!' }, setAuthorAction (newValue) { if (this.debug) console.log('setAuthorAction triggered with', newValue) this.state.author = newValue }, deleteAuthorAction () { if (this.debug) console.log('deleteAuthorAction triggered') this.state.author = '' } } export default store Copy code
The above code principle is store JS file exposes an object store. By introducing store JS file to jointly maintain the store object
Like Vuex, the change of state in the store is triggered by the action inside the store, and can be triggered through the console Log() print the trigger trace. This method is very suitable for small projects that do not need to use Vuex.
Compared with the method of $root accessing the root instance, this centralized state management method can be implemented through console Log() record to determine how the current change is triggered, which makes it easier to locate the problem.
Access the root instance through $root
Through $root, any component can obtain the root Vue instance of the current component tree. By maintaining the data on the root instance, data sharing between components can be achieved.
//main.js root instance new Vue({ el: '#app', store, router, // The data attribute of the root instance to maintain general data data: function () { return { author: '' } }, components: { App }, template: '<App/>', }); <!--assembly A--> <script> export default { created() { this.$root.author = 'so' } } </script> <!--assembly B--> <template> <div><span>Author</span>{{ $root.author }}</div> </template> Copy code
Note: Although communication can be realized in this way, any data change at any time in any part of the application will not leave a record of the change. For slightly complex applications, debugging is fatal, and it is not recommended to be used in practical applications.
$attrs and $listeners
Now let's discuss a situation. In the component diagram we gave at the beginning, component A and component D are inter generational. What are the ways they communicate before?
- Use props binding to transfer information level by level. If the state change in D component needs to transfer data to A, use event system to transfer data level by level
- Using eventBus is still more suitable in this case, but when encountering multi person cooperative development, the code maintainability and readability are low
- Vuex is used for data management, but if it is only to transfer data without intermediate processing, it feels a little overqualified to use vuex processing
So there are $attrs / $listeners, which are usually used together with inheritAttrs.
By default, attribute bindings of the parent scope that are not recognized as props will be "rolled back" and applied to the root element of the child component as normal HTML attribute s. When composing a component that wraps a target element or another component, this may not always behave as expected.
By setting inheritAttrs to false, these default behaviors will be removed. These attribute s can be made effective through the instance property $attribs (also added in 2.4), and can be explicitly bound to non root elements through v-bind.
Note: this option does not affect class and style bindings.
The above is the official description: it is still difficult to understand.
Simply put
- inheritAttrs: inherit all attributes except props when true
- inheritAttrs:false inherit only class and style attributes
$attrs: contains attribute bindings (except class and style) that are not considered (and not expected to be) props in the parent scope, and can be passed into internal components through v-bind="$attrs". When a component does not declare any props, it contains all the parent scope bindings (except class and style).
$listeners: contains v-on event listeners in the parent scope (excluding the.native modifier). It can pass in internal components through v-on="$listeners". It is an object that contains all event listeners acting on this component, which is equivalent to that the child component inherits the events of the parent component.
After talking about so many literal concepts, let's take a look at the code example:
Create a new father Vue component
<template> <child :name="name" :age="age" :infoObj="infoObj" @updateInfo="updateInfo" @delInfo="delInfo" /> </template> <script> import Child from '../components/child.vue' export default { name: 'father', components: { Child }, data () { return { name: '123', age: 22, infoObj: { from: 'Hubei', job: 'policeman', hobby: ['reading', 'writing', 'skating'] } } }, methods: { updateInfo() { console.log('update info'); }, delInfo() { console.log('delete info'); } } } </script> Copy code
child.vue components:
<template> <!-- adopt $listeners Pass events in the parent scope into grandSon Component so that it can get father Events in --> <grand-son :height="height" :weight="weight" @addInfo="addInfo" v-bind="$attrs" v-on="$listeners" /> </template> <script> import GrandSon from '../components/grandSon.vue' export default { name: 'child', components: { GrandSon }, props: ['name'], data() { return { height: '180cm', weight: '70kg' }; }, created() { console.log(this.$attrs); // Result: age, infoObj, because the parent component passed a total of three values: name, age, infoObj. Because name was received by props, there are only age and infoobj attributes console.log(this.$listeners); // updateInfo: f, delInfo: f }, methods: { addInfo () { console.log('add info') } } } </script> Copy code
grandSon.vue component
<template> <div> {{ $attrs }} --- {{ $listeners }} <div> </template> <script> export default { props: ['weight'], created() { console.log(this.$attrs); // age, infoObj, height console.log(this.$listeners) // updateInfo: f, delInfo: f, addInfo: f this.$emit('updateInfo') // You can trigger the updateInfo function in the father component } } </script> Copy code
Although this method of value transmission is not commonly used, it does not feel very readable. However, it is deeply nested for the component level, and it will be cumbersome to use props, or the project is relatively small, so it is not suitable to use Vuex. You can consider using it
summary
Common usage scenarios can be divided into three categories:
- Parent child component communication: props, $parent / $children, provide / inject, ref \ $refs, $attrs / $listeners
- Brother component communication: eventBus, vuex, self implementing simple Store mode
- Cross level communication: eventBus, Vuex, self implemented simple Store mode, provide / inject, $attrs / $listeners