preface

There are several general types of data transfer between components in VUE

  1. props / $emit
  2. vuex
  3. event bus
  4. $parent / $children
  5. $ref
  6. provide /inject
  7. $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:

  1. Suitable for use in component libraries/advanced plug-ins, not normal code
  2. 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
  1. In the resolveInject functionresult[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
  2. The initInjections functionDefineReactive functionInner -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
on every button, we need to uniformly rewrap van-button

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

  1. provide / injectUsage 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).
  2. $attrs / $listenersApplication scenario: Encapsulate components twice