FrontEnd笔记 -- Vue 组件间消息

五、组件间消息

5.1 todo-list 案例

在这里插入图片描述

  • 编码思路
  1. 实现静态组件:抽取组件,使用组件实现静态页面效果
  2. 展示动态数据:数据的类型、名称是什么?数据保存在哪个组件?
  3. 实现交互功能:从绑定事件监听开始
  • 实现静态组件

在这里插入图片描述

  1. 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>
  1. 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>
  1. 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>
  1. 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>
  1. 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>
  • 展示动态数据
  1. 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>
  1. 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>
  • 实现交互
  1. 添加一个 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>
  1. 动态勾选功能
<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>
  1. 删除 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>
  1. 底部统计功能
<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>
  1. 底部全部勾选及清除
<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.1 拆分静态组件:组件要按照功能点拆分,命名不要与html元素冲突。
    1.2 实现动态组件:考虑好数据的存放位置,数据是一个组件在用,还是一些组件在用:
    1.2.1 一个组件在用:放在组件自身即可。
    1.2.2 一些组件在用:放在他们共同的父组件上(状态提升)。
    1.3 实现交互:从绑定事件开始。
  2. props适用于:
    2.1 父组件 =》 子组件 通信
    2.2 子组件 =》 父组件 通信(要求父先给子一个函数)
  3. 使用v-model时要切记:v-model绑定的值不能是props传过来的值,因为props是不可以修改的!
  4. props传过来的若是对象类型的值,修改对象中的属性时Vue不会报错,但不推荐这样做。

5.2 local / sessionStorage 浏览器本地存储

  • 简介
  1. 存储内容大小一般支持5MB左右(不同浏览器可能还不一样)
  2. 浏览器端通过 Window.sessionStorage 和 Window.localStorage 属性来实现本地存储机制。
  3. 相关API:
方法说明
xxxxxStorage.setItem('key', 'value');该方法接受一个键和值作为参数,会把键值对添加到存储中,如果键名存在,则更新其对应的值。
xxxxxStorage.getItem('person');该方法接受一个键名作为参数,返回键名对应的值。
xxxxxStorage.removeItem('key');该方法接受一个键名作为参数,并把该键名从存储中删除。
xxxxxStorage.clear()该方法会清空存储中的所有数据。
  • 说明
  1. SessionStorage存储的内容会随着浏览器窗口关闭而消失。
  2. LocalStorage存储的内容,需要手动清除才会消失。
  3. xxxxxStorage.getItem(xxx)如果xxx对应的value获取不到,那么getItem的返回值是null。
  4. 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中

  • 知识点
  1. 定义 组件自定义事件;
  2. 解绑 组件自定义事件
  3. 销毁 组件实例:所有自定义事件失效。
  • 父子组件通过自定义事件传递消息
<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>
  • 注意点
  1. 组件上也可以绑定原生DOM事件,需要使用 native 修饰符,例如 <Student @click.native="clicked" /> 默认交给上层 DOM 。
  2. 注意:通过 this.$refs.xxx.$on('atguigu',回调) 绑定自定义事件时,回调要么配置在methods中,要么用箭头函数,否则this指向会出问题!
  3. 自定义事件回调函数中的 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 全局事件总线

  1. 安装全局事件总线
new Vue ({
    el: '#app',
    render: h => h(App),
    beforeCreate() {
        // 1. 安装全局事件总线,即 vm 充当组件间消息传递的 '代理'
        Vue.prototype.$bus = this
    }
})
  1. 注册自定义事件
<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>
  1. 触发自定义事件
<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 改造:事件总线

  1. 安装全局事件总线
new Vue ({
    el: '#app',
    render: h => h(App),
    beforeCreate() {
        Vue.prototype.$bus = this
    }
})
  1. 注册自定义事件
<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>
  1. 触发自定义事件
<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>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值