Composition API
1.Composition API
接下来我们来介绍一下Vue3中新增的Composition API如何使用。注意Composition API仅仅是Vue3中新增的API,我们依然可以使用Options API。先来实现一下之前演示的获取鼠标位置的案例。做这个案例之前,需要先介绍一下createApp这个函数,这里不借助任何的构建工具,直接使用浏览器中原生的ES Module的方式来加载Vue模块。注意,这里我们会使用vue.esm.browser.js完整版的Vue。
首先,安装Vue3.0,创建createApp.html文件。
createApp.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
x: {{ position.x }} <br>
y: {{ position.y }} <br>
</div>
<script type="module">
import { createApp } from './node_modules/vue/dist/vue.esm-browser.js'
/*
* createApp的作用是创建一个vue对象,它可以接受一个选项作为参数,也就是一个组件的选项,
* 跟Vue2中给Vue构造函数传入的选项一样,它可以传入data、methods、create、computer等选项。
*
* */
const app = createApp({
data() {
return {
position: {
x: 0,
y: 0,
}
}
}
})
console.log(app)
app.mount('#app')
</script>
</body>
</html>
打开浏览器,可以看到打印出来的app对象。

这里可以看到X和Y都可以正常响应式,然后打开开发人员工具来看一下刚刚打印的vue对象,把这个对象展开,这里的成员要比Vue2中的vue对象的成员要少很多,而且这些成员都没有使用$开头,说明未来我们基本不用给这个对象上新增成员。这里面可以看到component 、directive、 mixin还有use。它和以前的使用方式都是一样的,mount和过去的$mount的作用类似,还有一个unmount,它类似于过去的$destroyed。
Composition API还是在选项的这个位置来书写,不过要用到一个新的选项,叫做setup,setup函数是Composition API的入口。
setup执行的时机
它是在
prop被解析完毕,但是在组件实例被创建之前执行的,所以在setup内部无法通过this获取到组件的实例,因为组件实例还未被创建,所以在setup中也无法访问到组件中的data、computed、methods,setup的内部的this此时指向的是undefined。
VUe3中提供了一个新的API,让我们来创建响应式对象,这个函数是reactive,在使用reactive之前,先要导入这个函数,在import的后边我们直接来导入reactive。导入完成之后,在setup中就可以使用reactive函数来创建响应式对象,
reactive函数的作用是把一个对象转换成响应式对象,并且该对象的嵌套属性也都会转换成响应式对象,它返回的是一个proxy对象。
createApp.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
x: {{ position.x }} <br>
y: {{ position.y }} <br>
</div>
<script type="module">
import { createApp, reactive } from './node_modules/vue/dist/vue.esm-browser.js'
/*
* createApp的作用是创建一个vue对象,它可以接受一个选项作为参数,也就是一个组件的选项,
* 跟Vue2中给Vue构造函数传入的选项一样,它可以传入data、methods、create、computer等选项。
*
* */
const app = createApp({
setup() {
// 第一个参数 props,props的作用是用来接收外部传入的参数,并且prop是一个响应式的对象,它不能被解构.
// 第二个参数 context,它具有三个成员,分别是attrs、emit、slots
// setUp需要返回一个对象,可以使用在template、methods compute以及生命周期的钩的函数中
const position = reactive({
x: 0,
y: 0,
})
return {
position
}
},
mounted() {
this.position.x = 100
}
})
console.log(app)
app.mount('#app')
</script>
</body>
</html>

2.生命周期钩子函数
接下来再来演示如何在setup中使用生命周期的钩子函数。先来回顾一下上面的案例,这里的响应式数据已经搞定了,下面需要注册鼠标移动的事件,当鼠标移动的时候响应式当前鼠标的位置。当这个组件被卸载的时候,这个鼠标移动的事件要被移除,注册mousemove事件可以在mounted来实现,但是别忘了最终的目标是要让获取鼠标位置的整个逻辑,封装到一个函数中,这样任何组件都可以重用。这个时候使用mounted的选项就不合适了。其实我们在setup中也可以使用生命周期的钩子函数。
在setup函数中可以使用组件生命周期中的钩子函数,但是需要在生命周期钩子函数前面加上on,然后首字母大写,比如选项中的mounted,在setup中对应的这个函数是onMounted。
另外,setup是在组件初始化之前执行的,是在beforeCreate和created之间执行的,所以在beforeCreate和created中的代码都可以放在setup函数中,这里的beforeCreate和created不需要在setup中有对应的实现。
下面的这些选项中的勾子函数的写法对应在setup中的实线,分别是在前面加上on,然后首字母大写。
注意这里的unmounted,它类似于之前的destroyed。还有下面的renderTracked、renderTriggered的这两个钩子函数非常相似,都是在render函数被重新调用的时候触发的。那它们不同的是renderTracked是在首次调用render的时候也会触发。renderTriggered在首次定用的时候不会触发。

