导航


HTML

CSS

JavaScript

浏览器 & 网络

版本管理

框架

构建工具

TypeScript

性能优化

软实力

算法

UI、组件库

Node

业务技能

针对性攻坚

AI


Untitled

常规父传子,父监听子组件方案

App.vue

<template>
  <div>
    <my-selector
      :value="selectorValue"
      @changeOption="changeOption"
    ></my-selector>
  </div>
</template>

<script>
import MySelector from './components/MySelector.vue';
export default {
  name: 'App',
  components: {
    MySelector
  },
  data() {
    return {
      selectorValue: '3'
    }
  },
  methods: {
    changeOption(val) {
      console.log(val);
    }
  }
}
</script>

MySelector.vue

<template>
  <select
    v-model="selectValue"
    @change="changeOption"
  >
    <option value="1">选项1</option>
    <option value="2">选项2</option>
    <option value="3">选项3</option>
  </select>
</template>

<script>
export default {
  name: 'MySelector',
  props: {
    value: {
      type: String,
      default: '1'
    }
  },
  data() {
    return {
      selectValue: this.value
    }
  },
  methods: {
    changeOption() {
      this.$emit('changeOption', this.selectValue);
    }
  }
}
</script>

attributes 方案

单个根元素的组件,使用组件传值的所有属性,都会增加到子组件的根元素上

我们删除父组件的自定义事件绑定以及子组件的 emit 监听,只传值:

App.vue

<template>
  <div>
    <my-selector
      :value="selectorValue"
      model="123"
      id="mySelector"
      class="my-selector"
    ></my-selector>
  </div>
</template>

<script>
import MySelector from './components/MySelector.vue';
export default {
  name: 'App',
  components: {
    MySelector
  },
  data() {
    return {
      selectorValue: '3'
    }
  },
}
</script>

MySelector.vue

<template>
  <!-- <select
    v-model="selectValue"
    @change="changeOption"
  > -->
  <select>
    <option value="1">选项1</option>
    <option value="2">选项2</option>
    <option value="3">选项3</option>
  </select>
</template>

<script>
export default {
  name: 'MySelector',
  // props: {
  //   value: {
  //     type: String,
  //     default: '1'
  //   }
  // },
  // data() {
  //   return {
  //     selectValue: this.value
  //   }
  // },
}
</script>

我们此刻会发现,页面上默认vue就帮我们把父传子的值传过来了,而且给组件 <my-selector> 绑定的 model 属性,也自动传给了组件内部的 select 标签:

Untitled

我们可以验证下标题的结论:

MySelector.vue

<template>
  <select>
    <option value="1">选项1</option>
    <option value="2">选项2</option>
    <option value="3">选项3</option>
  </select>
</template>

<script>
export default {
  name: 'MySelector',
  created() {
    console.log(this.$attrs);
  }
}
</script>

Untitled

现在父组件给子组件绑定事件、而子组件不 $emit

App.vue

<template>
  <div>
    <my-selector
      :value="selectorValue"
      model="123"
      id="mySelector"
      class="my-selector"
      @change="changeOption"
    ></my-selector>
  </div>
</template>

<script>
import MySelector from './components/MySelector.vue';
export default {
  name: 'App',
  components: {
    MySelector
  },
  data() {
    return {
      selectorValue: '3'
    }
  },
  methods: {
    changeOption(e) {
      console.log(e.target.value); // 子组件不监听 @change 事件,此处也能监听到
    }
  }
}
</script>

Untitled

禁用子组件的根组件的继承

MySelector.vue

<template>
  <select>
    <option value="1">选项1</option>
    <option value="2">选项2</option>
    <option value="3">选项3</option>
  </select>
</template>

<script>
export default {
  name: 'MySelector',
  inheritAttrs: false, // 禁用根组件 select 继承父组件传递过来的属性
  created() {
    console.log(this.$attrs);
  }
}
</script>

这样标签就不会继承属性了,切换selector也没法触发事件:

Untitled

但是 this.$attrs 依然有值:

Untitled

如果我们此时手动给 select 标签绑定 $attrs (平铺属性),就又能继承了(事件也能生效):

<template>
  <select v-bind="$attrs">
    <option value="1">选项1</option>
    <option value="2">选项2</option>
    <option value="3">选项3</option>
  </select>
</template>

...

Untitled

什么情况下需要禁止继承(input 的 autofocus)

