【Vue核心篇Ⅲ】组件定义、组件注册、组件通信、自定义指令、插槽

315 阅读7分钟

组件

组件化:将页面拆分成多个组件,每个组件有自己独立的 结构样式行为

组件化的好处:提高开发效率,便于代码的维护和复用

单文件组件

一个 xxx.vue 文件就是一个 组件,也叫 单文件组件。比如 HelloWorld.vue 就是一个单文件组件

image-20231006154447462.png

<template>
    <!-- HTML结构 -->
    <!-- 里面只能有一个根元素(Vue2) -->
    <div class="box"></div>
</template>

<script>
// JavaScript行为
// 通常需要导出一个Vue配置对象
export default {
	// data、methods、computed、watch...
}
</script>

<style scoped>
/* CSS样式 */
/* 默认只支持css语法,使用less语法的两个前提: */
/* 1.安装 less、less-loader 依赖包 */
/* 2.给style标签添加 lang="less" 属性 */
</style>

组件注册

组件注册:使用特定语法注册组件,然后将 组件对象 作为 html自定义标签 使用

为什么组件要先注册再使用?

为了明确组件之间的依赖关系,方便进行组件的复用和组件间的数据传递

局部注册

局部注册的组件只能在 当前组件 中使用。语法:

// App.vue
// 局部注册(只在当前组件中使用)
import 组件对象 from '组件文件路径'
export default {
    components: { 
        组件名: 组件对象
        // ... ...
    }
}

全局注册

全局注册的组件在 所有组件 中都能使用,这类组件一般是通用的,如按钮组件、提示框组件等。语法:

// main.js
// 全局注册(通用组件,可以在任意组件中使用)
import 组件对象 from '组件文件路径'
Vue.component("组件名", 组件对象);
// ... ...

eg.组件的拆分与注册

<!-- App.vue -->
<template>
	<!-- 在组件内部使用局部注册的组件 -->
    <div class="box">
        <MyHeader/>
        <MyMain/>
        <MyFooter/>
    </div>
</template>

<script>
import MyHeader from './components/MyHeader.vue'
import MyMain from './components/MyMain.vue'
import MyFooter from './components/MyFooter.vue'
export default {
    // 局部注册
    components: {
        MyHeader,
        MyMain,
        MyFooter,
    }
}
</script>
// main.js
import MyButton from "./components/MyButton.vue";
// 全局注册
Vue.component("MyButton", MyButton);
<!-- MyHeader.vue -->
<template>
    <div class="header">
        <span>MyHeader</span>
        <!-- 使用全局注册的组件 -->
        <MyButton/>
    </div>
</template>

image-20231007151607088.png

组件通信

image-20231006155601038.png

父子关系是相对的,一个组件既可以作为父组件,也可以是子组件

非直接嵌套的两个组件不构成父子关系!

父子组件通信

image-20231006165806422.png

image-20231006165823001.png

# 父传子
- 自定义属性配合props接收
- 可以传递任意数量
- 可以传递任意类型
- 不能直接修改,数据流是单向的

# 子传父
- 自定义事件配合$emit触发
- 主要是为了间接修改父组件中的数据
- 需要一个触发时机

进行组件通信时,一般将公共数据维护在父组件中,需要时再传递给子组件;子组件不能直接修改父组件传递的数据,只能通过 $emit 触发父组件中的事件处理函数,通知父组件修改。这就是 Vue单向数据流 的体现

props类型检查

子组件可以对接收的数据进行 类型检查。当父组件传递的数据不符合要求时,会在浏览器控制台中输出错误信息,帮助开发人员纠错

// 使用props的对象形式进行类型检查
props: {
    nums: {
      // 类型检查
      type: Number,
      // 是否必传
      required: true,
      // 没有传递数据时的默认值
      default: 10,
      // 自定义校验规则,返回true则校验通过
      // value 接收到的数据
      validator(value) {
        if (value >= 0 && value <= 100) return true
      }
    }
}

非父子组件通信

provide&inject依赖注入

适用于 跨层级组件间 的通信

image-20231009205600287.png

// 父组件provide提供数据
export default {
    provide () {
        return {
            // 普通类型(非响应式)
            color: this.color,
            // 复杂类型(响应式)
            userInfo: this.userInfo
        }
    }
}
// 子孙组件inject注入取值使用
// 不限制嵌套层级
export default {
    inject: ['color', 'userInfo'],
    created() {
        console.log(this.color, this.userInfo)
    }
}
eventBus事件总线

事件总线 的本质就是一个空的 Vue 实例,这个实例拥有 $on()$emit() 方法,用来在不同组件间传递数据

image-20231008112035571.png

image-20231008112016319.png

组件深入

组件中的data函数

# 为什么组件中的 data() 必须是一个函数?
为了保证每个组件实例,都维护一份独立的数据,不要相互影响
每复用一次组件,都会重新执行一次data()函数,得到一个新的对象

scoped原理

# scoped解决样式冲突的原理(默认组件中的样式全局生效)
给当前组件内的所有标签都添加一个 `data-v-hash值` 的自定义属性
给 style 中每个CSS选择器添加 `[data-v-hash值]` 的属性选择器
最终效果: 样式只作用于当前组件

image-20231007175529735.png

v-model原理

image-20231008144454229.png

v-model / .sync 双向绑定语法

image-20231009101444039.png

image-20231009104006866.png

组件上的事件都是自定义事件,因为组件本身就是一个自定义的标签

拓展:v-model实现原理