createApp.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
x: {{ position.x }} <br>
y: {{ position.y }} <br>
</div>
<script type="module">
import { createApp, reactive, onMounted, onUnmounted } from './node_modules/vue/dist/vue.esm-browser.js'
/*
* createApp的作用是创建一个vue对象,它可以接受一个选项作为参数,也就是一个组件的选项,
* 跟Vue2中给Vue构造函数传入的选项一样,它可以传入data、methods、create、computer等选项。
*
* */
function useMousePosition() {
// 第一个参数 props,props的作用是用来接收外部传入的参数,并且prop是一个响应式的对象,它不能被解构.
// 第二个参数 context,它具有三个成员,分别是attrs、emit、slots
// setUp需要返回一个对象,可以使用在template、methods compute以及生命周期的钩的函数中
const position = reactive({
x: 0,
y: 0,
})
const update = e => {
position.x = e.pageX
position.y = e.pageY
}
// 注册事件
onMounted(() => {
window.addEventListener('mousemove', update)
})
// 移除事件
onUnmounted(() => {
window.removeEventListener('mousemove', update)
})
return position
}
const app = createApp({
setup() {
const position = useMousePosition()
return {
position
}
},
})
console.log(app)
app.mount('#app')
</script>
</body>
</html>
3.reactive-toRefs-ref
接下来再来介绍Composition API中的三个函数,reactive、toRefs还有ref,这三个函数都是创建响应式数据的。
先从一个问题看起,先来看一下刚刚案例中使用的reactive函数的一个小问题。当我们不希望在模板中使用position.x和position.y,而是只是用x和y。可以在setup函数中解构useMousePosition的返回值。const { x, y } = useMousePosition,
并且直接在setup中返回x和y。
createApp.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
x: {{ x }} <br>
y: {{ y }} <br>
</div>
<script type="module">
import { createApp, reactive, onMounted, onUnmounted } from './node_modules/vue/dist/vue.esm-browser.js'
/*
* createApp的作用是创建一个vue对象,它可以接受一个选项作为参数,也就是一个组件的选项,
* 跟Vue2中给Vue构造函数传入的选项一样,它可以传入data、methods、create、computer等选项。
*
* */
function useMousePosition() {
// 第一个参数 props,props的作用是用来接收外部传入的参数,并且prop是一个响应式的对象,它不能被解构.
// 第二个参数 context,它具有三个成员,分别是attrs、emit、slots
// setUp需要返回一个对象,可以使用在template、methods compute以及生命周期的钩的函数中
const position = reactive({
x: 0,
y: 0,
})
const update = e => {
position.x = e.pageX
position.y = e.pageY
}
// 注册事件
onMounted(() => {
window.addEventListener('mousemove', update)
})
// 移除事件
onUnmounted(() => {
window.removeEventListener('mousemove', update)
})
return position
}
const app = createApp({
setup() {
const { x, y } = useMousePosition()
return {
x,
y
}
},
})
console.log(app)
app.mount('#app')
</script>
</body>
</html>
此时,打开浏览器,发现鼠标移动,页面上的x和y并没有随着变化。这是为什么呢?
这里来解释一下,这里的position是响应式对象,因为我们在useMousePosition中去借用了reactive函数将传入的position对象包装成了proxy对象。将来访问position的x和y的时候,会调用代理对象proxy中的getter拦截收集依赖,当x和y变化之后,会调用代理对象proxy中的setter进行拦截触发更新。
当把代理对象解构的时候,当把position代理对象解构的时候,就相当于定义了x和y两个变量来接收position.x和position.y,而基本类型的赋值就是把值在内存中复制一份,所以这里的x和y就是两个基本类型的变量,跟代理对象无关。当重新给x和y赋值的时候,也不会调用代理对象的setter,无法触发更新的操作,所以不能对当前的响应式对象进行解构。
3.1 toRefs
如果我们就想像刚刚那么做呢?这里来介绍一个新的API,叫做toRefs,先演示它如何使用。
导入toRefs,在useMousePosition返回position时,使用toRefs包裹position。
createApp.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
x: {{ x }} <br>
y: {{ y }} <br>
</div>
<script type="module">
import { createApp, reactive, onMounted, onUnmounted, toRefs } from './node_modules/vue/dist/vue.esm-browser.js'
/*
* createApp的作用是创建一个vue对象,它可以接受一个选项作为参数,也就是一个组件的选项,
* 跟Vue2中给Vue构造函数传入的选项一样,它可以传入data、methods、create、computer等选项。
*
* */
function useMousePosition() {
// 第一个参数 props,props的作用是用来接收外部传入的参数,并且prop是一个响应式的对象,它不能被解构.
// 第二个参数 context,它具有三个成员,分别是attrs、emit、slots
// setUp需要返回一个对象,可以使用在template、methods compute以及生命周期的钩的函数中
const position = reactive({
x: 0,
y: 0,
})
const update = e => {
position.x = e.pageX
position.y = e.pageY
}
// 注册事件
onMounted(() => {
window.addEventListener('mousemove', update)
})
// 移除事件
onUnmounted(() => {
window.removeEventListener('mousemove', update)
})
return toRefs(position) // 可以把响应式对象中的所有属性,都转为响应式对象
}
const app = createApp({
setup() {
const { x, y } = useMousePosition()
return {
x,
y
}
},
})
console.log(app)
app.mount('#app')
</script>
</body>
</html>
现在来解释原因,toRefs要求我们传入的这个参数必须是一个代理对象proxy,当前的position就是我们reactive返回的一个代理对象,如果的position不是代理对象的话,它会报警告,提示需要传递代理对象。
接下来它内部会创建一个新的对象,然后遍历传入的这个代理对象的所有属性,把所有属性的值都转换成响应式对象。注意toRefs里边是把position这个对象的所有属性的值都转换成响应式对象,然后再挂载到新创建的对象上,最后把这个新创建的对象返回。
它内部会为代理对象的每一个属性创建一个具有value属性的对象,该对象是响应式的value属性,具有getter和setter,这一点和下面要讲的ref函数类似。getter里面返回代理对象中对应属性的值,setter中给代理对象的属性赋值,所以返回的每一个属性都是响应式的。
toRefs这个函数的作用就是把对象的每一个属性都转换成响应式数据,所以可以解构toRefs返回的对象,解构的每一个属性也都是响应式。下边在解构的时候,解构的是toRefs这个函数返回的新的对象,这个对象的所有属性都是响应式对象,并且这个属性是一个对象,它有一个value属性,在模板中使用的时候可以把这个value省略,但是我们在代码中去写的时候,这个value是不可以去省略的,我们稍后的时候会去演示。
3.2 ref
接下来再来介绍一个响应式的API,叫做ref,这是一个函数,它的作用是把普通数据转换成响应式数据。和reactive不同的是,reactive是把一个对象转换成响应式数据。ref可以把基本类型的数据包装成响应式对象。
ref.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<button @click="increase">按钮</button>
<span>{{ count }}</span>
</div>
<script type="module">
import { createApp, ref } from './node_modules/vue/dist/vue.esm-browser.js'
function useCount () {
const count = ref(0)
return {
count,
increase: () => {
count.value++
}
}
}
createApp({
setup () {
return {
...useCount()
}
}
}).mount('#app')
</script>
</body>
</html>
下面来解释一下ref内部做了什么?首先基本数据类型,它存储的是值,所以它不可能是响应式数据,我们知道响应式数据要通过该收集依赖通过setter触发更新。ref的参数它如果是对象,内部会调用reactive返回一个代理对象,也就是如果调用ref的时候,参数传递的是对象的话,它内部其实就是调用reactive。ref的参数如果是基本类型的值,比如我们传入的是0,它内部会创建一个只有value属性的对象,该对象的value属性具有getter和setter,在getter中收集依赖在setter中触发更新。
4.computed
接下来我们再来介绍几个API。首先来看计算属性,计算属性的作用是简化模板中的代码,可以缓存计算的结果,当数据变化后才会重新计算。
我们依然可以向Vue2.x的时候,在创建组件的时候传入computed的选项来创建计算属性。在Vue3中,也可以在setup中通过computed的函数来创建计算属性。computed的函数有两种用法,第一种是传入一个获取值的函数,函数内部依赖响应式的数据,当依代的数据发生变化后,会重新执行该函数获取数据。computed的函数返回一个不可变的响应式对象,类似于使用ref创建的对象只有一个value属性。
获取计算属性的值要通过value属性来获取,模板中使用计算属性可以省略value, computed的第二种用法是传入一个对象,这个对象具有getter和setter,返回一个不可变的响应式对象。例如下面这段代码,当获取值的时候会触发这个对象的getter,当设置值的时候会触发这个对象的setter。

