Vue笔记-系列
Vue笔记[一]-初识
Vue笔记[二]-组件化
Vue笔记[三]-ToDoList
Vue笔记[四]-动画、Vuex
Vue笔记[五]-路由
Vue笔记[六]-Vue3
Vite、Pinia、Router
QX-AI
GPT-4
QX-AI初始化中...
暂无预设简介,请点击下方生成AI简介按钮。
介绍自己
生成预设简介
推荐相关文章
生成AI简介

查看和运行

github仓库

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

组件化编码流程

组件化编码流程:

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

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数组

通过 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() 触发该自定义事件,并且给该事件的回调函数传入所需的参数,以此实现子组件向父组件传数据

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本地存储

使用 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')

使用全局事件总线$busA 组件需要 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

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

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() 方法实现了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进行某些操作