51工具盒子

依楼听风雨
笑看云卷云舒,淡观潮起潮落

Vue笔记[三]-ToDoList

QX-AI
GPT-4
QX-AI初始化中...
暂无预设简介,请点击下方生成AI简介按钮。
介绍自己
生成预设简介
推荐相关文章
生成AI简介

查看和运行 {#查看和运行}

github仓库

在1024code上在线查看和运行ToDoList-Vue

组件化编码流程 {#组件化编码流程}

组件化编码流程:

  1. 实现静态组件: 设计页面,根据功能区域分割页面板块设计组件,使用组件实现静态页面效果
  2. **展示动态数据:**设计每个组件应有的数据,以及数据之间的联系,将数据样例应用到组件中
  3. 交互从绑定事件监听开始

ToDoList {#ToDoList}

页面设计与组件:

将页面整体分为三大块,header(头部)、list(内容)、footer(尾部)

list 中重复的部分提取为 item 组件

实现静态组件 {#实现静态组件}

写好静态页面和样式 App.vue

|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | <template> <div id="app"> <div id="todo-container"> <div id="todo-wrap"> <ToDoHeader></ToDoHeader> <ToDoList></ToDoList> <ToDoFooter></ToDoFooter> </div> </div> </div> </template> <script> import ToDoHeader from "./components/ToDoHeader.vue"; import ToDoList from "./components/ToDoList.vue"; import ToDoFooter from "./components/ToDoFooter.vue"; export default { name: "App", components: { ToDoHeader, ToDoList, ToDoFooter, }, }; </script> <style lang="less"> @todo-border: 1px solid #ccc; @todo-border-radius: 6px; * { margin: 0; padding: 0; box-sizing: border-box; } ul { list-style: none; } #app { margin: 20px 10px; font-size: 16px; } #todo-container { max-width: 600px; min-width: 300px; height: auto; margin: 0 auto; padding: 10px; border: @todo-border; border-radius: @todo-border-radius; #todo-wrap { width: 100%; } } </style> |

ToDoHeader.vue

|---------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | <template> <div class="todo-header"> <input type="text" placeholder="回车新增ToDo" /> <button>新增</button> </div> </template> <script> export default { name: "ToDoHeader", }; </script> <style lang="less" scoped> @todo-border: 1px solid #ccc; @todo-border-radius: 6px; .input-focus { box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6); border-color: rgba(82, 168, 236, 0.8); outline: none; } .todo-header { display: flex; flex-direction: row; flex-wrap: nowrap; margin-bottom: 15px; input { width: 100%; font-size: 16px; padding: 8px 10px; border: @todo-border; border-radius: @todo-border-radius; &:focus { .input-focus(); } } button { margin-left: 10px; width: 65px; border-radius: @todo-border-radius; border: @todo-border; background: rgb(52, 201, 238); font-size: 16px; color: #fff; } } </style> |

ToDoList.vue

|------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | <template> <div class="todo-list"> <ul> <ListItem></ListItem> <ListItem></ListItem> <ListItem></ListItem> <ListItem></ListItem> </ul> </div> </template> <script> import ListItem from "./ListItem.vue"; export default { name: "ToDoList", components: { ListItem, }, }; </script> <style lang="less" scoped> @todo-border: 1px solid #ccc; @todo-border-radius: 6px; .todo-list { margin-bottom: 15px; border-radius: @todo-border-radius; border: @todo-border; } </style> |

ListItem.vue

|------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | <template> <li> <label> <input type="checkbox"> <span>这是一个ToDo</span> </label> <button class="list-item-btn">删除</button> </li> </template> <script> export default { name: 'ListItem', } </script> <style lang="less" scoped> @todo-border: 1px solid #ccc; @todo-border-radius: 6px; li { border-bottom: @todo-border; display: flex; height: 38px; line-height: 37px; justify-content: space-between; &:last-child{ border-bottom: none; } label { margin-left: 10px; input { top: -1px; position: relative; vertical-align: middle; margin-right: 10px; } } button { border-radius: @todo-border-radius; border: @todo-border; font-size: 14px; margin: 5px; padding: 0 5px; background: #dc7878; color: #fff; } } </style> |

ToDoFooter.vue

|------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | <template> <div id="todo-footer"> <input type="checkbox"> <div class="statistics">已完成0 / 全部4</div> <button>清除已完成</button> </div> </template> <script> export default { name: 'ToDoFooter', } </script> <style lang="less" scoped> @todo-border: 1px solid #ccc; @todo-border-radius: 6px; #todo-footer { width: 100%; height: 40px; line-height: 40px; padding-left: 10px; input { top: 1px; position: relative; } .statistics { width: fit-content; display: inline-block; margin-left: 20px; } button { float: right; padding: 5px 10px; margin: 5px 0; border-radius: @todo-border-radius; border: @todo-border; color: #fff; background: rgb(220, 120, 120); font-size: 15px; } } </style> |

设计数据 {#设计数据}

数据在 List 中展示,所以下面操作 ToDoList.vue 组件

有许多todo,而每个todo都有许多属性,如内容、是否完成、以及唯一标识该 todo 的id

所以数据的结构应该是这样:

|---------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | todos: [ { id: "001", title: "第一件事", complete: false }, { id: "002", title: "第二件事", complete: true }, { id: "003", title: "第三件事", complete: false } ], } |

有了动态的数据,使用 v-for 遍历数据,将原来写死的模板替换掉,

并且将遍历的每个 todo 通过组件标签 传给子组件 ToDoItem.vue ToDoList.vue

|---------------|--------------------------------------------------------------------------------------------| | 1 2 3 | <ul> <ListItem v-for="item in todos" :key="item.id" :todo="item"></ListItem> </ul> |

子组件使用 props 接收父组件传来的 todo 进行内容展示 ToDoItem.vue

|---------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | <template> <li> <label> <input type="checkbox" :checked="todo.complete"> <span>{{ todo.title }}</span> </label> <button class="list-item-btn">删除</button> </li> </template> <script> export default { name: 'ListItem', props: ['todo'], } </script> |

完善添加功能 {#完善添加功能}

vue 是以数据驱动的,所以不应该直接操控dom,而是让数据发生变化,触发视图的更新

添加一个todo分为两步:

  1. 按下回车或按钮时获取输入框的内容
  2. 将获取到的内容添加到 ToDoList.vue 组件 data 中的 todos 数组中

获取输入框内容 {#获取输入框内容}

这里还是要写点原生js代码的,但不多 ToDoHeader.vue

|------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | <template> <div class="todo-header"> <input type="text" placeholder="回车新增ToDo" @keyup.enter="addToDo"/> <button @click="addToDo">新增</button> </div> </template> <script> import {nanoid} from "nanoid"; export default { name: "ToDoHeader", methods: { addToDo(e){ // 获取input框元素 let input = e.target.tagName === "INPUT" ? e.target : e.target.parentNode.firstElementChild // 输入框为空返回 if(input.value === ""){ return } // 生成数据对象 let todo = { // 正常来说数据由后端返回,id由数据库维护,这里使用nanoid生成唯一id id: nanoid(), title: input.value, complete: false } // 清空input input.value = ""; console.log(todo); } } }; </script> |

修改todos数组 {#修改todos数组}

通过 props 子组件能接收父组件传来的数据,但现在 ToDoHeader.vueToDoList.vue 是兄弟组件

实现兄弟组件之间的通信有很多种办法:全局事件总线、消息对外发布、vuex等

这些高级的办法后面再学,现在先用一种老办法:让兄弟组件共同的父组件(App.vue)去管理数据(todos),通过 props 传数据给子组件使用,父组件通过传一个函数来接收子组件的数据(让子组件在合适时候调用该接口函数,并传入父组件所需的数据)

修改 App.vue: App.vue

|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | <template> <div id="app"> <div id="todo-container"> <div id="todo-wrap"> <!-- 将接口函数传给子组件 --> <ToDoHeader :receive="receive"></ToDoHeader> <!-- 将todo数据传给list组件使用 --> <ToDoList :todos="todos"></ToDoList> <ToDoFooter></ToDoFooter> </div> </div> </div> </template> <script> import ToDoHeader from "./components/ToDoHeader.vue"; import ToDoList from "./components/ToDoList.vue"; import ToDoFooter from "./components/ToDoFooter.vue"; export default { name: "App", components: { ToDoHeader, ToDoList, ToDoFooter, }, data(){ return { todos: [ { id: "001", title: "第一件事", complete: false }, { id: "002", title: "第二件事", complete: true }, { id: "003", title: "第三件事", complete: false } ], } }, methods: { // 接口函数,让子组件在合适时候调用,并传入父组件所需的数据 receive(value){ // 操控数组数据 this.todos.unshift(value); } } }; </script> |

自定义事件与$emit() {#自定义事件与-emit}

子组件调用父组件的函数,以向父组件传数据 ,更规范的做法是用组件的自定义事件

用法: 父组件给子组件实例绑定一个自定义事件 ,并且指定回调 (父组件中的函数),子组件在合适的时候 使用 $emit() 触发该自定义事件,并且给该事件的回调函数传入所需的参数,以此实现子组件向父组件传数据

|------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | <!-- 父组件绑定一个事件 --> <ToDoHeader @receive="receive" ></ToDoHeader> <script> export default { methods: { // 事件回调函数 receive(value){ // 操控数组数据 this.todos.unshift(value); } } } </script> <!-- 子组件也有一个自己的事件 --> <button @click="addToDo">新增</button> <script> export default { methods: { addToDo(e){ let todo = {} // 去触发父组件的receive事件,并向receive事件的回调函数传入todo对象作为参数 this.$emit('receive',todo) } } } </script> |

当一个子组件要绑定多个 事件时,在组件标签上写太多绑定就过于臃肿了,可以使用 $refs 获取子组件实例对象,再在父组件生命周期 mounted() 上用 $on() 绑定自定义事件

并且这样动态绑定 事件更加灵活

注意: 绑定在组件标签上的事件都会被当做自定义事件,所以若要绑定原生事件 (如click),需加上 native 修饰符

|---------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | <ToDoHeader ref="ToDoHeader" @click.native="alter('原生事件')"></ToDoHeader> <script> export default { methods: { // 事件回调函数 receive(value){ // 操控数组数据 this.todos.unshift(value); } }, mounted(){ // 等效于<ToDoHeader @receive="receive" ></ToDoHeader> this.$refs.ToDoHeader.$on('receive',this.receive); } } </script> |

注意: 看起来是子组件触发父组件的回调,但 $emit 底层并不是触发 ,而是冒泡

有时要在合适时刻用vc 上的 $off(<事件名>) 解绑事件,解绑多个事件则传入事件名数组 ,若不传入参数则解绑所有事件

完善勾选功能 {#完善勾选功能}

**功能:**页面中勾选 todo 后,修改对应数据的 complete 属性,以表示 todo 是否已完成

**实现方式:**点击勾选后,拿到该 todo 的 id 值,传给管数据的 App.vue,找到对应的 todo,让其 complete 取反

由于ListItem是App的孙子,所以要先通过 $emit() 将数据传给父组件(ToDoList),再由父组件传给爷爷组件(App)

|---------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | <!-- App.vue中,updateComplete接收id并对complete取反 --> <ToDoList :todos="todos" @updateComplete="updateComplete"></ToDoList> <script> updateComplete(id){ // 遍历找到对应的todo this.todos.forEach((item)=>{ if(item.id === id){ // complete取反 item.complete = !item.complete; } }); } </script> <!-- ToDoList.vue中receiveId将子组件传过来的id再传给App,起中转效果 --> <ListItem v-for="item in todos" :key="item.id" :todo="item" @receiveId="receiveId"></ListItem> <script> receiveId(id){ this.$emit('updateComplete', id) } </script> <!-- ListItem.vue中 --> <input type="checkbox" :checked="todo.complete" @change="changeComplete(todo.id)"> <script> changeComplete(id){ // 由于ListItem是App的孙子,所以要先通过$emit()将数据传给父组件,再由父组件传给爷爷组件 this.$emit('receiveId', id) } </script> |

完善删除功能 {#完善删除功能}

**功能:**点击每个todo对应的删除按钮就在数据中删除该todo

实现起来和勾选功能差不多,也是逐层传id给App,再遍历查找删除

|---------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | <!-- App.vue中 --> <ToDoList :todos="todos" @updateComplete="updateComplete" @deleteToDo="deleteToDo"></ToDoList> <script> deleteToDo(id) { this.todos.forEach((item, index, arr) => { if (item.id === id) { arr.splice(index, 1) } }); } </script> <!-- ToDoList.vue中 --> <ListItem v-for="item in todos" :key="item.id" :todo="item" @receiveId="receiveId" @deleteToDo="deleteToDo"></ListItem> <script> deleteToDo(id){ this.$emit('deleteToDo', id) } </script> <!-- ListItem.vue中 --> <button class="list-item-btn" @click="deleteToDo(todo.id)">删除</button> <script> deleteToDo(id) { if (!confirm('确认删除吗?')) { return } this.$emit('deleteToDo', id) } </script> |

完善底部 {#完善底部}

**功能:**全选按钮、已完成统计、清除所有已完成按钮

**实现:**使用计算属性并遵循数据驱动,结合之前的父子组件通信方法很容易实现 ToDoFooter.vue

|------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | <template> <div id="todo-footer"> <div v-show="this.todos.length"> <input type="checkbox" v-model="checkValue"> <div class="statistics">已完成{{ doneToDo }} / 全部{{ todos.length }}</div> <button @click="deleteComplete">清除已完成</button> </div> <div class="tip" v-show="!this.todos.length">没有ToDo</div> </div> </template> <script> export default { name: 'ToDoFooter', props: ['todos'], computed: { // 计算已完成数量 doneToDo() { // 通过reduce统计已完成 return this.todos.reduce((pre, item) => { return pre + (item.complete ? 1 : 0) }, 0) }, // 全选按钮的状态 checkValue: { // checkValue无论是设置还是确定值,都要通过数据驱动,遍历所有数据来确定todo是否全部完成 get() { // 比较已完成数量和全部todo数量,且todo至少要有一个才打勾 return this.doneToDo === this.todos.length && this.todos.length > 0; }, // 将changeAll()移到此处,点击checkbox后通过changeAll()修改数据后,自动触发计算属性的getter并更新视图 set() { this.changeAll() return } } }, methods: { // 全选或全不选,即全都变为目前全选按钮状态的反状态 changeAll() { // 因为现在checkValue是计算属性,checkbox点击变化后的未来值要通过"非"体现 // 数据变化,checkValue计算属性也自动触发getter更新视图,v-model更新checked this.$emit('changeAll', !this.checkValue) }, // 删除所有已完成项 deleteComplete() { this.$emit('deleteComplete') } } } </script> |

App.vue

|------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | <ToDoFooter :todos="todos" @changeAll="changeAll" @deleteComplete="deleteComplete"></ToDoFooter> <script> // 全选或全不选,即全都变为目前全选按钮状态的反状态 changeAll(checkValue){ this.todos.forEach((item) => { if(item.complete !== checkValue) { item.complete = checkValue; } }); }, // 删除所有已完成项 deleteComplete(){ this.todos = this.todos.filter((item) => { return !item.complete; }); } </script> |

ToDo本地存储 {#ToDo本地存储}

使用 localStorage 保存todos,初始化todos,本地存储有则拿,没有则赋值空数组

watch 监视 todos 的变化,数据多层开启深度监视,只要数据发生改变就更新本地存储的数据 App.vue

|------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | export default { data() { return { // 初始化todos,本地存储有则拿,没有则赋值空数组 todos: JSON.parse(localStorage.getItem("todos")) || [], } }, watch: { // 监视todos的变化 todos: { // 数据多层开启深度监视 deep: true, handler(value) { // 只要数据发生改变就更新本地存储的数据 localStorage.setItem("todos", JSON.stringify(value)); } } } }; |

全局事件总线 {#全局事件总线}

在之前,使用 props 、自定义事件、$emit() 来实现父组件子组件的通信(数据传输)

现在可以通过全局事件总线 实现任意组件之间的通信

实现全局事件总线并不需要学习新的API,本质还是通过自定义事件来实现的,是一种经验

实现:
1、总线: vm 和所有 vc 都能获取的东西,所以要将其放在 Vue 的原型
2、事件总线: 这个总线应用要能够调用 $on() 绑定各种事件,能够调用 $emit() 触发事件,因为 $on()$emit() 两个方法都在 Vue 原型 上,所以使用现成的 vm 作为总线即可,在生命周期 beforeCreate() 将 vm 放到 Vue.prototype 上,Vue.prototype.$bus = this

|---------------------|-----------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 | new Vue({ render: h => h(App), beforeCreate(){ Vue.prototype.$bus = this;// 安装全局事件总线,this指的就是vm } }).$mount('#app') |

使用全局事件总线$bus A 组件需要 B 组件传来某些数据,则 A 组件调用 $bus$on() 方法绑定一个自定义事件 ,事件的回调函数写在 A组件中,然后在 B 组件中调用 $bus$emit() 方法,触发对应的事件,并传入 A 组件所需的数据,于是 A 组件就能通过回调函数收到 B 组件传来数据

|---------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | // A组件 export default { name: 'A' methods: { // 接收数据的方法 receive(data){ console.log(`收到了数据:${data}`); } }, mounted(){ // 在全局事件总线上发布一个事件 this.$bus.$on('receive', this.receive) } } // B组件 export default { name: 'B' methods: { // 在合适时刻调用该方法,通过$bus发送数据给A组件 sendOut(){ let data = 'chuckle' // 触发receive事件,将data传给该事件的回调函数 this.$bus.$emit('receive', data) } } } |

简单得说,就是在 Vue 原型上的 $bus 绑定了一堆自定义事件,通过自定义事件的回调接收与触发,就实现了任意组件间的通信

当然,若事件较多,可以不使用vm作为全局事件总线,不同类型组件的通信,单独 new 一个 vc 出来当事件总线

注意: 规范起见,每个组件应该在生命周期 beforeDestroy() 时解绑事件

|---------------|--------------------------------------------------------| | 1 2 3 | beforeDestroy(){ this.$bus.$off('<要解绑的事件名>') } |

Vue3中没有Vue(),而是createApp(),且$on $off $once都已废弃,所以Vue3中使用全局事件总线要安装第三方库:mitt

TodoList使用$bus {#TodoList使用-bus}

ListItem 组件通过全局事件总线,直接和管理 todos 数据的 App 组件通信,不再经过 ToDoList 组件

修改 App.vue

|------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | <!-- 不再给ToDoList绑定事件 --> <ToDoList :todos="todos"></ToDoList> <script> export default { name: "App", /* ······ */ mounted(){ // 在全局事件总线上发布事件 this.$bus.$on('updateComplete', this.updateComplete) this.$bus.$on('deleteToDo', this.deleteToDo) } } </script> |

修改 ListItem.vue 的相关函数

|------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 | changeComplete(id) { // 触发事件传入id this.$bus.$emit('updateComplete', id) }, deleteToDo(id) { if (!confirm('确认删除吗?')) { return } // 触发事件传入id this.$bus.$emit('deleteToDo', id) } |

消息发布与订阅 {#消息发布与订阅}

消息发布与订阅 可以实现任意组件间的通信

提供 数据的组件发布消息接收 数据的组件订阅该消息

vue并不自带消息库,原生js实现起来也麻烦,所以一般调用第三方库,这里使用 pubsub-js

安装: npm i pubsub-js

使用: subscribe() 订阅消息、publish() 发布消息、unsubscribe() 根据id取消订阅

|------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | // 引入pubsub-js库 import pubsub from 'pubsub-js'; // 订阅消息,返回一个唯一的订阅的id const subID = pubsub.subscribe('<消息名>', (msgName, data)=>{ // 消息回调函数传入两个参数 // 第一个是消息名,第二个是发布消息时传入的数据 console.log(data); }) // 发布消息 pubsub.publish('<消息名>', '<数据>') // 根据id取消订阅 pubsub.unsubscribe(subID) |

对比全局事件总线,两者非常相似

TodoList使用pubsub {#TodoList使用pubsub}

ListItemApp 组件通信,将每个todo的删除功能改为消息发布与订阅实现,勾选功能仍然保留全局事件总线的实现,方便对比学习

修改 App.vue 的相关函数

|------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | export default { name: "App", /* ······ */ mounted(){ this.$bus.$on('updateComplete', this.updateComplete) // this.$bus.$on('deleteToDo', this.deleteToDo) // 订阅消息 pubsub.subscribe('deleteToDo', (magName, data)=>{ magName; // eslint不允许不使用参数,这里使用一下 // 将接收到的数据(要删除的id)传给删除函数 this.deleteToDo(data) }) } } |

修改 ListItem.vue 的相关函数

|---------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 | deleteToDo(id) { if (!confirm('确认删除吗?')) { return } // 触发事件传入id // this.$bus.$emit('deleteToDo', id) // 发布消息 pubsub.publish('deleteToDo', id); } |

添加编辑功能 {#添加编辑功能}

这个功能较为综合

**功能:**点击编辑按钮,title变成可编辑的input框并获取焦点,编辑按钮变取消按钮,并显示保存按钮,输入框失去焦点后也能保存修改

修改 App.vue 相关函数

|------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | methods: { // 修改todo的title editToDo(id,value){ this.todos.forEach((item, index, arr) => { if (item.id === id) { arr[index].title = value; } }); } } mounted(){ // 订阅修改todo的消息 pubsub.subscribe('editToDo', (magName,arr)=>{ magName; this.editToDo(arr[0],arr[1]) }) } |

大改 ListItem.vue

|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 | <template> <li> <label> <input v-show="!isEdit" type="checkbox" :checked="todo.complete" @change="changeComplete(todo.id)"> <span v-show="!isEdit" ref="titleToDo">{{ todo.title }}</span> <input v-show="isEdit" class="todo-title-input" type="text" :value="todo.title" ref="titleInput" @blur="editToDo(todo.id)" contenteditable="true"> </label> <div class="btn-box"> <button v-show="isEdit" class="list-item-btn" @click="editToDo(todo.id)">保存</button> <button v-show="isEdit" class="list-item-btn" @click="completeEdit()">取消</button> <button v-show="!isEdit" class="list-item-btn" @click="edit()">编辑</button> <button class="btn-delete" @click="deleteToDo(todo.id)">删除</button> </div> </li> </template> <script> import pubsub from "pubsub-js"; export default { name: 'ListItem', props: ['todo'], data() { return { // 是否在修改,控制输入框显隐 isEdit: false, } }, methods: { changeComplete(id) { // 触发事件传入id this.$bus.$emit('updateComplete', id) }, deleteToDo(id) { if (!confirm('确认删除吗?')) { return } // 触发事件传入id // this.$bus.$emit('deleteToDo', id) // 发布消息 pubsub.publish('deleteToDo', id); }, // 进行修改 edit() { this.isEdit = true; // $nextTick的回调函数会在dom节点更新之后再执行 this.$nextTick(()=>{ // 让输入框获取焦点 this.$refs.titleInput.focus(); }); }, // 完成修改(取消也是完成) completeEdit() { this.isEdit = false; this.ifEdit = false; }, // 修改todo并发生消息 editToDo(id) { this.completeEdit() // 获取input框元素 let input = this.$refs.titleInput; // 如果输入框内容没有变则返回 if (input.value === this.todo.title) return; // 如果输入框内容为空提示为空并返回 if (input.value.trim() === "") { alert('ToDo内容为空!请重新修改') return; } pubsub.publish('editToDo', [id, input.value]); } } } </script> |

$nextTick {#nextTick}

在编辑功能中,通过 $nextTick() 方法实现了input框出现时自动获取焦点

**作用:**在下一次DOM更新结束后执行其指定的回调

何时使用: 当改变数据后,要基于更新后的新DOM进行某些操作时,要在 $nextTick() 的回调函数中进行操作

|---------------------------|--------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 | edit() { this.isEdit = true; // $nextTick的回调函数会在dom节点更新之后再执行 this.$nextTick(()=>{ // 让输入框获取焦点 this.$refs.titleInput.focus(); }); } |

也可以不使用这个 API,直接包裹一个没设定时间的定时器,由于事件循环的机制,也能成功对更新后的新DOM进行某些操作

赞(1)
未经允许不得转载:工具盒子 » Vue笔记[三]-ToDoList