文章目录
五、组件间消息
5.1 todo-list 案例
- 编码思路
- 实现静态组件:抽取组件,使用组件实现静态页面效果
- 展示动态数据:数据的类型、名称是什么?数据保存在哪个组件?
- 实现交互功能:从绑定事件监听开始
- 实现静态组件
- App.vue
<template>
<div id="root">
<div class="todo-container">
<div class="todo-wrap">
<MyHeader/>
<MyList/>
<MyFooter/>
</div>
</div>
</div>
</template>
<script>
import MyHeader from './components/MyHeader'
import MyList from './components/MyList'
import MyFooter from './components/MyFooter'
export default {
name: 'App',
components: {MyHeader, MyList, MyFooter}
}
</script>
<style>
/*base*/
body {
background: #fff;
}
.btn {
display: inline-block;
padding: 4px 12px;
margin-bottom: 0;
font-size: 14px;
line-height: 20px;
text-align: center;
vertical-align: middle;
cursor: pointer;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
border-radius: 4px;
}
.btn-danger {
color: #fff;
background-color: #da4f49;
border: 1px solid #bd362f;
}
.btn-danger:hover {
color: #fff;
background-color: #bd362f;
}
.btn:focus {
outline: none;
}
.todo-container {
width: 600px;
margin: 0 auto;
}
.todo-container .todo-wrap {
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
}
</style>
- Header.vue
<template>
<div class="todo-header">
<input type="text" placeholder="请输入你的任务名称,按回车键确认"/>
</div>
</template>
<script>
export default {
name: 'MyHeader'
}
</script>
<style scoped>
/*header*/
.todo-header input {
width: 560px;
height: 28px;
font-size: 14px;
border: 1px solid #ccc;
border-radius: 4px;
padding: 4px 7px;
}
.todo-header input:focus {
outline: none;
border-color: rgba(82, 168, 236, 0.8);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6);
}
</style>
- MyList.vue
<template>
<ul class="todo-main">
<MyItem/>
<MyItem/>
<MyItem/>
</ul>
</template>
<script>
import MyItem from './MyItem'
export default {
name: 'MyList',
components: {MyItem}
}
</script>
<style scoped>
/*main*/
.todo-main {
margin-left: 0px;
border: 1px solid #ddd;
border-radius: 2px;
padding: 0px;
}
.todo-empty {
height: 40px;
line-height: 40px;
border: 1px solid #ddd;
border-radius: 2px;
padding-left: 5px;
margin-top: 10px;
}
</style>
- MyItem.vue
<template>
<li>
<label>
<input type="checkbox"/>
<span>xxxxx</span>
</label>
<button class="btn btn-danger" style="display:none">删除</button>
</li>
</template>
<script>
export default {
name: 'MyItem'
}
</script>
<style scoped>
/*item*/
li {
list-style: none;
height: 36px;
line-height: 36px;
padding: 0 5px;
border-bottom: 1px solid #ddd;
}
li label {
float: left;
cursor: pointer;
}
li label li input {
vertical-align: middle;
margin-right: 6px;
position: relative;
top: -1px;
}
li button {
float: right;
display: none;
margin-top: 3px;
}
li:before {
content: initial;
}
li:last-child {
border-bottom: none;
}
</style>
- MyFooter.vue
<template>
<div class="todo-footer">
<label>
<input type="checkbox"/>
</label>
<span>
<span>已完成0</span> / 全部2
</span>
<button class="btn btn-danger">清除已完成任务</button>
</div>
</template>
<script>
export default {
name: 'MyFooter'
}
</script>
<style scoped>
/*footer*/
.todo-footer {
height: 40px;
line-height: 40px;
padding-left: 6px;
margin-top: 5px;
}
.todo-footer label {
display: inline-block;
margin-right: 20px;
cursor: pointer;
}
.todo-footer label input {
position: relative;
top: -1px;
vertical-align: middle;
margin-right: 5px;
}
.todo-footer button {
float: right;
margin-top: 5px;
}
</style>
- 展示动态数据
- MyList.vue
<template>
<ul class="todo-main">
<MyItem v-for="todo in todos" :key="todo.id" :todo="todo"/>
</ul>
</template>
<script>
import MyItem from './MyItem'
export default {
name: 'MyList',
components: {MyItem},
data() {
return {
todos: [
{id: '001', title: '读书', done: true},
{id: '002', title: '游泳', done: false},
{id: '003', title: '跑步', done: true}
]
}
},
}
</script>
- MyItem.vue
<template>
<li>
<label>
<input type="checkbox" :checked="todo.done" />
<span>{{todo.title}}</span>
</label>
<button class="btn btn-danger" style="display:none">删除</button>
</li>
</template>
<script>
export default {
name: 'MyItem',
props: ['todo']
}
</script>
- 实现交互
- 添加一个 todo
添加新 todo 项时,需要将数据从 MyHeader 传入 todos,由于组件间目前无法实现数据交互,只能将 todos 抽取到父组件 App .
<template>
<div id="root">
<div class="todo-container">
<div class="todo-wrap">
<!-- 父组件将添加 todo 的方法传递给子组件,从而反向完成数据的交互动作 -->
<MyHeader :addTodo="addTodo"/>
<MyList :todos="todos"/>
<MyFooter/>
</div>
</div>
</div>
</template>
<script>
import MyHeader from './components/MyHeader'
import MyList from './components/MyList'
import MyFooter from './components/MyFooter'
export default {
name: 'App',
components: {MyHeader, MyList, MyFooter},
data() {
return {
todos: [
{id: '001', title: '读书', done: true},
{id: '002', title: '游泳', done: false},
{id: '003', title: '跑步', done: true}
]
}
},
methods: {
addTodo(newTodo) {
this.todos.unshift(newTodo)
}
},
}
</script>
<template>
<div class="todo-header">
<input type="text" placeholder="请输入你的任务名称,按回车键确认" v-model="title" @keyup.enter="add"/>
</div>
</template>
<script>
import {nanoid} from 'nanoid'
export default {
name: 'MyHeader',
data() {
return {
title: ''
}
},
props: ['addTodo'],
methods: {
add(event) {
// 校验数据
if(!this.title.trim()) return alert('输入不能为空')
// 通知 App 去添加一个组件
this.addTodo({id: nanoid(), title: event.target.value, done: false})
// 清空输入
this.title = ''
}
}
}
</script>
<template>
<ul class="todo-main">
<MyItem v-for="todo in todos" :key="todo.id" :todo="todo"/>
</ul>
</template>
<script>
import MyItem from './MyItem'
export default {
name: 'MyList',
components: {MyItem},
props: ['todos']
}
</script>
- 动态勾选功能
<template>
<li>
<label>
<input type="checkbox" :checked="todo.done" @change="handleCheck(todo.id)"/>
<!-- 如下代码也可以实现功能,但违反了原则:即不能修改 props -->
<!-- <input type="checkbox" v-model="todo.done"/> -->
<span>{{todo.title}}</span>
</label>
<button class="btn btn-danger" style="display:none">删除</button>
</li>
</template>
<script>
export default {
name: 'MyItem',
props: ['todo', 'checkTodo'],
methods: {
handleCheck(todoID) {
// 通知 App 组件将对应的 todo 对象的 done 值取反
this.checkTodo(todoID)
}
}
}
</script>
<template>
<div id="root">
<div class="todo-container">
<div class="todo-wrap">
<!-- 父组件将添加 todo 的方法传递给子组件,从而反向完成数据的交互动作 -->
<MyHeader :addTodo="addTodo"/>
<MyList :todos="todos" :checkTodo="checkTodo"/>
<MyFooter/>
</div>
</div>
</div>
</template>
<script>
import MyHeader from './components/MyHeader'
import MyList from './components/MyList'
import MyFooter from './components/MyFooter'
export default {
name: 'App',
components: {MyHeader, MyList, MyFooter},
data() {
return {
todos: [
{id: '001', title: '读书', done: true},
{id: '002', title: '游泳', done: false},
{id: '003', title: '跑步', done: true}
]
}
},
methods: {
// 添加 todo
addTodo(newTodo) {
this.todos.unshift(newTodo)
},
// 勾选或取消勾选 todo
checkTodo(todoID) {
this.todos.forEach((todo) => {
if(todo.id === todoID) todo.done = !todo.done
})
}
}
}
</script>
<template>
<ul class="todo-main">
<MyItem
v-for="todo in todos"
:key="todo.id"
:todo="todo"
:checkTodo="checkTodo"
/>
</ul>
</template>
<script>
import MyItem from './MyItem'
export default {
name: 'MyList',
components: {MyItem},
props: ['todos', 'checkTodo']
}
</script>
- 删除 todo
<template>
<li>
<label>
<input type="checkbox" :checked="todo.done" @change="handleCheck(todo.id)"/>
<!-- 如下代码也可以实现功能,但违反了原则:即不能修改 props -->
<!-- <input type="checkbox" v-model="todo.done"/> -->
<span>{{todo.title}}</span>
</label>
<button class="btn btn-danger" @click="handleDelete(todo.id)">删除</button>
</li>
</template>
<script>
export default {
name: 'MyItem',
props: ['todo', 'checkTodo', "removeTodo"],
methods: {
// 勾选或取消勾选
handleCheck(todoID) {
// 通知 App 组件将对应的 todo 对象的 done 值取反
this.checkTodo(todoID)
},
// 删除
handleDelete(todoID) {
if(confirm('确认删除吗?'))
this.removeTodo(todoID)
}
}
}
</script>
<style scoped>
//...
li:hover {
background-color: #ddd;
}
li:hover button {
display: block;
}
</style>
<template>
<div id="root">
<div class="todo-container">
<div class="todo-wrap">
<!-- 父组件将添加 todo 的方法传递给子组件,从而反向完成数据的交互动作 -->
<MyHeader :addTodo="addTodo"/>
<MyList :todos="todos" :checkTodo="checkTodo" :removeTodo="removeTodo"/>
<MyFooter/>
</div>
</div>
</div>
</template>
<script>
import MyHeader from './components/MyHeader'
import MyList from './components/MyList'
import MyFooter from './components/MyFooter'
export default {
name: 'App',
components: {MyHeader, MyList, MyFooter},
data() {
return {
todos: [
{id: '001', title: '读书', done: true},
{id: '002', title: '游泳', done: false},
{id: '003', title: '跑步', done: true}
]
}
},
methods: {
// 添加 todo
addTodo(newTodo) {
this.todos.unshift(newTodo)
},
// 勾选或取消勾选 todo
checkTodo(todoID) {
this.todos.forEach((todo) => {
if(todo.id === todoID) todo.done = !todo.done
})
},
// 删除 todo
removeTodo(todoID) {
this.todos = this.todos.filter(todo => todo.id !== todoID)
}
}
}
</script>
<template>
<ul class="todo-main">
<MyItem
v-for="todo in todos"
:key="todo.id"
:todo="todo"
:checkTodo="checkTodo"
:removeTodo="removeTodo"
/>
</ul>
</template>
<script>
import MyItem from './MyItem'
export default {
name: 'MyList',
components: {MyItem},
props: ['todos', 'checkTodo', 'removeTodo']
}
</script>
- 底部统计功能
<template>
<div class="todo-footer">
<label>
<input type="checkbox"/>
</label>
<span>
<span>已完成{{doneTotal}}</span> / 全部{{todos.length}}
</span>
<button class="btn btn-danger">清除已完成任务</button>
</div>
</template>
<script>
export default {
name: 'MyFooter',
props: ['todos'],
computed: {
doneTotal() {
/*
* reduce(func, init)
* func: 计算函数
* init: 计算结果的初始值
* func(pre, current)
* pre: 前一次的计算结果
* current: 当前的数组对象
*/
return this.todos.reduce((pre, current) => pre + (current.done ? 1 : 0), 0)
}
}
}
</script>
- 底部全部勾选及清除
<template>
<div class="todo-footer" v-show="total">
<label>
<!-- <input type="checkbox" :checked="isAllChecked" @change="reverseCheckAll"/> -->
<!-- // 2. 全勾选或全不勾选第二种实现方法 -->
<input type="checkbox" v-model="isAllChecked"/>
</label>
<span>
<span>已完成{{doneTotal}}</span> / 全部{{total}}
</span>
<button class="btn btn-danger" @click="clearAll">清除已完成任务</button>
</div>
</template>
<script>
export default {
name: 'MyFooter',
props: ['todos', 'reverseCheckAllTodo', 'clearAllTodo'],
computed: {
total() {
return this.todos.length
},
doneTotal() {
/*
* reduce(func, init)
* func: 计算函数
* init: 计算结果的初始值
* func(pre, current)
* pre: 前一次的计算结果
* current: 当前的数组对象
*/
return this.todos.reduce((pre, current) => pre + (current.done ? 1 : 0), 0)
},
isAllChecked: {
get() {
return this.doneTotal === this.total && this.total > 0
},
set(value) {
this.reverseCheckAllTodo(value)
}
}
},
methods: {
// 1. 全勾选或全不勾选第一种实现方法
// reverseCheckAll(event) {
// this.reverseCheckAllTodo(event.target.checked)
// }
clearAll() {
this.clearAllTodo()
}
}
}
</script>
<template>
<div id="root">
<div class="todo-container">
<div class="todo-wrap">
<!-- 父组件将添加 todo 的方法传递给子组件,从而反向完成数据的交互动作 -->
<MyHeader :addTodo="addTodo"/>
<MyList :todos="todos" :checkTodo="checkTodo" :removeTodo="removeTodo"/>
<MyFooter :todos="todos" :reverseCheckAllTodo="reverseCheckAllTodo" :clearAllTodo="clearAllTodo"/>
</div>
</div>
</div>
</template>
<script>
import MyHeader from './components/MyHeader'
import MyList from './components/MyList'
import MyFooter from './components/MyFooter'
export default {
name: 'App',
components: {MyHeader, MyList, MyFooter},
data() {
return {
todos: [
{id: '001', title: '读书', done: true},
{id: '002', title: '游泳', done: false},
{id: '003', title: '跑步', done: true}
]
}
},
methods: {
// 添加 todo
addTodo(newTodo) {
this.todos.unshift(newTodo)
},
// 勾选或取消勾选 todo
checkTodo(todoID) {
this.todos.forEach((todo) => {
if(todo.id === todoID) todo.done = !todo.done
})
},
// 删除 todo
removeTodo(todoID) {
this.todos = this.todos.filter(todo => todo.id !== todoID)
},
// 反转全部勾选或全不勾选
reverseCheckAllTodo(checked) {
this.todos.forEach(todo => todo.done = checked)
},
// 删除所有已经完成的 todo
clearAllTodo() {
this.todos = this.todos.filter((todo) => {
return !todo.done
})
}
}
}
</script>
- 总结
- 组件化编码流程:
1.1 拆分静态组件:组件要按照功能点拆分,命名不要与html元素冲突。
1.2 实现动态组件:考虑好数据的存放位置,数据是一个组件在用,还是一些组件在用:
1.2.1 一个组件在用:放在组件自身即可。
1.2.2 一些组件在用:放在他们共同的父组件上(状态提升)。
1.3 实现交互:从绑定事件开始。 - props适用于:
2.1 父组件 =》 子组件 通信
2.2 子组件 =》 父组件 通信(要求父先给子一个函数) - 使用v-model时要切记:v-model绑定的值不能是props传过来的值,因为props是不可以修改的!
- props传过来的若是对象类型的值,修改对象中的属性时Vue不会报错,但不推荐这样做。
5.2 local / sessionStorage 浏览器本地存储
- 简介
- 存储内容大小一般支持5MB左右(不同浏览器可能还不一样)
- 浏览器端通过 Window.sessionStorage 和 Window.localStorage 属性来实现本地存储机制。
- 相关API:
方法 | 说明 |
---|---|
xxxxxStorage.setItem('key', 'value'); | 该方法接受一个键和值作为参数,会把键值对添加到存储中,如果键名存在,则更新其对应的值。 |
xxxxxStorage.getItem('person'); | 该方法接受一个键名作为参数,返回键名对应的值。 |
xxxxxStorage.removeItem('key'); | 该方法接受一个键名作为参数,并把该键名从存储中删除。 |
xxxxxStorage.clear() | 该方法会清空存储中的所有数据。 |
- 说明
- SessionStorage存储的内容会随着浏览器窗口关闭而消失。
- LocalStorage存储的内容,需要手动清除才会消失。
xxxxxStorage.getItem(xxx)
如果xxx对应的value获取不到,那么getItem的返回值是null。JSON.parse(null)
的结果依然是null。
- localStorage
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>localStorage</title>
</head>
<body>
<h2>localStorage</h2>
<button onclick="saveData()">点我保存一个数据</button>
<button onclick="readData()">点我读取一个数据</button>
<button onclick="deleteData()">点我删除一个数据</button>
<button onclick="deleteAllData()">点我清空一个数据</button>
<script type="text/javascript" >
let p = {name:'张三',age:18}
function saveData(){
localStorage.setItem('msg','hello!!!')
localStorage.setItem('msg2',666)
localStorage.setItem('person',JSON.stringify(p))
}
function readData(){
console.log(localStorage.getItem('msg'))
console.log(localStorage.getItem('msg2'))
const result = localStorage.getItem('person')
console.log(JSON.parse(result))
// console.log(localStorage.getItem('msg3'))
}
function deleteData(){
localStorage.removeItem('msg2')
}
function deleteAllData(){
localStorage.clear()
}
</script>
</body>
</html>
- sessionStorage
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>sessionStorage</title>
</head>
<body>
<h2>sessionStorage</h2>
<button onclick="saveData()">点我保存一个数据</button>
<button onclick="readData()">点我读取一个数据</button>
<button onclick="deleteData()">点我删除一个数据</button>
<button onclick="deleteAllData()">点我清空一个数据</button>
<script type="text/javascript" >
let p = {name:'张三',age:18}
function saveData(){
sessionStorage.setItem('msg','hello!!!')
sessionStorage.setItem('msg2',666)
sessionStorage.setItem('person',JSON.stringify(p))
}
function readData(){
console.log(sessionStorage.getItem('msg'))
console.log(sessionStorage.getItem('msg2'))
const result = sessionStorage.getItem('person')
console.log(JSON.parse(result))
// console.log(sessionStorage.getItem('msg3'))
}
function deleteData(){
sessionStorage.removeItem('msg2')
}
function deleteAllData(){
sessionStorage.clear()
}
</script>
</body>
</html>
5.3 todo-list 改造:持久化
<script>
import MyHeader from './components/MyHeader'
import MyList from './components/MyList'
import MyFooter from './components/MyFooter'
export default {
name: 'App',
components: {MyHeader, MyList, MyFooter},
data() {
return {
todos: JSON.parse(localStorage.getItem('todos')) || []
}
},
methods: {
// 添加 todo
addTodo(newTodo) {
this.todos.unshift(newTodo)
},
// 勾选或取消勾选 todo
checkTodo(todoID) {
this.todos.forEach((todo) => {
if(todo.id === todoID) todo.done = !todo.done
})
},
// 删除 todo
removeTodo(todoID) {
this.todos = this.todos.filter(todo => todo.id !== todoID)
},
// 反转全部勾选或全不勾选
reverseCheckAllTodo(checked) {
this.todos.forEach(todo => todo.done = checked)
},
// 删除所有已经完成的 todo
clearAllTodo() {
this.todos = this.todos.filter((todo) => {
return !todo.done
})
}
},
watch: {
todos: {
deep: true,
handler(value) {
localStorage.setItem('todos', JSON.stringify(value))
}
}
}
}
</script>
5.4 组件自定义事件
一种组件间通信的方式,适用于:子组件 ===> 父组件
使用场景:A是父组件,B是子组件,B想给A传数据,那么就要在 A中给B绑定自定义事件,事件的回调在A中。
- 知识点
- 定义 组件自定义事件;
- 解绑 组件自定义事件
- 销毁 组件实例:所有自定义事件失效。
- 父子组件通过自定义事件传递消息
<template>
<div class="app">
<h1>{{msg}}</h1>
<!-- 通过父组件给子组件传递函数类型的 props 实现:子给父传递数据 -->
<School :getSchoolName="getSchoolName"/>
<!-- 通过父组件给子组件绑定一个自定义事件实现:子给父传递数据(第一种写法,使用 @ 或 v-on) -->
<!-- <Student @getStudentName="getStudentName"/> -->
<!-- 通过父组件给子组件绑定一个自定义事件实现:子给父传递数据(第二种写法,使用 ref) -->
<Student ref="student" />
</div>
</template>
<script>
import Student from './components/Student'
import School from './components/School'
export default {
name: 'App',
components: {Student, School},
data() {
return {
msg: '你好啊!'
}
},
methods: {
getSchoolName(name) { console.log(name) },
// 函数通过 ...params 接收不定参数列表
getStudentName(name, ...params) { console.log(name, params) }
},
mounted() {
setTimeout(() => {
this.$refs.student.$on('getStudentName', this.getStudentName)
}, 3000)
}
}
</script>
<template>
<div class="school">
<h2>学校姓名:{{name}}</h2>
<h2>学校地址:{{address}}</h2>
<button @click="sendSchoolName">点击发送学校姓名给 App</button>
</div>
</template>
<script>
export default {
name: 'School',
data() {
return {
name: '尚硅谷atguigu',
address: '北京.昌平'
}
},
props: ['getSchoolName'],
methods: {
sendSchoolName() {
this.getSchoolName(this.name)
}
}
}
</script>
<template>
<div class="student">
<h2>学生姓名:{{name}}</h2>
<h2>学生性别:{{sex}}</h2>
<h2>当前累加:{{number}}</h2>
<button @click="add">点我number++</button>
<button @click="sendStudentName">点击发送学生姓名给 App</button>
<button @click="unbind">点击解绑 App 绑定的 '获取学生姓名' 事件</button>
<button @click="goDeath">销毁当前Student组件的实例</button>
</div>
</template>
<script>
export default {
name: 'Student',
data() {
return {
name: '李四',
sex: '女',
number: 0
}
},
methods: {
add() {
this.number++
console.log(this.number)
},
sendStudentName() {
// 模拟传递多个不定参数
this.$emit('getStudentName', this.name, 555, 666, 888)
},
unbind() {
this.$off('getStudentName') // 解绑一个自定义事件
// this.$off(['m1', 'm2']) // 解绑多个自定义事件
// this.$off() // 解绑所有的自定义事件
},
goDeath() {
this.$destroy() // 销毁当前Student组件实例,后所有自定义事件失效
}
},
}
</script>
- 注意点
- 组件上也可以绑定原生DOM事件,需要使用 native 修饰符,例如
<Student @click.native="clicked" />
默认交给上层 DOM 。 - 注意:通过
this.$refs.xxx.$on('atguigu',回调)
绑定自定义事件时,回调要么配置在methods中,要么用箭头函数,否则this指向会出问题! - 自定义事件回调函数中的 this 默认是触发函数的对象实例 Student,除非使用箭头函数(无 this 默认往外找)向外找到接收回调的对象实例 App。
5.5 todo-list 改造:自定义事件
- 由 ‘:addTodo’ 传参 =》 ‘@addTodo’ 自定义事件
<template>
<div id="root">
<div class="todo-container">
<div class="todo-wrap">
<!-- <MyHeader :addTodo="addTodo"/> -->
<MyHeader @addTodo="addTodo"/>
<MyList :todos="todos" :checkTodo="checkTodo" :removeTodo="removeTodo"/>
<!-- <MyFooter :todos="todos" :reverseCheckAllTodo="reverseCheckAllTodo" :clearAllTodo="clearAllTodo"/> -->
<MyFooter :todos="todos" @reverseCheckAllTodo="reverseCheckAllTodo" @clearAllTodo="clearAllTodo"/>
</div>
</div>
</div>
</template>
- 由直接调用 =》 this.$emit 触发
<script>
import {nanoid} from 'nanoid'
export default {
name: 'MyHeader',
data() {
return {
title: ''
}
},
methods: {
add(event) {
if(!this.title.trim()) return alert('输入不能为空')
// this.addTodo({id: nanoid(), title: event.target.value, done: false})
this.$emit('addTodo', {id: nanoid(), title: event.target.value, done: false})
this.title = ''
}
}
}
</script>
<script>
export default {
name: 'MyFooter',
props: ['todos'],
computed: {
total() {
return this.todos.length
},
doneTotal() {
return this.todos.reduce((pre, current) => pre + (current.done ? 1 : 0), 0)
},
isAllChecked: {
get() {
return this.doneTotal === this.total && this.total > 0
},
set(value) {
// this.reverseCheckAllTodo(value)
this.$emit('reverseCheckAllTodo', value)
}
}
},
methods: {
clearAll() {
// this.clearAllTodo()
this.$emit('clearAllTodo')
}
}
}
</script>
* 5.6 全局事件总线
- 安装全局事件总线
new Vue ({
el: '#app',
render: h => h(App),
beforeCreate() {
// 1. 安装全局事件总线,即 vm 充当组件间消息传递的 '代理'
Vue.prototype.$bus = this
}
})
- 注册自定义事件
<script>
export default {
name: 'School',
data() {
return {
name: '尚硅谷atguigu',
address: '北京.昌平'
}
},
methods: {
getStudentName(name) {
console.log(name, this)
}
},
mounted() {
// 2. 向消息总线注册自定义事件
this.$bus.$on('getStudentName', this.getStudentName)
}
}
</script>
- 触发自定义事件
<template>
<div class="student">
<h2>学生姓名:{{name}}</h2>
<h2>学生性别:{{sex}}</h2>
<button @click="sendStudentName">发送 Student 姓名给 School</button>
</div>
</template>
<script>
export default {
name: 'Student',
data() {
return {
name: '李四',
sex: '女'
}
},
methods: {
sendStudentName() {
this.$bus.$emit('getStudentName', this.name)
}
},
}
</script>
5.7 todo-list 改造:事件总线
- 安装全局事件总线
new Vue ({
el: '#app',
render: h => h(App),
beforeCreate() {
Vue.prototype.$bus = this
}
})
- 注册自定义事件
<template>
<div id="root">
<div class="todo-container">
<div class="todo-wrap">
<MyHeader @addTodo="addTodo"/>
<MyList :todos="todos"/>
<MyFooter :todos="todos" @reverseCheckAllTodo="reverseCheckAllTodo" @clearAllTodo="clearAllTodo"/>
</div>
</div>
</div>
</template>
<script>
import MyHeader from './components/MyHeader'
import MyList from './components/MyList'
import MyFooter from './components/MyFooter'
export default {
name: 'App',
components: {MyHeader, MyList, MyFooter},
data() {
return {
todos: JSON.parse(localStorage.getItem('todos')) || []
}
},
methods: {
// 添加 todo
addTodo(newTodo) {
this.todos.unshift(newTodo)
},
// 勾选或取消勾选 todo
checkTodo(todoID) {
this.todos.forEach((todo) => {
if(todo.id === todoID) todo.done = !todo.done
})
},
// 删除 todo
removeTodo(todoID) {
this.todos = this.todos.filter(todo => todo.id !== todoID)
},
// 反转全部勾选或全不勾选
reverseCheckAllTodo(checked) {
this.todos.forEach(todo => todo.done = checked)
},
// 删除所有已经完成的 todo
clearAllTodo() {
this.todos = this.todos.filter((todo) => {
return !todo.done
})
}
},
watch: {
todos: {
deep: true,
handler(value) {
localStorage.setItem('todos', JSON.stringify(value))
}
}
},
mounted() {
this.$bus.$on('checkTodo', this.checkTodo)
this.$bus.$on('removeTodo', this.removeTodo)
},
beforeDestroy() {
this.$bus.$off(['checkTodo', 'removeTodo'])
}
}
</script>
MyList.vue 没必要再充当中间人了
<template>
<ul class="todo-main">
<MyItem
v-for="todo in todos"
:key="todo.id"
:todo="todo"
/>
</ul>
</template>
<script>
import MyItem from './MyItem'
export default {
name: 'MyList',
components: {MyItem},
props: ['todos']
}
</script>
- 触发自定义事件
<template>
<li>
<label>
<input type="checkbox" :checked="todo.done" @change="handleCheck(todo.id)"/>
<span>{{todo.title}}</span>
</label>
<button class="btn btn-danger" @click="handleDelete(todo.id)">删除</button>
</li>
</template>
<script>
export default {
name: 'MyItem',
props: ['todo'],
methods: {
handleCheck(todoID) {
// this.checkTodo(todoID)
this.$bus.$emit('checkTodo', todoID)
},
handleDelete(todoID) {
if(confirm('确认删除吗?'))
// this.removeTodo(todoID)
this.$bus.$emit('removeTodo', todoID)
}
}
}
</script>
5.8 消息订阅与发布
一种组件间通信的方式,适用于 任意组件间通信。
- 安装库依赖
PS D:\workspace\vscode\vue_test> npm i pubsub-js
...
+ pubsub-js@1.9.4
added 1 package from 1 contributor in 13.897s
...
PS D:\workspace\vscode\vue_test>
- 样例
<template>
<div class="school">
<h2>学校姓名:{{name}}</h2>
<h2>学校地址:{{address}}</h2>
</div>
</template>
<script>
// 一、库的引入
import pubsub from 'pubsub-js'
export default {
name: 'School',
data() {
return {
name: '尚硅谷atguigu',
address: '北京.昌平'
}
},
methods: {
getStudentName(msgName, data) {
console.log(msgName, data)
}
},
mounted() {
// 2. 向消息总线注册自定义事件
// this.$bus.$on('getStudentName', this.getStudentName)
// 二、订阅消息
this.pubId = pubsub.subscribe('getStudentName', this.getStudentName)
},
beforeDestroy() {
// 四、取消订阅
pubsub.unsubscribe(this.pubId)
}
}
</script>
<style scoped>
.school {
background-color: burlywood;
padding: 15px;
}
</style>
<template>
<div class="student">
<h2>学生姓名:{{name}}</h2>
<h2>学生性别:{{sex}}</h2>
<button @click="sendStudentName">发送 Student 姓名给 School</button>
</div>
</template>
<script>
import pubsub from 'pubsub-js'
export default {
name: 'Student',
data() {
return {
name: '李四',
sex: '女'
}
},
methods: {
sendStudentName() {
// 3. 触发消息总线的自定义事件
// this.$bus.$emit('getStudentName', this.name)
// 三、发布消息
pubsub.publish('getStudentName', this.name)
}
},
}
</script>
<style scoped>
.student {
background-color: blanchedalmond;
padding: 15px;
margin-top: 15px;
}
</style>
5.9 todo-list 改造:pubsub
修改 todo 的删除功能为 pubsub 模式
<script>
import MyHeader from './components/MyHeader'
import MyList from './components/MyList'
import MyFooter from './components/MyFooter'
// 1. 引入库依赖
import pubsub from 'pubsub-js'
export default {
name: 'App',
components: {MyHeader, MyList, MyFooter},
data() {
return {
todos: JSON.parse(localStorage.getItem('todos')) || []
}
},
methods: {
addTodo(newTodo) {
this.todos.unshift(newTodo)
},
checkTodo(todoID) {
this.todos.forEach((todo) => {
if(todo.id === todoID) todo.done = !todo.done
})
},
// 使用 '_' 占位
removeTodo(_, todoID) {
this.todos = this.todos.filter(todo => todo.id !== todoID)
},
reverseCheckAllTodo(checked) {
this.todos.forEach(todo => todo.done = checked)
},
clearAllTodo() {
this.todos = this.todos.filter((todo) => {
return !todo.done
})
}
},
watch: {
todos: {
deep: true,
handler(value) {
localStorage.setItem('todos', JSON.stringify(value))
}
}
},
mounted() {
this.$bus.$on('checkTodo', this.checkTodo)
// this.$bus.$on('removeTodo', this.removeTodo)
//2. 订阅消息
this.subId = pubsub.subscribe('removeTodo', this.removeTodo)
},
beforeDestroy() {
// this.$bus.$off(['checkTodo', 'removeTodo'])
this.$bus.$off('checkTodo')
// 4. 取消订阅消息
pubsub.unsubscribe(this.subId)
}
}
</script>
<template>
<li>
<label>
<input type="checkbox" :checked="todo.done" @change="handleCheck(todo.id)"/>
<span>{{todo.title}}</span>
</label>
<button class="btn btn-danger" @click="handleDelete(todo.id)">删除</button>
</li>
</template>
<script>
import pubsub from 'pubsub-js'
export default {
name: 'MyItem',
props: ['todo'],
methods: {
handleCheck(todoID) {
this.$bus.$emit('checkTodo', todoID)
},
handleDelete(todoID) {
if(confirm('确认删除吗?'))
// this.$bus.$emit('removeTodo', todoID)
// 3. 发布消息
pubsub.publish('removeTodo', todoID)
}
}
}
</script>
5.10 todo-list 编辑功能 $nextTick
<template>
<li>
<label>
<input type="checkbox" :checked="todo.done" @change="handleCheck(todo.id)"/>
<!-- 3. 根据 isEdit 决定展示哪个标签 -->
<span v-show="!todo.isEdit">{{todo.title}}</span>
<input
type="text"
v-show="todo.isEdit"
:value="todo.title"
@blur="handleBlur(todo, $event)"
ref="inputTitle">
</label>
<button class="btn btn-danger" @click="handleDelete(todo.id)">删除</button>
<!-- 1. 增加编辑按钮 -->
<button class="btn btn-edit" v-show="!todo.isEdit" @click="handleEdit(todo)">编辑</button>
</li>
</template>
<script>
import pubsub from 'pubsub-js'
export default {
name: 'MyItem',
props: ['todo'],
methods: {
handleCheck(todoID) {
this.$bus.$emit('checkTodo', todoID)
},
handleDelete(todoID) {
if(confirm('确认删除吗?'))
pubsub.publish('removeTodo', todoID)
},
// 2. 编辑的处理方法
handleEdit(todo) {
if(Object.prototype.hasOwnProperty.call(todo, 'isEdit'))
todo.isEdit = true
else
this.$set(todo, 'isEdit', true)
// vue 在方法执行完才进行 DOM 重绘,则执行该行代码时 input 框还没放在页面上
// this.$refs.inputTitle.focus()
// 使用 nextTIck,vue 会在下一次 DOM 更新结束后执行其指定的回调。
this.$nextTick(function() {
this.$refs.inputTitle.focus()
})
},
// 4. 编辑完毕后失去焦点的处理方法
handleBlur(todo, event) {
todo.isEdit = false
var newValue = event.target.value
if(!newValue.trim())
return alert('输入不能为空')
this.$bus.$emit('updateTodo', todo.id, event.target.value)
}
}
}
</script>
<script>
import MyHeader from './components/MyHeader'
import MyList from './components/MyList'
import MyFooter from './components/MyFooter'
import pubsub from 'pubsub-js'
export default {
name: 'App',
components: {MyHeader, MyList, MyFooter},
data() {
return {
todos: JSON.parse(localStorage.getItem('todos')) || []
}
},
methods: {
addTodo(newTodo) {
this.todos.unshift(newTodo)
},
checkTodo(todoID) {
this.todos.forEach((todo) => {
if(todo.id === todoID) todo.done = !todo.done
})
},
removeTodo(_, todoID) {
this.todos = this.todos.filter(todo => todo.id !== todoID)
},
reverseCheckAllTodo(checked) {
this.todos.forEach(todo => todo.done = checked)
},
clearAllTodo() {
this.todos = this.todos.filter((todo) => {
return !todo.done
})
},
// 更新 todo
updateTodo(todoID, newValue) {
this.todos.forEach((todo) => {
if(todo.id === todoID) todo.title = newValue
})
}
},
watch: {
todos: {
deep: true,
handler(value) {
localStorage.setItem('todos', JSON.stringify(value))
}
}
},
mounted() {
this.$bus.$on('checkTodo', this.checkTodo)
this.subId = pubsub.subscribe('removeTodo', this.removeTodo)
// 5. 向消息总线注册 '编辑过值更新' 的回调
this.$bus.$on('updateTodo', this.updateTodo)
},
beforeDestroy() {
this.$bus.$off('checkTodo')
pubsub.unsubscribe(this.subId)
this.$bus.$off('updateTodo')
}
}
</script>
5.11 动画
- 动画
<template>
<div>
<button @click="isShow = !isShow">显示/隐藏</button>
<!-- appear : 开头就播放动画 -->
<transition name="simple" appear>
<h1 v-show="isShow">你好啊!(动画)</h1>
</transition>
</div>
</template>
<script>
export default {
name: 'Test',
data() {
return {
isShow: true
}
},
}
</script>
<style scoped>
h1 {
background-color: orange;
}
.simple-enter-active {
animation: simple-animation 1s linear;
}
.simple-leave-active {
animation: simple-animation 1s reverse,linear;
}
@keyframes simple-animation {
from {
transform: translateX(-100%);
}
to {
transform: translateX(0px);
}
}
</style>
- 过度
<template>
<div>
<button @click="isShow = !isShow">显示/隐藏</button>
<!-- appear : 开头就播放动画 -->
<transition name="simple" appear>
<h1 v-show="isShow">你好啊!(过度)</h1>
</transition>
</div>
</template>
<script>
export default {
name: 'Test',
data() {
return {
isShow: true
}
},
}
</script>
<style scoped>
h1 {
background-color: orange;
}
/* 进入的起点 + 离开的终点 */
.simple-enter, .simple-leave-to {
transform: translateX(-100%);
}
.simple-enter-active, .simple-leave-active {
transition: 1s linear;
}
/* 进入的终点 + 离开的起点 */
.simple-enter-to, .simple-leave {
transform: translateX(0px);
}
</style>
- 多个元素
<template>
<div>
<button @click="isShow = !isShow">显示/隐藏</button>
<!-- appear : 开头就播放动画 -->
<transition-group name="simple" appear>
<h1 v-show="isShow" key="1">你好啊!(多个元素)</h1>
<h1 v-show="!isShow" key="2">吃了吗?(多个元素)</h1>
</transition-group>
</div>
</template>
- 第三方动画库
https://animate.style/
PS D:\workspace\vscode\vue_test> npm install animate.css
+ animate.css@4.1.1
added 1 package from 1 contributor in 6.521s
PS D:\workspace\vscode\vue_test>
<template>
<div>
<button @click="isShow = !isShow">显示/隐藏</button>
<!-- appear : 开头就播放动画 -->
<transition
appear
name="animate__animated animate__bounce"
enter-active-class="animate__swing"
leave-active-class="animate__bounceOutDown"
>
<h1 v-show="isShow">你好啊!(第三方动画库)</h1>
</transition>
</div>
</template>
<script>
// 1. 引入第三方动画库
import 'animate.css'
export default {
name: 'Test',
data() {
return {
isShow: true
}
},
}
</script>
<style scoped>
h1 {
background-color: orange;
}
</style>
- 总结
作用:在插入、更新或移除 DOM元素时,在合适的时候给元素添加样式类名。
- 元素进入的样式:
1. v-enter:进入的起点
2. v-enter-active:进入过程中
3. v-enter-to:进入的终点
- 元素离开的样式:
1. v-leave:离开的起点
2. v-leave-active:离开过程中
3. v-leave-to:离开的终点
5.12 todo-list 改造:动画
<template>
<transition
appear
name="animate__animated animate__bounce"
enter-active-class="animate__bounceIn"
leave-active-class="animate__bounceOut"
>
<li>
<label>
<input type="checkbox" :checked="todo.done" @change="handleCheck(todo.id)"/>
<!-- 3. 根据 isEdit 决定展示哪个标签 -->
<span v-show="!todo.isEdit">{{todo.title}}</span>
<input
type="text"
v-show="todo.isEdit"
:value="todo.title"
@blur="handleBlur(todo, $event)"
ref="inputTitle">
</label>
<button class="btn btn-danger" @click="handleDelete(todo.id)">删除</button>
<!-- 1. 增加编辑按钮 -->
<button class="btn btn-edit" v-show="!todo.isEdit" @click="handleEdit(todo)">编辑</button>
</li>
</transition>
</template>
<script>
import pubsub from 'pubsub-js'
import 'animate.css'
export default {
name: 'MyItem',
props: ['todo'],
methods: {
handleCheck(todoID) {
this.$bus.$emit('checkTodo', todoID)
},
handleDelete(todoID) {
if(confirm('确认删除吗?'))
pubsub.publish('removeTodo', todoID)
},
// 2. 编辑的处理方法
handleEdit(todo) {
if(Object.prototype.hasOwnProperty.call(todo, 'isEdit'))
todo.isEdit = true
else
this.$set(todo, 'isEdit', true)
// vue 在方法执行完才进行 DOM 重绘,则执行该行代码时 input 框还没放在页面上
// this.$refs.inputTitle.focus()
// 使用 nextTIck,vue 会在下一次 DOM 更新结束后执行其指定的回调。
this.$nextTick(function() {
this.$refs.inputTitle.focus()
})
},
// 4. 编辑完毕后失去焦点的处理方法
handleBlur(todo, event) {
todo.isEdit = false
var newValue = event.target.value
if(!newValue.trim())
return alert('输入不能为空')
this.$bus.$emit('updateTodo', todo.id, event.target.value)
}
}
}
</script>