这里已经创建好了一个页面,并且做了一些准备工作。我们这里放了一个按钮,当点击按钮的时候,会利用push方法创建一个待办事项,下面去显示未完成的代办事项个数。
computed.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<button @click="push">按钮</button>
未完成:{{ activeCount }}
</div>
<script type="module">
import { createApp, reactive, computed } from './node_modules/vue/dist/vue.esm-browser.js'
const data = [
{ text: '看书', completed: false },
{ text: '敲代码', completed: false },
{ text: '约会', completed: true }
]
createApp({
setup () {
const todos = reactive(data)
const activeCount = computed(() => {
return todos.filter(item => !item.completed).length
})
return {
activeCount,
push: () => {
todos.push({
text: '开会',
completed: false
})
}
}
}
}).mount('#app')
</script>
</body>
</html>
点击页面中的按钮后,可以发现未完成的数量+1。当往todos中添加一项的时候,todos变化了,计算属性中传入的函数会重新执行,重新获取未完成的待办事项的个数。
computed可以创建一个响应式的数据,这个显示的数据依赖于其他响应式的数据,当依赖类的数据发生变化后,会重新计算属性传入的这个函数。
5.watch
接下来我们再来看侦听器。和computed类似,在setup函数中可以使用watch来创建一个侦听器,它的使用方式和之前使用this.$watch或者选项中的watch作用是一样的,监听响应式数据的变化,然后执行一个相应的回调函数,可以获取到监听数据的新值和旧值。
- Watch的三个参数
- 要监听的数据
- 监听到数据变化后执行的函数,这个函数有两个参数分别是新值和旧值
- 选项对象,deep和immediate
- Watch的返回值
- 取消监听的函数
下面来介绍一下要写的这个案例。这个案例来自vue的官网,这是一个选择困难症必备的应用,可以在这个文本框中输入一个只需要回答是和否的问题,然后会发送一个请求,请求这个接口,它会随机返回一个yes or no。这个接口除了返回yes no之外,还会随机返回一个好玩的图片,如果你需要的话,可以把图片也展示出来。
watch.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<p>
请问一个 yes/no 的问题:
<input v-model="question">
</p>
<p>{{ answer }}</p>
</div>
<script type="module">
// https://www.yesno.wtf/api
import { createApp, ref, watch } from './node_modules/vue/dist/vue.esm-browser.js'
createApp({
setup () {
const question = ref('')
const answer = ref('')
watch(question, async (newValue, oldValue) => {
const response = await fetch('https://www.yesno.wtf/api')
const data = await response.json()
answer.value = data.answer
})
return {
question,
answer
}
}
}).mount('#app')
</script>
</body>
</html>
模板中用到了question和answer,所以需要在setup中定义并且返回question和answer ,question和answer都是响应式的,因为他们都是自符串,所以这里可以使用ref来创建响应式的对象。定义question和answer ,question就等于通过ref来创建我们这个响应式对象,question里边其实存储的就是一个字符串,接下来再创建一个answer。ref返回的对象是不可变的,可以改变这个对象的value属性。
当用户在文本框中输入问题以后,也就是question的值发生变化之后,这个时候要发送请求,获取答案给answer赋值,所以这里我们要监听question的变化,我们要用到watch。watch函数的第一个参数是ref或者reactive返回的对象,要监听question,注意第一个参数,这个位置跟以前不一样,我们在Vue2使用this.$watch的时候,第一个参数是字符串。然后是回调函数,这个回调函数可以接收新值和旧值。第三个参数我们暂时不需要。
当值变化之后,我们要去请求这个接口,那这里我们直接用fetch来发送请求。
fetch返回的是一个promise对象,所以这块可以用async、await来简化调用。当拿到response之后,要解析出来里边的答案,给answer去赋值,给answer赋值的时候,注意要给answer的value属性来赋值,注意response.json()它返回的也是一个promise对象。在setup的最后,还要把question和answer返回。
打开浏览器来测试一下。

watch使用起来和过去的this.$watch是一样的,不一样的是第一个参数不是字符串,而是ref或者返回的对象。
6.watchEffect
在Vue3中还提供了一个新的函数watchEffect,它其实就是watch函数的简化版本。内部实现是和watch调用的同一个函数doWatch,不同的是,watchEffect没有第二个回调函数的参数。watchEffect接受一个函数作为参数,它会监听这个函数内部使用的响应式数据的变化,它会立即执行一次这个函数,当数据变化之后会重新运行该函数。它也返回一个取消监听的函数。
- 是watch函数的简化版本,也用来监视数据的变化
- 接受一个函数作为参数,监听函数内响应式数据的变化
watchEffect.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<button @click="increase">increase</button>
<button @click="stop">stop</button>
<br>
{{ count }}
</div>
<script type="module">
import { createApp, ref, watchEffect } from './node_modules/vue/dist/vue.esm-browser.js'
createApp({
setup () {
const count = ref(0)
const stop = watchEffect(() => {
console.log(count.value)
})
return {
count,
stop,
increase: () => {
count.value++
}
}
}
}).mount('#app')
</script>
</body>
</html>
watchEffect中的函数初始的时候首先会执行一次,当count的值发生变化的时候,这个函数会再次被调用,另外watchEffect会返回一个函数,取消对数据的监视。点击stop后,再次点击increase,发现模板中的数据在增加,而控制台并没有再进行输出。
后续的案例中我们会使用watchEffect监听数据的变化,当数据变化后,把变化的数据存储到localStory中,那这个时候使用watchEffect会非常的方便。

7.todolist-功能演示
接下来我们来做一个案例todoList,todoList是代办事项清单,平时工作或者学习的时候也应该有一个自己的代办事项清单,它是一个非常经典的案例,学习一个新的技术后,可以通过这个案例快速来巩固所学的知识。
todoList需要实现以下功能:
- 添加待办事项
- 删除待办事项
- 编辑待办事项
- 切换待办事项
- 存储待办事项
8.todolist-项目结构
之前的案例都是直接在网页上引用vue模块,接下来在做todoList案例的时候,使用vue的脚手架来创建项目。首先我们升级vue-cli到4.5以上的版本,新版本在创建项目的时候可以选择使用view3.0,vue-cli使用的方式和之前是一样的,先使用vue create创建项目,创建项目的时候选择vue3.0。
全局安装/升级@vue/cli:npm install -g @vue/cli
vue create my-todolist
vue create my-todolist
使用Vue3
已经创建好了项目,并把组件和样式文件都提前设置好了。那下面我们来看一下页面的结构,然后一个功能一个功能来实现。

