前言

vue中组件间数据传递的方式大致有以下几类

  1. props / $emit
  2. vuex
  3. event bus
  4. $parent / $children
  5. $ref
  6. provide /inject
  7. $attrs / $listeners
    自己平时用的最多的就是props /$emitvuex,本次来探究一下provide /inject$attrs / $listeners的使用场景

provide / inject

使用场景(多级组件间传值,以element-ui设计为例)

element-ui中的checkbox的有以下的使用场景

 <el-form ref="form" :model="form" size="small">                
  <el-form-item label="活动性质">                             
    <el-checkbox-group v-model="ruleForm.type">                     
      <el-checkbox label="美食/餐厅线上活动" name="type"></el-checkbox>   
      <el-checkbox label="地推活动" name="type"></el-checkbox>
      <el-checkbox label="线下主题活动" name="type"></el-checkbox>
      <el-checkbox label="单纯品牌曝光" name="type"></el-checkbox>
    </el-checkbox-group>
  </el-form-item>
</el-form>

最外层的el-form 组件有size属性,用于控制组件大小,而el-checkbox也存在size属性,一般如果el-checkbox没有设置size的时候,el-checkbox会尝试向上查找祖辈元素有无size,有的话就会直接使用。
那么假如你来封装这个组件库,你会怎么让el-checkbox获取el-form的size呢?

自己实现(props / $emit)

一般开发当中,组件间传值的时候我会选择props /$emit,那么很容易得到以下的实现方式:

// app.vue
<template>
    <el-form size="medium"></el-form>
</template>
// el-form.vue
<template>
    <el-form-item :size="size"></el-form-item>
</template>
<script>
...
props: {
    size: {
        type: String,
        require: false,
    }
}
...
</script>
// el-checkbox-group.vue
<template>
    <el-checkbox :size="size"></el-checkbox>
</template>
<script>
...
props: {
    size: {
        type: String,
        require: false,
    }
}
...
</script>
// 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>

如上所示,这种实现存在的问题就是 size属性需要一层一层往下传递,比较繁琐,那么element-ui是怎么实现的呢

element-ui 实现

// el-form.vue
<script>
...
provide() {
  return {
    elForm: this
  };
...
</script>
    },
// el-form-item
<script>
...
provide() {
  return {
    elFormItem: this
  };
},

inject: ['elForm'],

computed: {
    _formSize() {
        return this.elForm.size;
      },
      elFormItemSize() {
        return this.size || this._formSize;
      },
}
...
</script>
// 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>

如上所示,element-ui 实际上会比较复杂,考虑的比较多,但size属性的大概传递流程就是通过provide暴露,在el-checkbox里inject接收

自己实现(provide / inject)

// app.vue
<template>
    <el-form size="medium"></el-form>
</template>
// 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>
// 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>

这样的话就不用经过中间层的多次传递了,比较简单

看下vue官网的介绍

image.png

其实主要是两点信息:

  1. 适合在组件库/高阶插件使用,不适合普通代码
  2. 绑定的值并不是可相应的,但如果你传的是一个可监听的对象, 那么 对象的property还是可以响应的

这里的第二句话比较饶,这里有个demo,大家可以猜猜点击 outter-click 和inner-click 视图中的A、B会变吗?

image.png

// 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>
// 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>

provide / inject 源码

要想解释以上现象,其实可以看下源码

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
  }
}

  1. resolveInject函数中的result[key] = source._provided[provideKey] 这里可以看出result是浅复制provide传来的值,这里就能解释outter-click的现象,B是引用类型,所以视图变了,而A是基本类型,所以视图没变
  2. initInjections函数中的defineReactive 函数是vue中进行双向绑定的操作,所以这里给inject对象里的每个属性做了双向绑定,这就解释了inner-click的现象,A、B都变了

$attrs / $listeners

场景(二次封装组件)

通常在项目开发中我们都会遇到一些这样的场景:视觉 给我们的设计稿中的一些组件样式和我们使用的组件库中的样式不太一致,需要对该组件进行二次封装。
假如我们使用vant进行开发,视觉提供给我们的组件中的按钮边框都是圆的,,van-buttonround属性是可以满足需求的,为了不在每一个按钮处都这么写<van-button round text="点我"></van-button>, 我们需要统一对van-button进行二次封装

自己实现(props / $emit)

// app.vue
<template>
    <my-button :text="button.btnTxt" @btnClick="handleBtnClick" round></my-button>
</template>

// 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>

这里的问题click事件 round属性都是vant-button已经提供的能力,我们这里都要传递一遍,换一句话说,如果我们不处理,那么我们就无法使用。假如所有的属性我们都要用,那么我们就要把所有的属性都传递一遍,这大大增大了我们的工作量。那么我们能不能直接使用van-button提供的能力呢?

vue官网介绍 $attrs / $listeners

image.png

image.png

自己实现($attrs / $listeners)

<template>
    <my-button :text="button.btnTxt" @Click="handleBtnClick" round></my-button>
</template>
<template>
  <van-button v-bind="$attrs" v-on="$listeners"></van-button>
</template>

<script>
export default {
  name: 'MyButton'
}
</script>

总结

  1. provide / inject使用场景: 出现多级组件间传值的时候可以考虑使用,但一般不鼓励直接在自组件改变祖辈组件里的值(这样会使vue中的数据流非常乱)
  2. $attrs / $listeners使用场景: 二次封装组件
Logo

前往低代码交流专区

更多推荐