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>