目录
- <<回到导览
- 组件
-
- 1.项目
- 2.组件
- 3.组件通信
-
- 3.1.父子通信
-
- 3.1.1.父传子(props)
- [3.1.2.子传父(emit)](#3.1.2.子传父(emit))
- 3.2.非父子通信
- 3.3.v-model详解
- 3.4.sync修饰符(已废弃)
- [3.5.(重点)ref和refs](#3.5.(重点)ref和refs)
- [3.6.ref和refs选择器](#3.6.ref和refs选择器)
- [3.7.nextTick](#3.7.nextTick)
- 4.自定义指令
- 5.插槽
- 6.my-tag组件封装
<<回到导览
组件
1.项目
1.1.Vue Cli
官方提供的一个全局命令工具
,可以快速生成vue项目的标准化基础架子(集成webpack),开箱即用
使用步骤:
-
全局安装(安装一次即可):
bashyarn global add @vue/cli // 或者 npm i @vue/cli -g
-
查看vue/cli版本:
bashvue --version
-
创建项目架子:
bashvue create project-name
-
启动项目:
bashyarn serve // 或者 npm run serve
启动项目的命令并不固定,其取决于package.json文件
1.2.项目目录

-
第三包文件夹(依赖)被删除,只要package.json还存在,就可以安装回来
npm i --force
或者npm i --legacy-peer-deps
1.3.运行流程

1.4.组件的组成
- 语法高亮插件

-
三部分构成
- template :结构 (有且只能一个根元素),在template中,最外层标签只能为div盒子
- script: js逻辑
- style: 样式 (可支持less,需要装包)
-
让组件支持less
-
style标签,lang="less" 开启less功能
-
装包:
bashyarn add less less-loader -D // 或者 npm i less less-loader -D
-
1.5.注意事项
-
在组件的template中,最外面必须有一个div盒子
-
组件名称最好采用大驼峰命名法,且至少两个驼峰
-
有时候有些错误改正后依旧报错(列如上面第二个报错),重新启动项目即可
-
启动项目时在项目的根目录下启动(我们项目根目录叫luyou)
2.组件
2.1.组件注册
-
局部注册
-
在components文件夹创建组件 xxx.vue
-
在根组件App.vue导入
js// 导入组件 // import xxx from 'xxx.vue的文件路径' import HmHeader from './components/HmHeader' export default{ // 局部注册 components:{ // '组件名':组件对象, // HmHeader:HmHeader 同名可简写 HmHeader } }
-
-
全局注册
-
在components文件夹创建组件 xxx.vue
-
在main.js进行全局注册
js// 导入组件 // import xxx from 'xxx.vue的文件路径' import HmHeader from './components/HmHeader' // 调用Vue.component进行全局注册 // Vue.component('组件名',组件对象) Vue.component('HmHeader',HmHeader)
一般都用局部注册,如果发现是通用组件,再注册为全局
-
2.2.scoped样式冲突
-
默认组件中的样式会全局生效
-
可以给组件加上scoped属性,可以让样式只作用于当前组件
html<style scoped></style>
scoped的原理:
- 组件被都添加 data-v-hash (添加自定义属性)
- css选择器添加 [data-v-hash] (添加自定义属性的属性选择器)
2.3.data是一个函数
在最开始的vue基本语法学习中,data被写为data: {},
键值(值为对象,对象又嵌套键值数据)
但在组件开发中,data选项必须是一个函数
js
data(){
return {
// 数据
}
}
-
这是因为组件具有复用性,在同一个组件的复用过程中,data里的数据应该不一样,且数据不相互影响
-
将data设置为一个函数,将数据写进return返回值中,每次创建组件都会调用data函数,返回一个独立数据对象,这些独立的对象分别用于保存同一组件的不同次使用的数据。
2.4.props详解
-
定义 :在组件使用时,注册的一些
自定义属性
-
作用:向子组件传递数据
-
示例:
jsexport default { props: ['w'], }
在为子组件传递数据时,我们应该为组件的 prop 指定验证要求
jsprops: { 校验的属性名: { type: 类型, // Number String Boolean ... required: true, // 是否必填 default: 默认值, // 默认值 validator (value) { // 自定义校验逻辑 return 是否通过校验 } } },
- default和required一般不同时写
- default后面如果是复杂类型,则需要以函数的形式return一个默认值
示例:
jsprops: { w: { type: Number, //required: true, default: 0, validator(val) { if (val >= 100 || val <= 0) { console.error('传入的范围必须是0-100之间') return false } else { return true } }, }, },
2.5.data和prop的区别
- data 的数据是自己的 → 随便改
- prop 的数据是外部 的 → 不能直接改,要遵循 单向数据流
单向数据流
:父级props 的数据更新,会向下流动,影响子组件。这个数据流动是单向的
3.组件通信
上面讲到,data是一个函数,同一组件的不同的使用之间数据独立,同样,不同组件之间数据也不共享
3.1.父子通信

3.1.1.父传子(props)
-
父组件通过 props 将数据传递给子组件
-
在父组件中的子组件标签添加动态属性(App.vue)
html<MySon :title="say"></MySon>
-
子组件通过props进行接收
jsexport default { data() { return {}; }, props: ["title"], };
-
3.1.2.子传父($emit)
-
子组件利用 $emit 通知父组件修改更新
-
给父组件元素添加事件和处理方法
html<button @click="changeFn">say</button>
jschangeFn() { // change为后面为子组件添加的事件名 // "Hello"为要修改的值,也就是后面的形参newSay // 并不一定要传字符串,也可以将"Hello"改为变量传递 this.$emit("change", "Hello"); },
-
触发事件后,利用$emit将父组件发送改变数据的通知
-
在父组件中的子组件标签添加事件,调用修改属性的方法
html<MySon :title="say" @change="changeFn"></MySon>
js// 形参newSay为传递过来的值"Hello" changeFn(newSay) { this.say = newSay; },
-
3.2.非父子通信
3.2.1.事件总线
概念:创建创建一个都能访问的事件总线,发送方向事件总线发送,
接收方向事件总线发送监听
-
在src文件夹下创建一个utils文件夹,在该文件夹下创建一个js文件
-
创建一个空Vue实例(事件总线)
jsimport Vue from 'vue' const Bus = new Vue() export default Bus
-
接受方和发送方都将数据总线引入
jsimport Bus from '../utils/EventBus'
-
A组件(发送方),触发Bus的$emit事件
js// 'sendMsg为发送的数据的标识 Bus.$emit('sendMsg', '这是一个消息')
-
B组件(接受方),监听Bus的 $on事件
jscreated () { // 形参msg为传递的信息('这是一个消息') Bus.$on('sendMsg', (msg) => { this.msg = msg }) }
3.2.2.provide-inject
作用 :跨层级共享数据
-
父组件 provide提供数据
jsexport default { provide () { return { // 普通类型【非响应式】 color: this.color, // 复杂类型【响应式】 userInfo: this.userInfo, } } }
-
.子/孙组件 inject获取数据
jsexport default { inject: ['color','userInfo'], created () { console.log(this.color, this.userInfo) } }
- provide提供的简单类型的数据不是响应式的,复杂类型数据是响应式。(
推荐提供复杂类型数据
) - 子/孙组件通过inject获取的数据,不能在自身组件内修改
3.3.v-model详解
-
v-model本质上是一个语法糖。例如应用在输入框上,就是value属性 和 input事件 的合写
html<template> <div id="app" > <input v-model="msg" type="text"> <!-- 等价于 --> <!-- 数据变,视图跟着变 :value --> <!-- 视图变,数据跟着变 @input --> <input :value="msg" @input="msg = $event.target.value" type="text"> </div> </template>
-
$event 用于在模板中,获取事件的形参
-
$event.target.value即获取输入框的值
-
将输入框的值赋值给data里的数据msg,再将msg通过
:value
赋值给输入框,从而实现数据的双向绑定。
注意:
-
不同的表单元素, v-model在底层的处理机制是不一样的。
比如给checkbox使用v-model,底层处理的是
:checked
属性和@change
事件。 -
v-model不能直接绑定父组件传过来的数据,因为父组件通过prop属性传过来的数据不能直接修改。
3.4.sync修饰符(已废弃)
-
在上面的父子通信中,子组件不能直接修改父组件数据,而是通过$emit间接修改
-
通过sync修饰符,子组件可以修改父组件传过来的props值
-
以上面的父子通信为例
父组件:
html<!-- 完整写法 --> <MySon :title="say" @update:title="isShow = $event" ></MySon> <!-- 简写 --> <MySon :title.sync="say"></MySon>
子组件:
jsprops: { title: Boolean }, this.$emit('update:title', 'Hello')
3.5.(重点)ref和$refs
-
利用ref 和 $refs 可以用于 获取
dom 元素
或组件实例
(要在dom渲染完后才能获取) -
获取元素还可以通过
document.querySeletctor()
获取,但是此方法是从整个页面开始获取的- 类名问题:比如我们在子组件中获取类名为app的元素,如果在此组件之前还有类名为app的元素,则会获取之前的类名为app的元素
- 解决方法 :如果我们利用ref 和 $refs 可以用于 获取
dom 元素
,则会从当前组件开始获取,避免类名问题。
-
示例:
-
给要获取的盒子添加ref属性
html<div ref="demo">我是渲染图表的容器</div>
-
通过 this.$refs.demo 获取
jsmounted(){ console.log(this.$refs.demo) }
-
3.6.ref和$refs选择器
-
利用ref 和 $refs 还可以用于
组件实例
-
-
在组件示例上,添加ref属性
html<MySon ref="demo" ></MySon>
-
通过 this.$refs.demo 获取,控制台会打印组件对象
jsmounted(){ console.log(this.$refs.demo) // 控制台 // VueComponent {_uid: 2, _isVue: true, __v_skip: true, _scope: EffectScope, $options: {...}, ...} }
-
我们可以通过
this.$refs.demo.子组件方法名
调用子组件方法,或者访问子组件属性,实现组件通信。注意:这种组件通信只是调用了子组件方法或者访问子组件属性,没有实现
双向绑定
。
-
3.7.$nextTick
-
$nextTick用于实现vue的异步更新
-
应用案例:编辑标题, 编辑框自动聚焦
-
-
效果:点击编辑,显示编辑框,并且让编辑框,立刻获取焦点
-
预想代码
jsthis.isShowEdit = true // 显示输入框 this.$ref.input.focus() // 获取焦点
-
预想代码并不能实现该效果,因为:
- 当执行显示输入框代码后,没等dom渲染出显示框,就执行到获取焦点的代码了(因为dom是
异步更新
),我们可以用$nextTick用于实现vue的异步更新。 - vue异步更新的原因是提高性能,当点击编辑按钮的@click绑定的事件执行完毕后,才会更新视图,这样避免每执行完一行代码就更新一次视图。
- 当执行显示输入框代码后,没等dom渲染出显示框,就执行到获取焦点的代码了(因为dom是
-
利用$nextTick实现以上代码的异步更新
jsthis.isShowEdit = true // 显示输入框 this.$nextTick(() => { this.$refs.inp.focus() // 获取焦点 })
- 当执行到$nextTick时,会更新渲染一次dom,输入框渲染出来,再向下执行代码
- 说到dom异步更新,很多人想到利用延时器实现,但是延时器延迟时间是固定的,而执行到dom渲染的时间并不固定(和网络、设备性能有关)。
注意:
$nextTick 内的函数体 一定是箭头函数,这样才能让函数内部的this指向Vue实例
-
4.自定义指令
- 与$nextTick类似,inserted会在指令所在元素被插入页面时触发
-
全局注册(下面以注册实现上面点击聚焦功能为例)
js//在main.js中 Vue.directive('focus', { // el为指令绑定的元素 "inserted" (el) { el.focus() } })
-
局部注册
js//在Vue组件的配置项中 directives: { "focus": { inserted (el) { el.focus() } } }
-
指令使用
html<input v-focus ref="inp" type="text">
4.1.指令的值
与vue内置指令相同,我们也可以为自定义指令设置值
-
示例:我们定义一个v-color指令,v-color的值变化时,带有v-color指令的标签也会变化
-
指令注册(局部注册)
jsdirectives: { color: { inserted (el, binding) { el.style.color = binding.value }, update (el, binding) { el.style.color = binding.value } } }
- binding.value为指令值
- 指令值修改会触发update函数(update也是一个生命周期钩子)
- 我们指令的值可以设置为变量,这时我们就需要设置update函数
4.2.封装v-loading指令
-
在加载时,页面通常会有一个loading动画效果
-
这个效果通常为一个蒙层(蒙层一般为伪元素)
-
loading的开启和关闭只需要添加移除类即可(因为伪元素是css生成的,自然也可以通过操作css移除)
示例:
html<template> <div class="main"> <div class="box" v-loading = "isLoading"> <ul> <li v-for="item in list" :key="item.id" class="news"> <div class="left"> <div class="title">{{ item.title }}</div> <div class="info"> <span>{{ item.source }}</span> <span>{{ item.time }}</span> </div> </div> <div class="right"> <img :src="item.img" alt=""> </div> </li> </ul> </div> </div> </template> <script> import axios from 'axios' export default { data () { return { list: [], isLoading: true } }, async created () { const res = await axios.get('http://hmajax.itheima.net/api/news') setTimeout(() => { this.list = res.data.data // 移除蒙层 this.isloading = false }, 2000) }, // 定义指令 directives: { loading: { inserted (el, binding) { binding.value?el.classList.add('loading'):el.classList.remove('loading') }, update (el, binding) { binding.value?el.classList.add('loading'):el.classList.remove('loading') } } } } </script> <style> /* 伪元素 */ .loading:before { content: ''; position: absolute; left: 0; top: 0; width: 100%; height: 100%; background: #fff url('./loading.gif') no-repeat center; } .box2 { width: 400px; height: 400px; border: 2px solid #000; position: relative; } .box { width: 800px; min-height: 500px; border: 3px solid orange; border-radius: 5px; position: relative; } .news { display: flex; height: 120px; width: 600px; margin: 0 auto; padding: 20px 0; cursor: pointer; } .news .left { flex: 1; display: flex; flex-direction: column; justify-content: space-between; padding-right: 10px; } .news .left .title { font-size: 20px; } .news .left .info { color: #999999; } .news .left .info span { margin-right: 20px; } .news .right { width: 160px; height: 120px; } .news .right img { width: 100%; height: 100%; object-fit: cover; } </style>
知识点:
-
安装axios :
yarn add axios
或npm i axios
-
定义指令部分:
jsdirectives: { loading: { inserted (el, binding) { // 如果加载,显示蒙层(移除loading类),否则,显示蒙层 binding.value?el.classList.add('loading'):el.classList.remove('loading') }, update (el, binding) { binding.value?el.classList.add('loading'):el.classList.remove('loading') } } }
-
-
v-loading工作流程:有些同学看到这里可能有些疑问,组件定义里面的函数是什么时候执行的?
- 上面有提到,inserted会在指令所在元素被
插入页面时触发
- 在上面示例中,先执行created生命周期钩子,向服务器发送获取数据请求,然后再将返回数据更新到 list 中,并改变isLoading
- 上面有提到,inserted会在指令所在元素被
5.插槽
5.1默认插槽
-
让组件内部的一些 结构 支持 自定义
-
语法:
- 组件内需要定制的结构部分,改用
<slot></slot>
占位 - 给插槽传入内容时,可以传入纯文本、html标签、组件
- 在标签内部, 传入结构替换slot
- 组件内需要定制的结构部分,改用
-
示例:弹框插槽
5.2.后备内容
上面示例中,如果不传内容,则会不会显示

我们可以为插槽设置默认显示内容,如果不传内容,则会显示默认显示内容
- 我们只需要在封装组件时,为预留的
<slot>
插槽提供后备内容即可(默认内容)

5.3.具名插槽
一个组件中,很多时候不单单只有一个插槽,这时我们需要使用name属性区分不同插槽。
-
使用name属性区分不同插槽。
-
template
配合v-slot:名字
来分发对应标签 -
为方便书写,上面可以将
v-slot:
替换为#
示例:
-
根组件(App.vue)
html<div id="app"> <HelloWorld> <template #age>年龄</template> <template #name>姓名</template> <template #gender>性别</template> </HelloWorld> </div>
-
子组件(HelloWorld.vue)
html<div> <slot name="name"></slot> <hr /> <slot name="age"></slot> <hr /> <slot name="gender"></slot> </div>
显示顺序取决于子组件的插槽位置
5.4.作用域插槽
-
作用域插槽不属于插槽的一种分类
-
定义slot插槽的同时,可以传值,给插槽绑定数据,这些数据与插槽绑定的组件也可以使用
-
给 slot 标签, 以 添加属性的方式传值(组件中)
html<slot :id="item.id" msg="测试文本"></slot>
-
在template中, 通过
#插槽名= "obj"
接收,默认插槽名为 default(根组件中)html<MyTable :list="list"> <template #default="obj"> <button @click="del(obj.id)">删除</button> </template> </MyTable>
-
-
所有添加的属性, 都会被收集到一个对象中,我们也可以将这个对象结构来使用
html<MyTable :list="list"> <template #default="{id, msg}"> <button @click="del(id)">删除</button> </template> </MyTable>
6.my-tag组件封装
实现功能:
- 双击显示,自动聚焦
- 失去焦点,隐藏输入框
- 回显标签内容
- 内容修改,回车,修改标签信息
代码:
-
MyTag
html<template> <div class="my-tag"> <input v-if="isEdit" v-focus class="input" type="text" placeholder="输入标签" :value="value" @blur="isEdit = false" @keyup.enter="handleEnter" /> <div v-else @dblclick="handleClick" class="text"> {{ value }} </div> </div> </template> <script> export default { props: { value: String }, data () { return { isEdit: false } }, methods: { handleClick () { // 双击后,切换到显示状态 (Vue是异步dom更新) this.isEdit = true }, handleEnter (e) { // 非空处理 if (e.target.value.trim() === '') return alert('标签内容不能为空') // 由于父组件是v-model,触发事件,需要触发 input 事件 this.$emit('input', e.target.value) // 提交完成,关闭输入状态 this.isEdit = false } } } </script> <style lang="less" scoped> .my-tag { cursor: pointer; .input { appearance: none; outline: none; border: 1px solid #ccc; width: 100px; height: 40px; box-sizing: border-box; padding: 10px; color: #666; &::placeholder { color: #666; } } } </style>
-
App.vue
html<template> <div> <MyTag v-model="item.tag"></MyTag> </div> </template> <script> import MyTag from './components/MyTag.vue' export default { name: 'TableCase', components: { MyTag, }, data () { return { // 测试组件功能的临时数据 tempText: '水杯', } } } </script> <style lang="less" scoped> .table-case { width: 1000px; margin: 50px auto; img { width: 100px; height: 100px; object-fit: contain; vertical-align: middle; } } </style>
-
main.js
jsimport Vue from 'vue' import App from './App.vue' Vue.config.productionTip = false // 封装全局指令 focus Vue.directive('focus', { // 指令所在的dom元素,被插入到页面中时触发 inserted (el) { el.focus() } }) new Vue({ render: h => h(App), }).$mount('#app')
知识点:
-
双击触发事件:@dblclick
-
两种自动聚焦方法:
在实现点击盒子切换为inpu标签并自动聚焦时,我们可以通过ref和refs操作dom,再配合$nextTick异步实现,不过为了提高复用性,我们通过自定义指令,封装到mian.js实现该功能
-
指令修饰符实现回车事件监听,@keyup.enter,由于该案例数据是由父组件传递过来的,所以还要将该值发送给父组件