认识组件

组件允许我们将 UI 划分为独立的、可重用的部分,并且可以对每个部分进行单独的思考。在实际应用中,组件常常被组织成层层嵌套的树状结构

定义一个组件

当使用构建步骤时,我们一般会将 Vue 组件定义在一个单独的 .vue 文件中,这被叫做单文件组件 (简称 SFC)

<script>
export default {
  data() {
    return {
      count: 0
    }
  }
}
</script>
<template>
  <button @click="count++">You clicked me {{ count }} times.</button>
</template>
<style>
</style>

组件注册

一个 Vue 组件在使用前需要先被“注册”,这样 Vue 才能在渲染模板时找到其对应的实现。组件注册有两种方式:全局注册和局部注册。

3.1 全局注册

我们可以使用 Vue 应用实例app.component() 方法,让组件在当前 Vue 应用中全局可用

定义组件

ComA.vue

<script>
    export default {
        //响应式数据
        data(){
            return {
                count: 0
            }
        },
        // 局部注册组件
        components:{},
        //方法
        methods: {
            plus(){
                this.count++
            }
        },
        //计算属性
        computed: {

        },
        //侦听器
        watch:{
        },
        // 生命周期钩子
        mounted(){}
    }
</script>
<template>
   <div class="com-a">
        <button @click="plus">count:{{ count }}</button>
   </div>
</template>
<style lang="scss" scoped>
</style>
在main.js中全局注册
import { createApp } from 'vue'
//引入组件文件
import ComA from './components/ComA.vue'
import ComB from './components/ComB.vue'
import ComC from './components/ComC.vue'
import './style.css'
import App from './App.vue'
// 修改以下代码,返回应用实例app
// createApp(App).mount('#app')
const app = createApp(App)
//全局注册组件,全局注册的组件可以在此应用的任意组件的模板中使用
app
  .component('ComponentA', ComA)
  .component('ComponentB', ComB)
  .component('ComponentC', ComC)
app.mount('#app')
使用全局组件

在任意组件的模板中直接使用模板,无需引入

<template>
   <div class="app">
      app
      <hr>
      <ComponentA></ComponentA>
      <ComponentB></ComponentB>
      <ComponentC></ComponentC>
   </div>
</template>

3.2 局部注册

全局注册虽然很方便,但有以下几个问题:

  1. 全局注册,但并没有被使用的组件无法在生产打包时被自动移除 (也叫“tree-shaking”)。如果你全局注册了一个组件,即使它并没有被实际使用,它仍然会出现在打包后的 JS 文件中。

  2. 全局注册在大型项目中使项目的依赖关系变得不那么明确。在父组件中使用子组件时,不太容易定位子组件的实现。和使用过多的全局变量一样,这可能会影响应用长期的可维护性。

相比之下,局部注册的组件需要在使用它的父组件中显式导入,并且只能在该父组件中使用。它的优点是使组件之间的依赖关系更加明确,并且对 tree-shaking 更加友好。

  1. 定义组件 ComA.vue

和全局注册时定义组件的方式相同

  1. 局部注册和使用

局部注册需要使用 components 选项

<script>
// 引入组件
import ComponentA from './components/ComA.vue'
export default {
  //局部注册组件
  components: {
    ComponentA
  }
}
</script>
<template>
   <!--使用组件-->
  <ComponentA />
</template>

对于每个 components 对象里的属性,它们的 key 名就是注册的组件名,而值就是相应组件的实现。上面的例子中使用的是 ES2015 的缩写语法,等价于:

export default {
  components: {
    ComponentA: ComponentA
  }
  // ...
}

请注意:局部注册的组件在后代组件中并不可用。在这个例子中,ComponentA 注册后仅在当前组件可用,而在任何的子组件或更深层的子组件中都不可用

3.3 组件名格式

建议使用 PascalCase 作为组件名的注册格式,即大驼峰, 首字母要大写

  1. PascalCase 是合法的 JavaScript 标识符。这使得在 JavaScript 中导入和注册组件都很容易,同时 IDE(编辑器, 比如vscode ) 也能提供较好的自动补全。

  2. <PascalCase /> 在模板中更明显地表明了这是一个 Vue 组件,而不是原生 HTML 元素。同时也能够将 Vue 组件和自定义元素 (web components) 区分开来

以下三种模板中组件的写法都支持

<ComA/>
<ComA></ComA>
<com-a></com-a>