App.vue
<template>
<section id="app" class="todoapp">
<header class="header">
<h1>todos</h1>
<input
class="new-todo"
placeholder="What needs to be done?"
autocomplete="off"
autofocus
>
</header>
<section class="main">
<input id="toggle-all" class="toggle-all" type="checkbox">
<label for="toggle-all">Mark all as complete</label>
<ul class="todo-list">
<li>
<div class="view">
<input class="toggle" type="checkbox">
<label>测试数据</label>
<button class="destroy"></button>
</div>
<input type="text" class="edit">
</li>
<li>
<div class="view">
<input class="toggle" type="checkbox">
<label>测试数据</label>
<button class="destroy"></button>
</div>
<input type="text" class="edit">
</li>
</ul>
</section>
<footer class="footer">
<span class="todo-count">
<strong>1</strong>item left
</span>
<ul class="filters">
<li><a href="#/all">All</a></li>
<li><a href="#/active">Active</a></li>
<li><a href="#/completed">Completed</a></li>
</ul>
<button class="clear-completed" v-show="count > remainingCount">
Clear completed
</button>
</footer>
</section>
<footer class="info">
<p>Double-click to edit a todo</p>
<!-- Remove the below line ↓ -->
<p>Template by <a href="http://sindresorhus.com">Sindre Sorhus</a></p>
<!-- Change this out with your name and url ↓ -->
<p>Created by <a href="https://www.lagou.com">教瘦</a></p>
<p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
</footer>
</template>
<script>
import './assets/index.css'
export default {
name: 'App',
setup () {
},
}
</script>
<style>
</style>
运行yarn serve查看效果

9.todolist-添加待办事项
<template>
<section id="app" class="todoapp">
<header class="header">
<h1>todos</h1>
<input
class="new-todo"
placeholder="What needs to be done?"
autocomplete="off"
autofocus
v-model="input"
@keyup.enter="addTodo"
>
</header>
<section class="main">
<input id="toggle-all" class="toggle-all" type="checkbox">
<label for="toggle-all">Mark all as complete</label>
<ul class="todo-list">
<li
v-for="todo in todos"
:key="todo.text"
>
<div class="view">
<input class="toggle" type="checkbox">
<label>{{ todo.text }}</label>
<button class="destroy"></button>
</div>
<input type="text" class="edit">
</li>
</ul>
</section>
<footer class="footer">
<span class="todo-count">
<strong>1</strong>item left
</span>
<ul class="filters">
<li><a href="#/all">All</a></li>
<li><a href="#/active">Active</a></li>
<li><a href="#/completed">Completed</a></li>
</ul>
<button class="clear-completed">
Clear completed
</button>
</footer>
</section>
<footer class="info">
<p>Double-click to edit a todo</p>
<!-- Remove the below line ↓ -->
<p>Template by <a href="http://sindresorhus.com">Sindre Sorhus</a></p>
<!-- Change this out with your name and url ↓ -->
<p>Created by <a href="https://www.lagou.com">教瘦</a></p>
<p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
</footer>
</template>
<script>
import './assets/index.css'
import { ref } from 'vue'
// 1.添加待办事项
const useAdd = todos => {
const input = ref('') // 创建响应式数据
const addTodo = () => {
const text = input.value && input.value.trim() // 获取输入框的值,并去除前后空格
if (text.length === 0) return
// 添加到todos数组中,用于模板渲染
todos.value.unshift({
text,
completed: false
})
// 回车后,清空输入框
input.value = ''
}
// 返回响应式数据和方法
return {
input,
addTodo
}
}
export default {
name: 'App',
setup() {
// 初始化todos
const todos = ref([])
return {
...useAdd(todos),
todos
}
}
}
</script>
<style>
</style>
查看效果

到这我们的第一个任务添加待办事项就完成了。
10.todolist-删除待办事项
<template>
<section id="app" class="todoapp">
<header class="header">
<h1>todos</h1>
<input
class="new-todo"
placeholder="What needs to be done?"
autocomplete="off"
autofocus
v-model="input"
@keyup.enter="addTodo"
>
</header>
<section class="main">
<input id="toggle-all" class="toggle-all" type="checkbox">
<label for="toggle-all">Mark all as complete</label>
<ul class="todo-list">
<li
v-for="todo in todos"
:key="todo.text"
>
<div class="view">
<input class="toggle" type="checkbox">
<label>{{ todo.text }}</label>
<button class="destroy" @click="remove(todo)"></button>
</div>
<input type="text" class="edit">
</li>
</ul>
</section>
<footer class="footer">
<span class="todo-count">
<strong>1</strong>item left
</span>
<ul class="filters">
<li><a href="#/all">All</a></li>
<li><a href="#/active">Active</a></li>
<li><a href="#/completed">Completed</a></li>
</ul>
<button class="clear-completed">
Clear completed
</button>
</footer>
</section>
<footer class="info">
<p>Double-click to edit a todo</p>
<!-- Remove the below line ↓ -->
<p>Template by <a href="http://sindresorhus.com">Sindre Sorhus</a></p>
<!-- Change this out with your name and url ↓ -->
<p>Created by <a href="https://www.lagou.com">教瘦</a></p>
<p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
</footer>
</template>
<script>
import './assets/index.css'
import { ref } from 'vue'
// 1.添加待办事项
const useAdd = todos => {
const input = ref('')
const addTodo = () => {
const text = input.value && input.value.trim()
if (text.length === 0) return
todos.value.unshift({
text,
completed: false
})
input.value = ''
}
return {
input,
addTodo
}
}
// 2.删除待办事项
const useRemove = todos => {
const remove = todo => {
const index = todos.value.indexOf(todo) // 找到todo在todos中的索引
todos.value.splice(index, 1) // 删除改元素
}
return {
remove
}
}
export default {
name: 'App',
setup() {
const todos = ref([])
return {
todos,
...useAdd(todos),
...useRemove(todos)
}
},
}
</script>
<style>
</style>
11.todolist-编辑待办事项
- 双击待办项,展示编辑文本框
- 按回车或者编辑文本框失去焦点,修改数据
- 按esc取消编辑
- 把编辑文本框清空按回车,删除这一项·显示编辑文本框的时候获取焦点
App.vue
<template>
<section id="app" class="todoapp">
<header class="header">
<h1>todos</h1>
<input
class="new-todo"
placeholder="What needs to be done?"
autocomplete="off"
autofocus
v-model="input"
@keyup.enter="addTodo"
>
</header>
<section class="main">
<input id="toggle-all" class="toggle-all" type="checkbox">
<label for="toggle-all">Mark all as complete</label>
<ul class="todo-list">
<li
v-for="todo in todos"
:key="todo"
:class="{ editing: todo === editingTodo }"
>
<div class="view">
<input class="toggle" type="checkbox">
<label @dblclick="editTodo(todo)">{{ todo.text }}</label>
<button class="destroy" @click="remove(todo)"></button>
</div>
<input
type="text"
class="edit"
v-model="todo.text"
@keyup.enter="doneEdit(todo)"
@blur="doneEdit(todo)"
@keyup.esc="cancelEdit(todo)"
>
</li>
</ul>
</section>
<footer class="footer">
<span class="todo-count">
<strong>1</strong>item left
</span>
<ul class="filters">
<li><a href="#/all">All</a></li>
<li><a href="#/active">Active</a></li>
<li><a href="#/completed">Completed</a></li>
</ul>
<button class="clear-completed">
Clear completed
</button>
</footer>
</section>
<footer class="info">
<p>Double-click to edit a todo</p>
<!-- Remove the below line ↓ -->
<p>Template by <a href="http://sindresorhus.com">Sindre Sorhus</a></p>
<!-- Change this out with your name and url ↓ -->
<p>Created by <a href="https://www.lagou.com">教瘦</a></p>
<p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
</footer>
</template>
<script>
import './assets/index.css'
import { ref } from 'vue'
// 1.添加待办事项
const useAdd = todos => {
const input = ref('')
const addTodo = () => {
const text = input.value && input.value.trim()
if (text.length === 0) return
todos.value.unshift({
text,
completed: false
})
input.value = ''
}
return {
input,
addTodo
}
}
// 2.删除待办事项
const useRemove = todos => {
const remove = todo => {
const index = todos.value.indexOf(todo) // 找到todo在todos中的索引
todos.value.splice(index, 1)
}
return {
remove
}
}
// 3.编辑待办项
const useEdit = (remove) => {
let beforeEditingText = '' // 保存编辑前的文本框的值
const editingTodo = ref(null) // 记录当前是否是编辑状态
// 编辑todo
const editTodo = (todo) => {
beforeEditingText = todo.text
editingTodo.value = todo
}
// 完成编辑todo
const doneEdit = (todo) => {
if (!editingTodo.value) return
todo.text = todo.text.trim()
todo.text || remove(todo)
editingTodo.value = null
}
// 取消编辑todo
const cancelEdit = todo => {
editingTodo.value = null
todo.text = beforeEditingText
}
return {
editingTodo,
editTodo,
doneEdit,
cancelEdit
}
}
export default {
name: 'App',
setup() {
const todos = ref([])
const { remove } = useRemove(todos)
return {
todos,
remove,
...useAdd(todos),
...useEdit(remove)
}
},
}
</script>
<style>
</style>
此时还没有处理“自动获得焦点”
12.todolist-编辑待办事项-编辑文本框获取焦点
下面来实现在编辑代办事项的时候让编辑文本框块获得焦点。这里需要用到自定义指令,Vue3中自定义指令的使用方式和Vue2中稍有不同。先来介绍一下Vue2和Vue3中自定义指令的差别。主要是自定义指令的钩子函数被重命名,Vue3把钩子函数的名称和组件中钩子函数的名称保持一致,这样很容易理解,但是自定义指令的钩子函数和组件钩子函数的执行方式是不一样的。
Vue3中的钩子函数的名称,它有三组,分别是mount、update还有unmounted,分别是在自定义指令修饰的元素被挂载到DOM树、更新、卸载的时候执行。这是自定义指令的第一种用法,在创建自定义指令的时候还可以传函数,这种用法比较简洁,更常用一些。第二个参数是函数的时候,Vue3和Vue2的用法是一样的。
指定名称后面的这个函数在Vue3的时候是在mounted和update的时候去执行,跟Vue2的执行实际其实是一样的。Vue2里这个函数是在bind和update的时候执行,那这个函数的el参数是我们指令所绑定的那个元素,binding这个参数可以获取到指定对应的值,通过binding.value来获取。