ref&$refs

image-20231008151413438.png

<template>
  <div id="app">
    <BaseForm ref="baseForm"></BaseForm><br>
    <button @click="resetForm">重置数据</button>
  </div>
</template>

<script>
import BaseForm from './components/BaseForm.vue'
export default {
  components: {
    BaseForm
  },
  methods: {
    resetForm() {
      // $refs 获取子组件的组件实例
      // reset() 子组件中的方法,清空表单数据
      this.$refs.baseForm.reset()
    }
  }
}
</script>

$refs的特点

# 用来获取指定的DOM元素或组件实例
- 只在当前组件中查找
- 可以在父组件中访问子组件实例上的属性或方法

注意点

- this:获取当前组件的组件实例
- this.$refs.组件ref属性值:获取子组件的组件实例,以访问子组件中的数据和方法

$nextTick&DOM异步更新

为了提高渲染性能,Vue 中的 DOM 更新是 异步 的。当响应式数据改变影响DOM元素的显示与隐藏时,立即操作DOM元素 可能会有问题(获取不到)

使用 $nextTick 函数可以解决这个问题,将函数中的 DOM 操作推迟到 DOM 更新完成之后自动执行

image-20231008154539127.png

handleEdit() {
    // 控制输入框的显示
    this.isShowEdit = true
    
    // 错误:由于Vue操作DOM是异步的,所以此时还获取不到输入框
    // this.$refs.inp.focus()
    // console.log(this.$refs.inp) //undefined
    
    // 正确:在DOM更新完毕后,执行其中的代码
    this.$nextTick(()=>{
        // 让输入框自动聚焦
        this.$refs.inp.focus()
    })
}

image-20231009083259793.png

自定义指令

Vue 允许开发者自定义指令,封装一些 DOM 操作,简化开发

注册自定义指令

自定义指令 需要先注册再使用。注册方式有两种:

  • 局部注册:只能在当前组件中使用。位置 xxx.vue
directives:{ 
    指令名,指令配置对象 
    // ... ...
}

image-20231008160712316.png

  • 全局注册:在所有组件中都能使用。位置 main.js
Vue.directive(指令名,指令配置对象)
// ... ...

image-20231008160646960.png

eg.利用自定义指令修改文字颜色

<div>
     <!-- 使用自定义指令 --> 
    <h2 v-color="colorName">Hello Vue!</h2>
    <!-- 修改颜色 -->
    <button @click="colorName = colorName === 'red' ? 'blue' : 'red'">改变颜色</button>
  </div>
export default {
  data() {
    return {
      // 文字颜色
      colorName: 'red'
    }
  },
  // 局部注册-自定义指令
  directives: {
    color: {
      // 修改文字颜色 
      // el就是自定义指令绑定的那个DOM元素
      // binding.value可以获取指令传递的值
        
      // inserted 函数仅在DOM首次渲染时执行一次
      inserted(el,binding) {
        el.style.color = binding.value
      },
      // update 函数每次指令值改变时都会执行
      update(el,binding) {
        el.style.color = binding.value
      }
    }
  }
}

zhiling-color.gif

自定义指令和内置指令的用法基本一致,在标签上书写 v-指令名 即可

拓展:自定义指令的详细介绍

插槽

使用 插槽 进行占位,可以动态设置组件的 UI 结构。在组件复用时,子组件中的部分内容不是固定的

默认插槽

默认插槽:子组件中只有一个插槽,并且没有定义名称,这个插槽就是默认插槽

image-20231008170920214.png

<!-- 封装插槽(子组件) -->
<template>
	<slot>后备内容(默认模版结构)</slot>
</template>
<!-- 使用插槽(父组件) -->
<child-component>
    <!-- 传递给插槽的模版结构 -->
    <!-- 子组件标签中间的所有内容都会添加到默认插槽的位置 -->
</child-component>

实际上,默认插槽有一个默认的名字 default,可以使用下面具名插槽的语法

具名插槽

具名插槽:在子组件中的不同位置可以定义多个插槽,多个插槽使用 name 属性区分(有名字的插槽)

image-20231008212721270.png

<!-- 封装插槽(子组件) -->
<template>
	<slot name='插槽1名称'></slot>
	<slot name='插槽2名称'></slot>
</template>
<!-- 使用插槽(父组件) -->
<!-- #插槽名称 是 v-slot:插槽名称 的简写形式-->
<child-component>
	<template #插槽1名称>
        <!-- 传递给插槽1的模版结构 -->
    </template>
    <template #插槽2名称>
        <!-- 传递给插槽2的模版结构 -->
    </template>
</child-component>

eg.具名插槽示例

slot-name.png

image-20231008220403113.png

作用域插槽

作用域插槽:用来传递子组件内部的数据

image-20231008181905883.png

<!-- 封装插槽(子组件) -->
<template>
	<slot name='插槽名称' :属性名1='数据1' :属性名2='数据2'></slot>
</template>
<!-- 使用插槽(父组件) -->
<child-component>
	<template #插槽名称='接收的数据|对象'>
        <!-- 传递给插槽的模版结构 -->
        <!-- 可以在模版中使用子组件传递的数据 -->
    </template>
</child-component>

作用域插槽会把所有要传递的数据封装为一个对象返回,可以直接在模版中进行解构

eg.作用域插槽示例

slot-scope.png

image-20231008215315821.png

以上就是本篇的全部内容,如果你愿意,可以在这里复习一下:Vue工程化开发&组件进阶&插槽