preface
There are several general types of data transfer between components in VUE
props / $emit
vuex
event bus
$parent / $children
$ref
provide /inject
$attrs / $listeners
Examples of how to use these tools are props /$emit and vuex, provide /inject and $attrs /$Listeners
provide / inject
Usage scenarios (value transfer between multiple levels of components, using Element-UI design as an example)
The checkbox in element-UI has the following usage scenarios
<el-form ref="form" :model="form" size="small"> <el-form-item label=" active nature "> <el-checkbox-group V-model =" ruleform.type "> <el-checkbox label=" name="type"></el-checkbox label=" name="type"></el-checkbox> <el-checkbox label=" offline theme activity "name="type"></el-checkbox> <el-checkbox label=" pure brand exposure" name="type"></el-checkbox> </el-checkbox-group> </el-form-item> </el-form>Copy the code
The outermost el-form component has the size attribute, which is used to control the size of the component. The el-checkBox also has the size attribute. Generally, if the el-checkbox does not set the size, the el-Checkbox will try to check whether the ancestor element has the size. So if you were to encapsulate the component library, how would you make el-CheckBox get the size of the El-Form?
Implement yourself (props / $emit)
In general, I want to use props /$emit to transfer values between components, so it is easy to get the following implementation:
// app.vue
<template>
<el-form size="medium"></el-form>
</template>
Copy the code
// el-form.vue
<template>
<el-form-item :size="size"></el-form-item>
</template>
<script>
...
props: {
size: {
type: String,
require: false,
}
}
...
</script>
Copy the code
// el-checkbox-group.vue
<template>
<el-checkbox :size="size"></el-checkbox>
</template>
<script>
...
props: {
size: {
type: String,
require: false,
}
}
...
</script>
Copy the code
// el-checkbox.vue
<template>
<div :class="[size]">checkbox</div>
</template>
<script>
...
props: {
size: {
type: String,
require: false,
default: "",
}
}
...
</script>
<style>
.small {
width: 100px;
}
.medium {
width: 200px;
}
.large {
width: 300px;
}
</style>
Copy the code
As shown above, the problem with this implementation is that the size attribute is passed layer by layer, which can be cumbersome. So how does the element-UI implementation work
Element – the UI
// el-form.vue <script> ... provide() { return { elForm: this }; . </script> },Copy the code
// el-form-item <script> ... provide() { return { elFormItem: this }; }, inject: ['elForm'], computed: { _formSize() { return this.elForm.size; }, elFormItemSize() { return this.size || this._formSize; }}... </script>Copy the code
// el-checkbox <script> ... inject: { elForm: { default: '' }, elFormItem: { // el-form-item default: '' } }, computed: { _elFormItemSize() { return (this.elFormItem || {}).elFormItemSize; }, checkboxSize() { const temCheckboxSize = this.size || this._elFormItemSize || (this.$ELEMENT || {}).size; return this.isGroup ? this._checkboxGroup.checkboxGroupSize || temCheckboxSize : temCheckboxSize; }}... </script>Copy the code
As shown above, the element-UI is actually quite complex and takes a lot of consideration, but the general delivery process of the size attribute is exposed by provide and injected into the el-checkbox
Self-implementation (provide/inject)
// app.vue
<template>
<el-form size="medium"></el-form>
</template>
Copy the code
// el-form.vue
<template>
<el-form-item :size="size"></el-form-item>
</template>
<script>
...
props: {
size: {
type: String,
require: false,
}
}
provide () {
return {
size: this.size
}
}
...
</script>
Copy the code
// el-checkbox.vue
<template>
<div :class="[size]">checkbox</div>
</template>
<script>
...
inject: ['size']
...
</script>
<style>
.small {
width: 100px;
}
.medium {
width: 200px;
}
.large {
width: 300px;
}
</style>
Copy the code
This way, you don’t have to pass through the middle layer many times. It’s easier
Take a look at the vUE website
There are two main messages:
- Suitable for use in component libraries/advanced plug-ins, not normal code
- The bound value is not commensurable, but if you pass a listening object, the object’s property will respond
Here is A demo. Can you guess if A and B in the outter-click and inner-click views change?
// app.vue
<template>
<c-a></c-a>
<button @click="handleClick">outter-click</button>
</template>
<script>
...
provide () {
return {
pa: this.a,
pb: this.b,
}
},
data() {
return {
a: 1,
b: {
v: 2
}
}
},
methods: {
handleClick() {
this.a = Math.random() + 'outer pa'
this.b.v = Math.random() + 'outer pb'
}
}
...
</script>
Copy the code
// c-a.vue
<template>
<div>
<div>
A: {{pa}}
</div>
<div>
B {{pb.v}}
</div>
<button @click="handleClick">inner-click</button>
</div>
</template>
<script>
export default {
name: 'CA.vue',
inject: ['pa', 'pb'],
methods: {
handleClick() {
this.pb.v = Math.random() + 'inner pa'
this.pa = Math.random() + 'inner pb'
}
}
}
</script>
Copy the code
Provide/inject source code
To explain the above phenomenon, you can actually look at the source code
export function initInjections (vm: Component) { const result = resolveInject(vm.$options.inject, vm) if (result) { toggleObserving(false) Object.keys(result).forEach(key => { /* istanbul ignore else */ if (process.env.NODE_ENV ! == 'production') { defineReactive(vm, key, result[key], () => { warn( `Avoid mutating an injected value directly since the changes will be ` + `overwritten whenever the provided component re-renders. ` + `injection being mutated: "${key}"`, vm ) }) } else { defineReactive(vm, key, result[key]) } }) toggleObserving(true) } } export function resolveInject (inject: any, vm: Component): ? Object { if (inject) { // inject is :any because flow is not smart enough to figure out cached const result = Object.create(null) const keys = hasSymbol ? Reflect.ownKeys(inject) : Object.keys(inject) for (let i = 0; i < keys.length; i++) { const key = keys[i] // #6574 in case the inject object is observed... if (key === '__ob__') continue const provideKey = inject[key].from let source = vm while (source) { if (source._provided && hasOwn(source._provided, provideKey)) { result[key] = source._provided[provideKey] break } source = source.$parent } if (! source) { if ('default' in inject[key]) { const provideDefault = inject[key].default result[key] = typeof provideDefault === 'function' ? provideDefault.call(vm) : provideDefault } else if (process.env.NODE_ENV ! == 'production') { warn(`Injection "${key}" not found`, vm) } } } return result } }Copy the code
- In the resolveInject function
result[key] = source._provided[provideKey]
B is A reference type, so the view has changed, while A is A primitive type, so the view has not changed - The initInjections function
DefineReactive function
Inner -click is A bidirectional binding operation in vue, so bidirectional binding is made for every attribute in the inject object, which explains inner-click phenomenon, A, B are changed
$attrs / $listeners
Scenario (Secondary encapsulation of components)
Often in project development, we will encounter scenarios where some component styles presented to us by visual design do not match the styles in the component library we are using, and the component needs to be rewrapped. If we use Vant for development, the buttons in the component provided by the vision are all round. Van-button’s round attribute can meet the requirements. In order not to write
Implement yourself (props / $emit)
// app.vue
<template>
<my-button :text="button.btnTxt" @btnClick="handleBtnClick" round></my-button>
</template>
Copy the code
// my-button.vue
<template>
<van-button :round="round" @click="$emit('btnClick')">{{text}}</van-button>
</template>
<script>
export default {
name: 'MyButton.vue',
props: {
text: {
require: true,
type: String,
},
round: {
require: false,
type: Boolean,
default: false
},
}
}
</script>
Copy the code
The problem here is that the click event round property is a capability that Ant-Button already provides, and we’re passing it around. In other words, if we don’t handle it, then we can’t use it. If we had to use all of the attributes, we would have to pass all of the attributes, which would add a lot of work. So can we just use the capabilities van-Button provides?
$Listeners are available on vue website for $attrs / $Listeners
$listeners / $listeners
<template>
<my-button :text="button.btnTxt" @Click="handleBtnClick" round></my-button>
</template>
Copy the code
<template>
<van-button v-bind="$attrs" v-on="$listeners"></van-button>
</template>
<script>
export default {
name: 'MyButton'
}
</script>
Copy the code
conclusion
provide / inject
Usage scenario: This can be used when there is a value transfer between multiple components, but it is generally not encouraged to change the value in the parent component directly (this can make the data flow in the VUE very messy).$attrs / $listeners
Application scenario: Encapsulate components twice