<template>
<section id="app" class="todoapp">
<header class="header">
<h1>todos</h1>
<input
class="new-todo"
placeholder="What needs to be done?"
autocomplete="off"
autofocus
v-model="input"
@keyup.enter="addTodo"
>
</header>
<section class="main">
<input id="toggle-all" class="toggle-all" type="checkbox">
<label for="toggle-all">Mark all as complete</label>
<ul class="todo-list">
<li
v-for="todo in todos"
:key="todo"
:class="{ editing: todo === editingTodo }"
>
<div class="view">
<input class="toggle" type="checkbox">
<label @dblclick="editTodo(todo)">{{ todo.text }}</label>
<button class="destroy" @click="remove(todo)"></button>
</div>
<input
type="text"
class="edit"
v-editing-focus="todo === editingTodo"
v-model="todo.text"
@keyup.enter="doneEdit(todo)"
@blur="doneEdit(todo)"
@keyup.esc="cancelEdit(todo)"
>
</li>
</ul>
</section>
<footer class="footer">
<span class="todo-count">
<strong>1</strong>item left
</span>
<ul class="filters">
<li><a href="#/all">All</a></li>
<li><a href="#/active">Active</a></li>
<li><a href="#/completed">Completed</a></li>
</ul>
<button class="clear-completed">
Clear completed
</button>
</footer>
</section>
<footer class="info">
<p>Double-click to edit a todo</p>
<!-- Remove the below line ↓ -->
<p>Template by <a href="http://sindresorhus.com">Sindre Sorhus</a></p>
<!-- Change this out with your name and url ↓ -->
<p>Created by <a href="https://www.lagou.com">教瘦</a></p>
<p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
</footer>
</template>
<script>
import './assets/index.css'
import { ref } from 'vue'
// 1.添加待办事项
const useAdd = todos => {
const input = ref('')
const addTodo = () => {
const text = input.value && input.value.trim()
if (text.length === 0) return
todos.value.unshift({
text,
completed: false
})
input.value = ''
}
return {
input,
addTodo
}
}
// 2.删除待办事项
const useRemove = todos => {
const remove = todo => {
const index = todos.value.indexOf(todo) // 找到todo在todos中的索引
todos.value.splice(index, 1)
}
return {
remove
}
}
// 3.编辑待办项
const useEdit = (remove) => {
let beforeEditingText = '' // 保存编辑前的文本框的值
const editingTodo = ref(null) // 记录当前是否是编辑状态
// 编辑todo
const editTodo = (todo) => {
beforeEditingText = todo.text
editingTodo.value = todo
}
// 完成编辑todo
const doneEdit = (todo) => {
if (!editingTodo.value) return
todo.text = todo.text.trim()
todo.text || remove(todo)
editingTodo.value = null
}
// 取消编辑todo
const cancelEdit = todo => {
editingTodo.value = null
todo.text = beforeEditingText
}
return {
editingTodo,
editTodo,
doneEdit,
cancelEdit
}
}
export default {
name: 'App',
setup() {
const todos = ref([])
const { remove } = useRemove(todos)
return {
todos,
remove,
...useAdd(todos),
...useEdit(remove)
}
},
directives: {
editingFocus: (el, binding) => {
binding.value && el.focus()
}
}
}
</script>
<style>
</style>

13.todolist-切换待办事项-演示效果