想要实现让子组件 input 聚焦:

LoginBox.vue

<template>
  <div>
    <input type="text" placeholder="Username">
    <input type="password" placeholder="Password">
    <button>Login</button>
  </div>
</template>

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

App.vue

<template>
  <div>
    <my-selector
      :value="selectorValue"
      model="123"
      id="mySelector"
      class="my-selector"
      @change="changeOption"
    ></my-selector>

    <login-box autofocus></login-box>
  </div>
</template>

<script>
import MySelector from './components/MySelector.vue';
import LoginBox from './components/LoginBox.vue';
export default {
  name: 'App',
  components: {
    MySelector,
    LoginBox
  },
  ...
}
</script>

我们会发现 input 并没有聚焦,而是把 autofocus 属性设置给了组件的根元素 div:

Untitled

此时我们就需要禁止默认继承来达到目的:

<template>
  <div>
    <input type="text" :autofocus="$attrs.autofocus" placeholder="Username">
    <!-- 或者 -->
    <input type="text" v-bind="$attrs" placeholder="Username">
    <input type="password" placeholder="Password">
    <button>Login</button>
  </div>
</template>

<script>
export default {
  inheritAttrs: false, // 其实不写这个,上边的 :autofocus="$attrs.autofocus" 也能生效
  name: 'LoginBox'
}
</script>

Untitled

多个根组件选择性传递 $attrs

App.vue

<template>
  <div>
    <my-selector
      :value="selectorValue"
      model="123"
      id="mySelector"
      class="my-selector"
      @change="changeOption"
    ></my-selector>

    <login-box autofocus></login-box>

    <my-list-container :listTitle="listTitle">
      <my-list :myList="myList" data-show="123" @click="bookSubscribe"></my-list>
    </my-list-container>
  </div>
</template>

<script>
import MySelector from './components/MySelector.vue';
import LoginBox from './components/LoginBox.vue';

import MyList from './components/MyList.vue';
import MyListContainer from './components/MyListContainer.vue';
export default {
  name: 'App',
  components: {
    MySelector,
    LoginBox,
    MyList,
    MyListContainer
  },
  data() {
    return {
      selectorValue: '3',
      listTitle: '小说订阅列表',
      myList: [
        {
          id: 1,
          title: '悲惨世界',
          isSubscribable: true,
          subscribed: false
        },
        {
          id: 2,
          title: '傲慢与偏见',
          isSubscribable: false,
          subscribed: false
        },
        {
          id: 3,
          title: '呼啸山庄',
          isSubscribable: true,
          subscribed: false
        }
      ]
    }
  },
  methods: {
    changeOption(e) {
      console.log(e.target.value);
    },
    bookSubscribe(e) {
      const id = e.target.dataset.id;
      this.myList = this.myList.map(item => {
        if (item.id == id) {
          item.subscribed = !item.subscribed;
        }
        return item;
      });
    }
  }
}
</script>

<style>

</style>

MyList.vue

<template>
  <!-- 多个根组件,n个li元素 -->
  <template
    v-for="item of myList">
    <li
      v-if="item.isSubscribable"
      :key="item.id"
      v-bind="$attrs"
    >
      <span>{{ item.title }}</span>
      <button :data-id="item.id">{{ item.subscribed ? '已订阅' : '订阅' }}</button>
    </li>
    <li v-else :key="item.id">
      <span>{{ item.title }}</span>
      <button disabled>不可订阅</button>
    </li>
  </template>
</template>

<script>
export default {
  name: 'MyList',
  props: {
    myList: Array
  },
  created() {
    console.log(this.$attrs);
  }
}
</script>

二次封装组件

基础组件透传(保持所有原生能力)

<!-- 封装增强型 el-input 组件 -->
<template>
  <div class="custom-wrapper">
    <el-input 
      v-bind="$attrs"  <!-- 透传所有属性 -->
      v-on="$listeners" <!-- 透传所有事件 -->
    >
      <!-- 保留原有插槽 -->
      <template v-for="(_, slotName) in $slots" :slot="slotName">
        <slot :name="slotName"/>
      </template>
    </el-input>
    <div class="extra-info">输入字符数:{{ value?.length || 0 }}</div>
  </div>
</template>

<script>
export default {
  // 显式声明需要特殊处理的 prop
  props: ['value']
}
</script>

使用效果