Props

组件通讯的13种方式:https://blog.csdn.net/weixin_49295874/article/details/142375041

父组件通过自定义属性向子组件传值

子组件通过props属性接收父组件的值

4.1 父组件

<script>
import ComA from './components/ComA.vue';
    export default {
    data() {
        return {
          msg: '父组件的msg',
          count: 12
        };
    },
    components: { ComA }
}
</script>
<template>
   <div class="app">
      父组件
      <hr>
      <!-- 父组件通过自定义属性向子组件传值 -->
      <ComA color="red" :count="count" :msg="msg" type="add"/>
   </div>
</template>
<style lang="scss" scoped>
</style>

4.2 子组件

<script>
export default {
    //子组件通过props选项接收父组件的值
    // props 会暴露到 this上,即可以通过this访问到props
    // props 接收的值在父组件被改变后,会触发子组件更新
    // 在子组件中不能直接修改父组件传入的值
    props: ['color', 'count'],
    created() {
        console.log(this.color); // 'red'
    }
}
</script>
<template>
    <div class="com-a">
        <p>count:{{ count}}</p>
    </div>
</template>
<style lang="scss" scoped></style>

没有用props显式声明的父组件中定义的属性,会成为透传attribute

<!-- props接收的属性不再显示在DOM元素上,透传的属性保留 -->
<div class="com-a" msg="父组件的msg" type="add">

4.3 **prop形式

  • 字符串数组形式
export default {
  props: ['color', 'count']
}
  • 对象形式
export default {
  props: {
    title: String,
    likes: Number
  }
}

对于以对象形式声明中的每个属性,key 是 prop 的名称,而值则是该 prop 预期类型的构造函数

对象形式的 props 声明不仅可以一定程度上作为组件的文档,而且如果其他开发者在使用你的组件时传递了错误的类型,也会在浏览器控制台中抛出警告

4.4 传递prop的细节

命名规范

如果一个 prop 的名字很长,应使用 camelCase 形式 (小驼峰)

  • 传值用kebab-case形式 (小写字母加中杠线)
<MyComponent greeting-message="hello" />
  • 接收用camelCase
export default {
  props: {
    greetingMessage: String
  }
}
单向数据流【了解】

所有的 props 都遵循着单向绑定原则,props 因父组件的更新而变化,自然地将新的状态向下流往子组件,而不会逆向传递。这避免了子组件意外修改父组件的状态的情况,不然应用的数据流将很容易变得混乱而难以理解

另外,每次父组件更新后,所有的子组件中的 props 都会被更新到最新值,这意味着你不应该在子组件中去更改一个 prop

导致你想要更改一个 prop 的需求通常来源于以下两种场景

  1. prop 被用于传入初始值;而子组件想在之后将其作为一个局部数据属性。在这种情况下,最好是新定义一个局部数据属性,从 props 上获取初始值即可
export default {
  props: ['initialCounter'],
  data() {
    return {
      // 计数器只是将 this.initialCounter 作为初始值
      // 像下面这样做就使 prop 和后续更新无关了
      counter: this.initialCounter
    }
  }
}
  1. 需要对传入的 prop 值做进一步的转换。在这种情况中,最好是基于该 prop 值定义一个计算属性
export default {
  props: ['size'],
  computed: {
    // 该 prop 变更时计算属性也会自动更新*
    normalizedSize() {
      return this.size.trim().toLowerCase()
    }
  }
}
更改对象 / 数组类型的 props

当对象或数组作为 props 被传入时,虽然子组件无法更改 props 绑定,但仍然可以更改对象或数组内部的值。这是因为 JavaScript 的对象和数组是按引用传递,而对 Vue 来说,禁止这样的改动,虽然可能生效,但有很大的性能损耗,比较得不偿失。

这种更改的主要缺陷是它允许了子组件以某种不明显的方式影响父组件的状态,可能会使数据流在将来变得更难以理解。在最佳实践中,你应该尽可能避免这样的更改,除非父子组件在设计上本来就需要紧密耦合。在大多数场景下,子组件应该抛出一个事件来通知父组件做出改变

Prop****校验

Vue 组件可以更细致地声明对传入的 props 的校验要求

如果传入的值不满足类型要求,Vue 会在浏览器控制台中抛出警告来提醒使用者。这在开发给其他开发者使用的组件时非常有用。