- 点击checkbox,改变所有待办项状态
- All/Active/Completed
- 其它
- 显示未完成待办项个数
- 移除所有完成的项目
- 如果没有待办项,隐藏 main和 footer
14.todolist-切换待办事项-改变待办事项完成状态
接下来开始来做切换待办事项状态的第一个子任务,点击checkbox,改变所有待办事项的完成状态。
<template>
<section id="app" class="todoapp">
<header class="header">
<h1>todos</h1>
<input
class="new-todo"
placeholder="What needs to be done?"
autocomplete="off"
autofocus
v-model="input"
@keyup.enter="addTodo"
>
</header>
<section class="main">
<input id="toggle-all" class="toggle-all" type="checkbox" v-model="allDone">
<label for="toggle-all">Mark all as complete</label>
<ul class="todo-list">
<li
v-for="todo in todos"
:key="todo"
:class="{ editing: todo === editingTodo, completed: todo.completed }"
>
<div class="view">
<input class="toggle" type="checkbox" v-model="todo.completed">
<label @dblclick="editTodo(todo)">{{ todo.text }}</label>
<button class="destroy" @click="remove(todo)"></button>
</div>
<input
type="text"
class="edit"
v-editing-focus="todo === editingTodo"
v-model="todo.text"
@keyup.enter="doneEdit(todo)"
@blur="doneEdit(todo)"
@keyup.esc="cancelEdit(todo)"
>
</li>
</ul>
</section>
<footer class="footer">
<span class="todo-count">
<strong>1</strong>item left
</span>
<ul class="filters">
<li><a href="#/all">All</a></li>
<li><a href="#/active">Active</a></li>
<li><a href="#/completed">Completed</a></li>
</ul>
<button class="clear-completed">
Clear completed
</button>
</footer>
</section>
<footer class="info">
<p>Double-click to edit a todo</p>
<!-- Remove the below line ↓ -->
<p>Template by <a href="http://sindresorhus.com">Sindre Sorhus</a></p>
<!-- Change this out with your name and url ↓ -->
<p>Created by <a href="https://www.lagou.com">教瘦</a></p>
<p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
</footer>
</template>
<script>
import './assets/index.css'
import { computed, ref } from 'vue'
// 1.添加待办事项
const useAdd = todos => {
const input = ref('')
const addTodo = () => {
const text = input.value && input.value.trim()
if (text.length === 0) return
todos.value.unshift({
text,
completed: false
})
input.value = ''
}
return {
input,
addTodo
}
}
// 2.删除待办事项
const useRemove = todos => {
const remove = todo => {
const index = todos.value.indexOf(todo) // 找到todo在todos中的索引
todos.value.splice(index, 1)
}
return {
remove
}
}
// 3.编辑待办项
const useEdit = (remove) => {
let beforeEditingText = '' // 保存编辑前的文本框的值
const editingTodo = ref(null) // 记录当前是否是编辑状态
// 编辑todo
const editTodo = (todo) => {
beforeEditingText = todo.text
editingTodo.value = todo
}
// 完成编辑todo
const doneEdit = (todo) => {
if (!editingTodo.value) return
todo.text = todo.text.trim()
todo.text || remove(todo)
editingTodo.value = null
}
// 取消编辑todo
const cancelEdit = todo => {
editingTodo.value = null
todo.text = beforeEditingText
}
return {
editingTodo,
editTodo,
doneEdit,
cancelEdit
}
}
// 4.切换待办项完成状态
const useFilter = (todos) => {
const allDone = computed({
get() {
return !todos.value.filter(todo => !todo.completed).length
},
set(value) {
todos.value.forEach(todo => {
todo.completed = value
})
}
})
return {
allDone
}
}
export default {
name: 'App',
setup() {
const todos = ref([])
const { remove } = useRemove(todos)
return {
todos,
remove,
...useAdd(todos),
...useEdit(remove),
...useFilter(todos)
}
},
directives: {
editingFocus: (el, binding) => {
binding.value && el.focus()
}
}
}
</script>
<style>
</style>
15.todolist-切换待办事项-切换状态
接下来再来实现切换待办事项状态的第二个子任务,点击all、active、completed个超链接的时候,查看不同状态的待办事项。
在模板中,先找到三个超链接的位置,超链接的href属性是三个锚点,这里不使用路由功能,自己来实现。首先要监视地址中hash的变化,当组件挂载完毕,要注册hashChange事件。当组件卸载的时候,要把hashChange事件移除。在hashChange事件中,要获取当前锚点的值,只需要这里的单词all、active、completed,所以可以把前面的#号杠去掉。然后再根据hash来判断当前要获取哪种状态的待办事项列表。
把过滤代办事项数据的函数定义到一个对象,然后根据hash的值去对象中获取对应的函数,当然函数的名称跟hash值是一样的,这是核心思路,这样写的话,就避免了写一堆if语句。
<template>
<section id="app" class="todoapp">
<header class="header">
<h1>todos</h1>
<input
class="new-todo"
placeholder="What needs to be done?"
autocomplete="off"
autofocus
v-model="input"
@keyup.enter="addTodo"
>
</header>
<section class="main">
<input id="toggle-all" class="toggle-all" type="checkbox" v-model="allDone">
<label for="toggle-all">Mark all as complete</label>
<ul class="todo-list">
<li
v-for="todo in filteredTodos"
:key="todo"
:class="{ editing: todo === editingTodo, completed: todo.completed }"
>
<div class="view">
<input class="toggle" type="checkbox" v-model="todo.completed">
<label @dblclick="editTodo(todo)">{{ todo.text }}</label>
<button class="destroy" @click="remove(todo)"></button>
</div>
<input
type="text"
class="edit"
v-editing-focus="todo === editingTodo"
v-model="todo.text"
@keyup.enter="doneEdit(todo)"
@blur="doneEdit(todo)"
@keyup.esc="cancelEdit(todo)"
>
</li>
</ul>
</section>
<footer class="footer">
<span class="todo-count">
<strong>1</strong>item left
</span>
<ul class="filters">
<li><a href="#/all">All</a></li>
<li><a href="#/active">Active</a></li>
<li><a href="#/completed">Completed</a></li>
</ul>
<button class="clear-completed">
Clear completed
</button>
</footer>
</section>
<footer class="info">
<p>Double-click to edit a todo</p>
<!-- Remove the below line ↓ -->
<p>Template by <a href="http://sindresorhus.com">Sindre Sorhus</a></p>
<!-- Change this out with your name and url ↓ -->
<p>Created by <a href="https://www.lagou.com">教瘦</a></p>
<p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
</footer>
</template>
<script>
import './assets/index.css'
import { computed, onMounted, onUnmounted, ref } from 'vue'
// 1.添加待办事项
const useAdd = todos => {
const input = ref('')
const addTodo = () => {
const text = input.value && input.value.trim()
if (text.length === 0) return
todos.value.unshift({
text,
completed: false
})
input.value = ''
}
return {
input,
addTodo
}
}
// 2.删除待办事项
const useRemove = todos => {
const remove = todo => {
const index = todos.value.indexOf(todo) // 找到todo在todos中的索引
todos.value.splice(index, 1)
}
return {
remove
}
}
// 3.编辑待办项
const useEdit = (remove) => {
let beforeEditingText = '' // 保存编辑前的文本框的值
const editingTodo = ref(null) // 记录当前是否是编辑状态
// 编辑todo
const editTodo = (todo) => {
beforeEditingText = todo.text
editingTodo.value = todo
}
// 完成编辑todo
const doneEdit = (todo) => {
if (!editingTodo.value) return
todo.text = todo.text.trim()
todo.text || remove(todo)
editingTodo.value = null
}
// 取消编辑todo
const cancelEdit = todo => {
editingTodo.value = null
todo.text = beforeEditingText
}
return {
editingTodo,
editTodo,
doneEdit,
cancelEdit
}
}
// 4.切换待办项完成状态
const useFilter = (todos) => {
const allDone = computed({
get() {
return !todos.value.filter(todo => !todo.completed).length
},
set(value) {
todos.value.forEach(todo => {
todo.completed = value
})
}
})
const filter = {
all: list => list,
active: list => list.filter(todo => !todo.completed),
completed: list => list.filter(todo => todo.completed),
}
const type = ref('all') // 保存type
const filteredTodos = computed(() => filter[type.value](todos.value)) // 计算属性
const onHashChange = () => {
const hash = window.location.hash.replace('#/', '')
console.log('hash',hash)
if (filter[hash]) {
console.log(11)
type.value = hash
} else {
type.value = 'all'
window.location.hash = ''
}
}
onMounted(() => {
window.addEventListener('hashchange', onHashChange)
onHashChange()
})
onUnmounted(() => {
window.removeEventListener('hashchange', onHashChange)
})
return {
allDone,
filteredTodos
}
}
export default {
name: 'App',
setup() {
const todos = ref([])
const { remove } = useRemove(todos)
return {
todos,
remove,
...useAdd(todos),
...useEdit(remove),
...useFilter(todos)
}
},
directives: {
editingFocus: (el, binding) => {
binding.value && el.focus()
}
}
}
</script>
<style>
</style>
16.todolist-切换待办事项-其它
- 显示待办事项个数
- 删除已完成的待办事项
<template>
<section id="app" class="todoapp">
<header class="header">
<h1>todos</h1>
<input
class="new-todo"
placeholder="What needs to be done?"
autocomplete="off"
autofocus
v-model="input"
@keyup.enter="addTodo"
>
</header>
<section class="main" v-show="count">
<input id="toggle-all" class="toggle-all" type="checkbox" v-model="allDone">
<label for="toggle-all">Mark all as complete</label>
<ul class="todo-list">
<li
v-for="todo in filteredTodos"
:key="todo"
:class="{ editing: todo === editingTodo, completed: todo.completed }"
>
<div class="view">
<input class="toggle" type="checkbox" v-model="todo.completed">
<label @dblclick="editTodo(todo)">{{ todo.text }}</label>
<button class="destroy" @click="remove(todo)"></button>
</div>
<input
type="text"
class="edit"
v-editing-focus="todo === editingTodo"
v-model="todo.text"
@keyup.enter="doneEdit(todo)"
@blur="doneEdit(todo)"
@keyup.esc="cancelEdit(todo)"
>
</li>
</ul>
</section>
<footer class="footer" v-show="count">
<span class="todo-count">
<strong>{{ remainingCount }}</strong> {{ remainingCount > 1 ? "items" : "item" }} left
</span>
<ul class="filters">
<li><a href="#/all" :class="{active : type === 'all'}">All</a></li>
<li><a href="#/active" :class="{active : type === 'active'}">Active</a></li>
<li><a href="#/completed" :class="{active : type === 'completed'}">Completed</a></li>
</ul>
<button class="clear-completed" @click="removeCompleted" v-show="count > remainingCount">
Clear completed
</button>
</footer>
</section>
<footer class="info">
<p>Double-click to edit a todo</p>
<!-- Remove the below line ↓ -->
<p>Template by <a href="http://sindresorhus.com">Sindre Sorhus</a></p>
<!-- Change this out with your name and url ↓ -->
<p>Created by <a href="https://www.lagou.com">教瘦</a></p>
<p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
</footer>
</template>
<script>
import './assets/index.css'
import { computed, onMounted, onUnmounted, ref } from 'vue'
// 1.添加待办事项
const useAdd = todos => {
const input = ref('')
const addTodo = () => {
const text = input.value && input.value.trim()
if (text.length === 0) return
todos.value.unshift({
text,
completed: false
})
input.value = ''
}
return {
input,
addTodo
}
}
// 2.删除待办事项
const useRemove = todos => {
const remove = todo => {
const index = todos.value.indexOf(todo) // 找到todo在todos中的索引
todos.value.splice(index, 1)
}
// 删除已完成的待办事项
const removeCompleted = () => {
todos.value = todos.value.filter(todo => !todo.completed)
}
return {
remove,
removeCompleted
}
}
// 3.编辑待办项
const useEdit = (remove) => {
let beforeEditingText = '' // 保存编辑前的文本框的值
const editingTodo = ref(null) // 记录当前是否是编辑状态
// 编辑todo
const editTodo = (todo) => {
beforeEditingText = todo.text
editingTodo.value = todo
}
// 完成编辑todo
const doneEdit = (todo) => {
if (!editingTodo.value) return
todo.text = todo.text.trim()
todo.text || remove(todo)
editingTodo.value = null
}
// 取消编辑todo
const cancelEdit = todo => {
editingTodo.value = null
todo.text = beforeEditingText
}
return {
editingTodo,
editTodo,
doneEdit,
cancelEdit
}
}
// 4.切换待办项完成状态
const useFilter = (todos) => {
const allDone = computed({
get() {
return !todos.value.filter(todo => !todo.completed).length
},
set(value) {
todos.value.forEach(todo => {
todo.completed = value
})
}
})
const filter = {
all: list => list,
active: list => list.filter(todo => !todo.completed),
completed: list => list.filter(todo => todo.completed),
}
const type = ref('all') // 保存type
const filteredTodos = computed(() => filter[type.value](todos.value)) // 计算属性
const remainingCount = computed(() => filter.active(todos.value).length)
const count = computed(() => todos.value.length)
const onHashChange = () => {
const hash = window.location.hash.replace('#/', '')
if (filter[hash]) {
type.value = hash
} else {
type.value = 'all'
window.location.hash = ''
}
}
onMounted(() => {
window.addEventListener('hashchange', onHashChange)
onHashChange()
})
onUnmounted(() => {
window.removeEventListener('hashchange', onHashChange)
})
return {
allDone,
filteredTodos,
remainingCount,
count,
type
}
}
export default {
name: 'App',
setup() {
const todos = ref([])
const { remove, removeCompleted } = useRemove(todos)
return {
todos,
remove,
removeCompleted,
...useAdd(todos),
...useEdit(remove),
...useFilter(todos)
}
},
directives: {
editingFocus: (el, binding) => {
binding.value && el.focus()
}
}
}
</script>
<style>
</style>
17.todolist-存储待办事项
接下来我们来实现todo list案例的最后一个功能,把待办事项存储到localStorage中,防止刷新的时候丢失数据,当数据修改的时候要把数据存储到localStorage,下次加载的时候再从本地存储中把数据还原。
接下来要操作本地存储,可以把操作本地存储的代码分装到一个模块中,这是一个通用的模块,将来在其他组件中也可以使用。
utils/useLocalStorage.js
function parse(str) {
let value
try {
value = JSON.parse(str)
} catch (e) {
value = null
}
return value
}
function strginify(obj) {
let value
try {
value = JSON.stringify(obj)
} catch (e) {
value = null
}
return value
}
export default function useLocalStorage() {
function setItem(key, value) {
value = strginify(value)
window.localStorage.setItem(key, value)
}
function getItem(key) {
let value = window.localStorage.getItem(key)
if (value) {
value = parse(value)
}
return value
}
return {
setItem,
getItem
}
}
准备工作都做好了,下面来想一下这个功能如何实现。当todos中数据变化的时候,需要把变化后的数据存储到本地存储中,要调用setItem,当添加数据或者删除数据或者编辑数据的时候,都会引起todos的变化,所以要去修改useAdd、useRemove、useEdit,这样太麻烦了,有没有简单一点的办法呢?
这个时候可以想到watchEffect,它可以监视数据的变化,如果数据改变了,可以执行相应的操作。还有当页面首次加载的时候,要首先从本地存储中获取数据,如果没有数据的话,可以初始化成一个空数组。所有的这些操作可以再封装到一个函数中。
通过composition API实现这个案例,把不同的逻辑代码拆分到不同的use函数中,同一功能的代码只存在一个函数中,而且更方便组件之间重用代码,这是composition API比options API好的地方。
<template>
<section id="app" class="todoapp">
<header class="header">
<h1>todos</h1>
<input
class="new-todo"
placeholder="What needs to be done?"
autocomplete="off"
autofocus
v-model="input"
@keyup.enter="addTodo"
>
</header>
<section class="main" v-show="count">
<input id="toggle-all" class="toggle-all" type="checkbox" v-model="allDone">
<label for="toggle-all">Mark all as complete</label>
<ul class="todo-list">
<li
v-for="todo in filteredTodos"
:key="todo"
:class="{ editing: todo === editingTodo, completed: todo.completed }"
>
<div class="view">
<input class="toggle" type="checkbox" v-model="todo.completed">
<label @dblclick="editTodo(todo)">{{ todo.text }}</label>
<button class="destroy" @click="remove(todo)"></button>
</div>
<input
type="text"
class="edit"
v-editing-focus="todo === editingTodo"
v-model="todo.text"
@keyup.enter="doneEdit(todo)"
@blur="doneEdit(todo)"
@keyup.esc="cancelEdit(todo)"
>
</li>
</ul>
</section>
<footer class="footer" v-show="count">
<span class="todo-count">
<strong>{{ remainingCount }}</strong> {{ remainingCount > 1 ? "items" : "item" }} left
</span>
<ul class="filters">
<li><a href="#/all" :class="{active : type === 'all'}">All</a></li>
<li><a href="#/active" :class="{active : type === 'active'}">Active</a></li>
<li><a href="#/completed" :class="{active : type === 'completed'}">Completed</a></li>
</ul>
<button class="clear-completed" @click="removeCompleted" v-show="count > remainingCount">
Clear completed
</button>
</footer>
</section>
<footer class="info">
<p>Double-click to edit a todo</p>
<!-- Remove the below line ↓ -->
<p>Template by <a href="http://sindresorhus.com">Sindre Sorhus</a></p>
<!-- Change this out with your name and url ↓ -->
<p>Created by <a href="https://www.lagou.com">教瘦</a></p>
<p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
</footer>
</template>
<script>
import './assets/index.css'
import { computed, onMounted, onUnmounted, ref, watchEffect } from 'vue'
import useLocalStorage from "./utils/useLocalStorage";
const storage = useLocalStorage()
// 1.添加待办事项
const useAdd = todos => {
const input = ref('')
const addTodo = () => {
const text = input.value && input.value.trim()
if (text.length === 0) return
todos.value.unshift({
text,
completed: false
})
input.value = ''
}
return {
input,
addTodo
}
}
// 2.删除待办事项
const useRemove = todos => {
const remove = todo => {
const index = todos.value.indexOf(todo) // 找到todo在todos中的索引
todos.value.splice(index, 1)
}
// 删除已完成的待办事项
const removeCompleted = () => {
todos.value = todos.value.filter(todo => !todo.completed)
}
return {
remove,
removeCompleted
}
}
// 3.编辑待办项
const useEdit = (remove) => {
let beforeEditingText = '' // 保存编辑前的文本框的值
const editingTodo = ref(null) // 记录当前是否是编辑状态
// 编辑todo
const editTodo = (todo) => {
beforeEditingText = todo.text
editingTodo.value = todo
}
// 完成编辑todo
const doneEdit = (todo) => {
if (!editingTodo.value) return
todo.text = todo.text.trim()
todo.text || remove(todo)
editingTodo.value = null
}
// 取消编辑todo
const cancelEdit = todo => {
editingTodo.value = null
todo.text = beforeEditingText
}
return {
editingTodo,
editTodo,
doneEdit,
cancelEdit
}
}
// 4.切换待办项完成状态
const useFilter = (todos) => {
const allDone = computed({
get() {
return !todos.value.filter(todo => !todo.completed).length
},
set(value) {
todos.value.forEach(todo => {
todo.completed = value
})
}
})
const filter = {
all: list => list,
active: list => list.filter(todo => !todo.completed),
completed: list => list.filter(todo => todo.completed),
}
const type = ref('all') // 保存type
const filteredTodos = computed(() => filter[type.value](todos.value)) // 计算属性
const remainingCount = computed(() => filter.active(todos.value).length)
const count = computed(() => todos.value.length)
const onHashChange = () => {
const hash = window.location.hash.replace('#/', '')
if (filter[hash]) {
type.value = hash
} else {
type.value = 'all'
window.location.hash = ''
}
}
onMounted(() => {
window.addEventListener('hashchange', onHashChange)
onHashChange()
})
onUnmounted(() => {
window.removeEventListener('hashchange', onHashChange)
})
return {
allDone,
filteredTodos,
remainingCount,
count,
type
}
}
// 5.存储待办事项
const useStorage = () => {
const KEY = 'TODOKEYS'
const todos = ref(storage.getItem(KEY) || [])
watchEffect(() => {
storage.setItem(KEY, todos.value)
})
return todos
}
export default {
name: 'App',
setup() {
const todos = useStorage()
const { remove, removeCompleted } = useRemove(todos)
return {
todos,
remove,
removeCompleted,
...useAdd(todos),
...useEdit(remove),
...useFilter(todos)
}
},
directives: {
editingFocus: (el, binding) => {
binding.value && el.focus()
}
}
}
</script>
<style>
</style>
