组件的三大组成部分
- template
- 只能有一个根元素
- style
- 全局样式(默认):影响所有组件
- 局部样式:
scoped下样式,只作用于当前组件
- script
- el根实例独有,data是一个函数,其他配置项一致
scoped样式冲突
默认情况下,写在组件中的样式会全局生效——>因此容易造成多个组件之间的样式冲突问题
全局样式:默认组件中的样式都会作用到全局局部样式:可以给组件加上scoped属性,让样式只作用于当前组件
应用
- 这是原来APP.vue,其中有两个组件baseone和basetwo
- 组件分为三部分
- 导入
- 注册
- 使用
<template>
<div id="app">
<!-- <img alt="Vue logo" src="./assets/logo.png">
<HelloWorld msg="Welcome to Your Vue.js App"/> -->
//使用
<BaseOne></BaseOne>
<BaseTwo></BaseTwo>
</div>
</template>
<script>
//导入
//import HelloWorld from './components/HelloWorld.vue'
import BaseOne from './components/BaseOne'
import BaseTwo from './components/BaseTwo'
export default {
name: 'App',
//注册
components: {
BaseOne,
BaseTwo
}
}
</script>
它们的.vue文件为下面这样,此时给他们的.vue文件即使分别加样式,也会同时改变。
<template>
<div class="base-one">
BaseOne
</div>
</template>
<script>
export default {
}
</script>
<style>
</style>
此时给style加上scoped,
<style scoped>,就会变成局部组件,都会有独立的样式
scoped原理:
- 给当前组件模板的所有元素,都会添加上一个自定义属性,叫 data-v-hash值,比如
data-v-5f6a9d56
- 同一组件的hash值一般相同
- 不同组件hash值自然不同,所以可以用这个来区分不同的组件
- css选择器后面,被自动处理,添加上了不同选择器 div[data-v-5f6a9d56]
data必须是一个函数
一个组件的data选项必须是一个函数——>保证每个组件实例,维护独立的一份数据对象
- 每次创建新的组件实例,都会新执行一次data函数,得到一个新对象
//在export default{}里写
data: function () {
return {
count: 100,
}
}
整体就是:
<template>
<div class="base-count">
<button @click="count--">-</button>
<span>{{ count }}</span>
<button @click="count++">+</button>
</div>
</template>
<script>
export default {
data: function () {
return {
count: 100,
}
},
}
</script>
<style scoped>
.base-count {
margin: 20px;
}
</style>
- 每次创建新的组件实例,都会新执行一次data函数,得到一个新对象
- 保证每个组件实例,维护独立的一份数据对象,而这两句到底是什么意思呢?
- 意思就是,如果你现在在app.vue里面写了多个这个组件,它们也分别都是独立的,你动上一个并不会影响它其他几个
- data用几次,执行几次
组件通信
就是指 组件与组件 之间的 数据传递
- 组件的数据是独立的,无法直接访问其他组件的数据
- 想用其他组件的数据——>组件通信
组件关系分类:
- 父子关系
- 非父子关系
组件通信解决方案
父子关系
父 ——> 子
- 如果子组件中需要用到父组件的数据,那么就需要 父组件通过props将数据传递给子组件
- 子组件利用
$emit,通知父组件修改更新
现在有一个需求:
- 就是现在想把父组件
‘学前端,来黑马!’这个title用在子组件中,但是目前这样理论上是实现不了的
正确的流程应该如下图:
- 先给父组件中的子组件添加自定义属性(动态传递需要带冒号),并传值,如下图的
mytitle为值- 然后在子组件的vue中通过props接受你这个冒号的内容
- 最后直接在子组件上面模板中使用
子 ——> 父
现在有一个需求:
- 就是想通过点击按钮来修改上面的title
<div class="son" style="border: 3px solid #000; margin: 10px">
我是Son组件 {{ title }}
<button @click="changeFn">修改title</button>
正确的流程为:
- 先在子组件的
template里写好你的按钮,并给它注册了一个点击事件,并在下面写了一个methods方法。但是你此时在methods里直接写是没有用的
<button @click="changeFn">修改title</button>
- 通过this.$emit() 向父组件发送通知
methods: {
changeFn() {
// 通过this.$emit() 向父组件发送通知
this.$emit('changTitle','传智教育')
},
},
- 父组件为了收到消息,需要在子组件上绑上监听
<Son :title="myTitle" @changTitle="handleChange"></Son>
- 在父组件中提供对应的处理函数,提供逻辑
整体流程图为:
props详解
定义、作用、特点、应用
props定义: 组件上注册的一些自定义属性
props:[]props作用: 向子组件传递数据
props特点:
- 可以传递 任意数量 的props
- 可以传递 任意类型 的props
父组件中提供数据,给这个子组件以添加属性的方式传值
tip:兴趣爱好那里,通过join转换为字符串,使用顿号分割
props校验
为组件的prop指定验证要求,不符合要求,控制台就会有错误提示——> 帮助开发者,快速发现错误
- 分为:
- 类型校验
- 非空校验
- 默认值
- 自定义
类型校验
可以严格约束你传过来的到底是什么类型
prop:{校验的属性名:类型},类型可以是number、string、boolean、array、object、function...
//原来
props:['w']
//现在把它写成对象的方式
//直接key的地方写原来的属性
props: {
w: Number,
},
更复杂的其他的校验
类型校验只需在校验的属性名后面写类型即可,但是,如果你有更高的需求,就需要在这里写成对象形式
props:{
检验的属性名:{
type:类型,//跟上面类型校验那一个意思
required:true,//是否必填
default:默认值,//给一个默认值
//如果对传进来的值有更细节的要求,用下面这个自定义校验
//return true则表示通过了这个校验
//里面这个value形参是可以拿到你要校验的这个值的
validator(value){
//自定义校验的逻辑
return 是否通过校验
}
}
}
比如:
props: {
w: {
type: Number,
required: true,
default: 0,
validator(val) {
// console.log(val)
if (val >= 100 || val <= 0) {
console.error('传入的范围必须是0-100之间')
return false
} else {
return true
}
},
},
},
}
prop和data关系与区别、单向数据流
prop和data共同点: 都可以给组件提供数据
区别:
- data的数据是 自己 的——> 随便改(自己的数据自己负责)
- prop的数据是 外部 的——> 不能直接改,要遵循单向数据流
单向数据流: 当父组件的prop更新,接着向下流,也会影响到子组件,子组件变化,视图也就更新了
还是这个案例:
App.vue,父组件
<template>
<div id="app">
<BaseTwo :count="count" @changeCount="handleChange"></BaseTwo>
</div>
</template>
<script>
import BaseTwo from './components/BaseTwo'
export default {
name: 'App',
components: {
BaseTwo
},
data(){
return {
count:100
}
},
methods:{
handleChange(newVal){
// console.log(newVal);
this.count = newVal
}
}
}
</script>
<style>
</style>
BaseTwo.vue,子组件
<template>
<div class="base-count">
<button @click="count--">-</button>
<span>{{ count }}</span>
<button @click="count++">+</button>
</div>
</template>
<script>
export default {
props: {
//父传子
count: {
type: Number,
},
},
methods: {
//子传父
handleSub() {
this.$emit('changeCount', this.count - 1)
},
handleAdd() {
this.$emit('changeCount', this.count + 1)
},
},
}
</script>
<style scoped>
.base-count {
margin: 20px;
}
</style>
小黑记事本——组件通信案例
1. 拆分基础组件(三步走)
//1.
<template>
<!-- 主体区域 -->
<section id="app">
<TodoHeader></TodoHeader>
<TodoMain ></TodoMain>
<TodoFooter></TodoFooter>
</section>
</template>
<script>
//2.
import TodoHeader from './components/TodoHeader.vue'
import TodoMain from './components/TodoMain.vue'
import TodoFooter from './components/TodoFooter.vue'
//3.
components: {
TodoHeader,
TodoMain,
TodoFooter,
}
</script>
2.渲染待办任务
-
- 提供数据——> 提供在 公共(data) 的父组件App.vue
- 这样的话,如果之后子组件要用,直接父传子就可以
- 提供数据——> 提供在 公共(data) 的父组件App.vue
为什么提供在父组件里?(上面
"data必须是一个函数")
- 每次创建新的组件实例,都会新执行一次data函数,得到一个新对象
- 保证每个组件实例,维护独立的一份数据对象,而这两句到底是什么意思呢?
- 意思就是,如果你现在在app.vue里面写了多个这个组件,它们也分别都是独立的,你动上一个并不会影响它其他几个
- data用几次,执行几次
//在App.vue,父组件中提供数据
data() {
return {
//提供数据
list: [
{ id: 1, name: '看琉璃' },
{ id: 2, name: '逛淘宝' },
{ id: 3, name: '打游戏' },
],
}
},
-
- 通过父传子,将数据传递给TodoMain这几个组件
//App.vue中
<TodoMain :list="list"></TodoMain>
//TodoMain中
props: {
list: {
type: Array,
},
},
-
- 利用v-for渲染
<template>
<!-- 列表区域 -->
<section class="main">
<ul class="todo-list">
<li class="todo" v-for="(item, index) in list" :key="item.id">
<div class="view">
<span class="index">{{ index + 1 }}.</span>
<label>{{ item.name }}</label>
<button class="destroy" @click="handleDel(item.id)"></button>
</div>
</li>
</ul>
</section>
</template>
3. 添加任务
-
- 拿到输入框数据(也就是收集表单数据) ——>
v-model——>data里的todoName
- 拿到输入框数据(也就是收集表单数据) ——>
-
- 监听事件(回车+点击)
@keyup.enter,@click——>methods
- 监听事件(回车+点击)
-
- 子传父,将任务名称传递给父组件
-
- 父组件进行unshift添加
<template>
<!-- 输入框 -->
<header class="header">
<h1>小黑记事本</h1>
<input placeholder="请输入任务" class="new-todo" v-model="todoName" @keyup.enter="handleAdd"/>
<button class="add" @click="handleAdd">添加任务</button>
</header>
</template>
<script>
export default {
data(){
return {
todoName:''
}
},
methods:{
handleAdd(){
if(this.todoName.trim()===''){
alert('任务名名称不能为空')
return
}
// console.log(this.todoName)
//子传父
this.$emit('add',this.todoName)
this.todoName = ''
}
}
}
</script>
传给父:
<TodoHeader @add="handleAdd"></TodoHeader>
methods: {
handleAdd(todoName) {
// console.log(todoName)
this.list.unshift({
id: +new Date(),
name: todoName,
})
},
}
4. 删除功能
-
- 监听删除的点击
-
- 因为最开始我们的数据就在父组件,所以第二步还是子传父,将删除的id传递给父组件,记得带id
-
- 在父组件处进行删除fliter
//在父组件中
<TodoMain :list="list" @del="handelDel"></TodoMain>
handelDel(id) {
// console.log(id);
this.list = this.list.filter((item) => item.id !== id)
},
//TodoMain中
<button class="destroy" @click="handleDel(item.id)"></button>
methods: {
handleDel(id) {
this.$emit('del', id)
},
},
5.底部合计和清空
底部合计: 父传子(把数据传给子)+渲染
底部清空: 子传父,通知到父组件,由父组件进行操作清空
//父组件
<TodoFooter :list="list" @clear="clear"></TodoFooter>
clear() {
this.list = []
},
//子组件TodoFooter
<template>
<!-- 统计和清空 -->
<footer class="footer">
<!-- 统计 -->
<span class="todo-count"
>合 计:<strong> {{ list.length }} </strong></span
>
<!-- 清空 -->
<button class="clear-completed" @click="clear">清空任务</button>
</footer>
</template>
<script>
export default {
props: {
list: {
type: Array,
},
},
methods:{
clear(){
this.$emit('clear')
}
}
}
</script>
<style>
</style>
6. 持久化存储:用watch去深度监听list的变化——>变化了就往本地存储——>进入页面优先读取本地
//父组件中(是与methods并列的)
list: JSON.parse(localStorage.getItem('list')) ||[
...
]
watch: {
list: {
deep: true,//深度监视
//处理函数
handler(newVal) {
localStorage.setItem('list', JSON.stringify(newVal))
},
},
},
非父子通信(拓展)——event bus事件总线
- 非父子组件之间,进行简易的消息传递
- 并不是一对一的,而是可以一对多的
- 只要监听了就都可以接收到然后进行渲染
思路:
在src文件夹中新建一个文件夹utils——>然后再新建一个EventBus.js文件
- 创建一个都能访问到的事件总线Event Bus(因为A、B组件本身没什么关联,所以我们现在要创建关联)
//创建一个vue实例,并进行导出
import Vue from 'vue'
//创建实例new vue
const Bus = new Vue()
export default Bus
- A组件(接受方),监听Bus实例的事件
<p>{{msg}}</p>
//再A里导入一下
import Bus from '../utils/EventBus'
//再导出,进行监听,监听Bus
export default{
created(){
//第一个‘’里写的是你要监听的事件名,后面是监听到后要干嘛
Bus.$on('sendMsg',(msg)=>{
this.msg=msg
})
}
}
- B组件(发送方),触发Bus实例的事件,
Bus.$emit('sendMsg', '这是一条消息')
<button @click="sendMsgFn">发送消息</button>
import Bus from '../utils/EventBus'
export default {
methods: {
sendMsgFn() {
Bus.$emit('sendMsg', '这是一条消息')
},
},
}
非父子通信(拓展)——provide & inject
provide & inject作用:跨层级 共享数据(爷孙)
- 爷爷provide的数据可以直接inject给孙子
- 不需要爷传父,父传子
- 父组件provide
export default {
provide() {
return {
// 简单类型 是非响应式的
color: this.color,
// 复杂类型 是响应式的
userInfo: this.userInfo,
}
},
//————————————————————————
data() {
return {
color: 'pink',//简单类型,不响应,不推荐
//复杂类型,响应,推荐
userInfo: {
name: 'zs',
age: 18,
},
}
},
- 子/孙组件inject直接接收使用,然后再渲染
export default {
inject: ['color', 'userInfo'],
}
v-model详解
原理
原理: v-model本质上是一个语法糖。
作用: 提供数据的双向绑定
- 数据变,视图跟着变
:value - 视图变,数据跟着变
@input
例如应用在输入框上,就是value属性和input事件的合写
tip:
$event用在模板中,获取事件的形参
表单类组件封装 & v-model简化代码
表单类组件封装
- 父传子:
:value,数据应该是父组件props传递过来的,v-model拆解绑定数据(因为你不拆的话,父传子理论上是无法修改的) - 子传父:
@input,监听输入,子传父传值给父组件修改
<template>
<div class="app">
<BaseSelect
:cityId="selectId"
@change="selectId=$event"
></BaseSelect>
</div>
</template>
<script>
import BaseSelect from './components/BaseSelect.vue'
export default {
data() {
return {
selectId: '102',
}
},
components: {
BaseSelect,
},
}
</script>
<style>
</style>
<template>
<div>
<select :value="cityId" @change="selectCity">
<option value="101">北京</option>
<option value="102">上海</option>
<option value="103">武汉</option>
<option value="104">广州</option>
<option value="105">深圳</option>
</select>
</div>
</template>
<script>
export default {
props: {
cityId: String,
},
methods: {
selectCity(e) {
this.$emit('changeId', e.target.value)
},
},
}
</script>
<style>
</style>
v-model简化代码
- 子组件中:
props通过value接收,事件触发input(这个不能简写成v-model,必须拆解) - 父组件中:
v-model给组件直接绑数据(把两个合并成v-model了)
1.
v-model="selectId"
<!-- 简化 -->
<!-- :cityId="selectId" @change="selectId=$event" -->
2.
props: {
value: String,
},
//props: {
// cityId: String,
//},
3.
<select :value="value" @change="selectCity">
//<select :value="cityId" @change="selectCity">
4.
methods: {
selectCity(e) {
this.$emit('input', e.target.value)
},
},
}
// methods: {
// selectCity(e) {
// this.$emit('changeId', e.target.value)
// },
// },
//}
sync修饰符
跟v-model作用一样,但是语法略有不同
作用:可以实现子组件与父组件数据的双向绑定,简化代码
特点:prop属性名,可以自定义,非固定为value
isShow.sync => :isShow="isShow" @update:isShow="isShow=$event"
实例:“确认要退出吗”
父组件中
//直接简写成
<BaseDialog :isShow.sync="isShow"></BaseDialog>
data() {
return {
isShow: false,
}
},
完整为:
<template>
<div class="app">
<button @click="openDialog">退出按钮</button>
<!-- isShow.sync => :isShow="isShow" @update:isShow="isShow=$event" -->
<BaseDialog :isShow.sync="isShow"></BaseDialog>
</div>
</template>
<script>
import BaseDialog from './components/BaseDialog.vue'
export default {
data() {
return {
isShow: false,
}
},
methods: {
openDialog() {
this.isShow = true
},
},
components: {
BaseDialog,
},
}
</script>
<style>
</style>
子组件中
<div class="base-dialog-wrap" v-show="isShow">
export default {
props: {
isShow: Boolean,
},
methods:{
closeDialog(){
//update:...为固定事件名
this.$emit('update:isShow',false)
}
}
}
完整为:
<template>
<!-- 显示隐藏 v-show="isShow"-->
<div class="base-dialog-wrap" v-show="isShow">
<div class="base-dialog">
<div class="title">
<h3>温馨提示:</h3>
<button class="close" @click="closeDialog">x</button>
</div>
<div class="content">
<p>你确认要退出本系统么?</p>
</div>
<div class="footer">
<button>确认</button>
<button>取消</button>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
isShow: Boolean,
},
methods:{
closeDialog(){
this.$emit('update:isShow',false)
}
}
}
</script>
ref和$refs 获取dom和组件
作用: 利用ref和$refs,可以用于 获取dom元素 或 组件实例
特点 :查找范围——> 当前组件内(更精确稳定)
querySelector查找范围——>整个页面
获取dom元素
- 获取dom:(在子组件)
目标标签——添加ref属性
<div class="base-chart-box" ref="baseChartBox">子组件</div>
- 恰当时机,通过
this.$refs.xxx,获取目标标签(在子组件)
mounted() {
// 基于准备好的dom,初始化echarts实例
// document.querySelector 会查找项目中所有的元素
// $refs只会在当前组件查找盒子
// var myChart = echarts.init(document.querySelector('.base-chart-box'))
var myChart = echarts.init(this.$refs.baseChartBox)
}
组件实例
- 获取组件
目标组件——添加ref属性
<BaseForm ref="baseForm"></BaseForm>
- 恰当时机,通过
this.$refs.xxx,获取目标组件(在子组件),
就可以 调用组件对象里面的方法
this.$refs.baseForm.组件方法()
Vue异步更新——解决方法$nextTick
需求:点击编辑,编辑标题,编辑框自动聚焦
$nextTick:等dom更新后,才会触发执行此方法里的函数体
- 语法:
this.$nextTick(函数体)
- 点击编辑,显示编辑框
- 让编辑框立刻获取焦点
原本代码思路:
使用$nextTick解决
//区别!!!!!!!!!!!!!,把原来的放到这个nextTick里
this.$nextTick(()=>{
this.$refs.inp.focus()
})
//this.$refs.inp.focus()
tip:setTimeout虽然也可以解决异步更新问题,但是它等待的时间需要你自己设置,但是你也不知道它多久才能渲染好,所以不够精准
完整代码就是:
//在APP.vue父组件里!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
<template>
<div class="app">
<div v-if="isShowEdit">
<input ref="inp" type="text" v-model="editValue" />
<button>确认</button>
</div>
<div v-else>
<span>{{ title }}</span>
<!-- 注册点击事件,一点的话isShowEdit变成true显示出来 -->
<button @click="handleEdit">编辑</button>
</div>
</div>
</template>
<script>
export default {
data() {
return {
title: '大标题',
//原来
isShowEdit: false,
editValue: '',
}
},
methods: {
handleEdit(){
//1.显示输入框
//(vue异步dom更新),还没被完全渲染出来
this.isShowEdit=true
//2.让输入框获取焦点
this.$nextTick(()=>{
this.$refs.inp.focus()
})
//this.$refs.inp.focus()
}
},
}
</script>
<style>
</style>