export default {
  props: {
    // 基础类型检查
    //(给出 null 和 undefined值则会跳过任何类型检查)
    propA: Number,
    // 多种可能的类型
    propB: [String, Number],
    // 必传,且为 String 类型
    propC: {
      type: String,
      required: true
    },
    // Number 类型的默认值
    propD: {
      type: Number,
      default: 100
    },
    // 对象类型的默认值
    propE: {
      type: Object,
      // 对象或者数组应当用工厂函数返回。
      // 工厂函数会收到组件所接收的原始 props
      // 作为参数
      default(rawProps) {
        return { message: 'hello' }
      }
    },
    // 自定义类型校验函数
    propF: {
      validator(value) {
        // The value must match one of these strings
        return ['success', 'warning', 'danger'].includes(value)
      }
    },
    // 函数类型的默认值
    propG: {
      type: Function,
      // 不像对象或数组的默认,这不是一个
      // 工厂函数。这会是一个用来作为默认值的函数
      default() {
        return 'Default function'
      }
    }
  }
}

事件

触发与监听事件

在子组件的模板表达式中,可以直接使用 $emit 方法触发自定义事件 (例如:在 v-on 的处理函数中)

<!-- MyComponent -->
<button @click="$emit('someEvent')">click me</button>

$emit() 方法在组件实例上也同样以 this.$emit() 的形式可用:

export default {
  methods: {
    submit() {
      this.$emit('someEvent')
    }
  }
}

父组件可以通过 v-on (缩写为 @) 来监听事件:

<MyComponent @some-event="callback" />

同样,组件的事件监听器也支持 .once 修饰符:

<MyComponent @some-event.once="callback" />

像组件与 prop 一样,事件的名字也提供了自动的格式转换。注意这里我们触发了一个以 camelCase 形式命名的事件,但在父组件中可以使用 kebab-case 形式来监听

和原生 DOM 事件不一样,组件触发的事件没有冒泡机制。你只能监听直接子组件触发的事件。平级组件或是跨越多层嵌套的组件间通信,应使用一个外部的事件总线,或是使用一个全局状态管理方案

事件参数

子组件

<button @click="$emit('increaseBy', 1)">
  Increase by 1
</button>

父组件

<MyButton @increase-by="(n) => count += n" />

或者,也可以用一个组件方法来作为事件处理函数

<MyButton @increase-by="increaseCount" />
methods: {
  increaseCount(n) {
    this.count += n
  }
}

声明触发的事件

组件可以通过 [emits](https://cn.vuejs.org/api/options-state.html#emits) 选项来声明它要触发的事件

尽管事件声明是可选的,我们还是推荐你完整地声明所有要触发的事件,以此在代码中作为文档记录组件的用法。同时,事件声明能让 Vue 更好地将事件和透传 attribute 作出区分,从而避免一些由第三方代码触发的自定义 DOM 事件所导致的边界情况

export default {
  emits: ['inFocus', 'submit']
}

安装vue Devtools (chrome)

Vue Devtools 是一个功能强大的浏览器插件工具,它可以极大提高我们调试与分析 Vue 应用的效率

  • 极简插件网址: https://chrome.zzzmh.cn/#/index

  • 搜 “Vue Devtools” ,vue3对应的版本为6.5.0, 下载

  • 解压

  • 在chrome中打开"扩展程序", 把解压后的crx格式的文件拖入

  • 重启chrome, 运行vue项目,在开发者工具中打开 "vue"选项

  • 查看组件树、数据与事件调试

ref传參

父组件:

<script setup>
import { ref } from "vue";
import Child from "./components/Child.vue";
// 1. 创建ref数据,默认为空
let childRef = ref(null);
const getChild = () => {
  // 4. 父组件通过 ref 获取子组件中暴漏的数据
  console.log(childRef.value.getMessage());
};
</script>

<template>
  <div class="parent">
    <span> 父组件 -- {{ childRef }}</span>
    <button @click="getChild">点击</button>
    <!--2. 给子组件绑定一个ref数据-->
    <Child ref="childRef"></Child>
  </div>
</template>

子组件:

<script setup>
import { ref } from "vue";
let message = ref("hello");
// 3. 使用defineExpose 用来显式暴露组件内部的属性或方法,使得父组件可以通过 ref 访问子组件的暴露内容。
defineExpose({
  getMessage() {
    return message.value;
  },
});
</script>

<template>
  <div class="child">子组件--{{ message }}</div>
</template>
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