危险

为之则易,不为则难

0%

01_Vue3 基础

今日目标

✔ 能够掌握 Vue3.0 的变化。

✔ 能够了解 Vite 的基本使用。

✔ 能够理解综合案例 Todos。

Vue3 基本概述

目标

了解 Vue3 现状,以及它的优点,展望它的未来。

内容

优点

  • 性能更高了:打包大小减少 41%、初次渲染快 55%、更新渲染快 133%、内存减少 54%,主要原因在于响应式原理换成了 Proxy,VNode 算法进行了优化。

  • 体积更小了:删除了一些不常用的 API,例如过滤器、EventBus 等,代码支持按需引入,能配合 Webpack 支持Tree Shaking support

  • 对 TS 支持更好了:因为源码就是用 TS 重写的。

  • Composition API ,能够更好的组织、封装、复用代码,RFCs

  • 新特性:Fragment、Teleport、Suspense。

  • 趋势:未来肯定会有越来越多的企业使用 Vue3.0 + TS 进行大型项目的开发,对于个人来说,学习流行的技术提升竞争力,加薪!

小结

说一下你对 Vue3 的理解?

Vite 创建项目

Vite 基本使用

目标

  • 了解 Vite 是什么?

  • 能够使用 Vite 创建 Vue 项目,在此项目的基础上学习 Vue3 的知识。

内容

  • 是什么:下一代前端开发与构建工具,热更新、打包构建速度相比较 Webpack 更快。

  • 为什么快?

    • Webpack:会将所有模块提前编译、打包,不管这个模块是否被用到,随着项目越来越大,打包启动速度自然越来越慢。

    • Vite:瞬间开启一个服务,并不会先编译所有文件,当浏览器用到某个文件时,Vite 服务会收到请求然后编译后响应到客户端。

使用

  1. 使用 Vite 创建项目。
1
2
3
npm create vite
# or
yarn create vite
  1. 输入项目名字,默认为 vite-project。

  2. 选择创建的项目类型,选择 vue 即可。

  3. 选择创建的 Vue 项目类型,选择 vue。

image-20220514091607723

了解使用 Vite 快速创建项目。

1
2
# 创建普通 Vue 项目
yarn create vite vite-demo --template vue
1
2
# 创建基于 TS 模板的 Vue 项目
yarn create vite vite-demo-ts --template vue-ts

下面是旧版本的写法,不建议。

1
2
3
4
5
6
# 注意 Node 版本要 12 以上
# yarn create vite-app <project-name>
npm init vite-app <project-name>
cd <project-name>
npm install
npm run dev

小结

Vite 是什么?

编写 Vue 应用

目标

掌握如何创建 Vue3 应用实例。

步骤

  1. 清空 src 里面的所有内容。

  2. src/main.js 中按需导入 createApp 函数。

  3. 定义 App.vue 根组件,导入到 main.js

  4. 使用 createApp 函数基于 App.vue 根组件创建应用实例。

  5. 挂载至 index.html#app 容器。

main.js

1
2
3
4
5
6
7
8
9
10
11
// 1. 导入 createApp 函数,不再是曾经的 Vue 了
// 2. 编写一个根组件 App.vue,导入进来
// 3. 基于根组件创建应用实例,类似 Vue2 的 vm,但比 vm 更轻量
// 4. 挂载到 index.html 的 #app 容器
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
app.mount('#app')

// Vue2: new Vue()、new VueRouter()、new Vuex.Store()
// Vue3: createApp()、createRouter()、createStore()

App.vue

1
2
3
4
5
6
7
8
<template>
<div class="container">我是根组件</div>
</template>
<script>
export default {
name: 'App',
}
</script>

index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
</head>

<body>
<!-- 容器,由 Vue 创建实例来渲染 -->
<div id="app"></div>
<!-- Webpack 导入的是打包后的代码 -->
<!-- Vite 直接导入的就是源码 -->
<script type="module" src="/src/main.js"></script>
</body>
</html>

小结

如何创建 Vue 应用实例?

安装开发工具

  • 禁用 Vetur 插件,安装 Volar 插件。

  • VSCode 代码片段插件:Vue VSCode Snippets,使用见文档

  • Vue3 的 Chrome 调试插件也变了,下载链接,注意安装后需要把之前的 Vue2 Devtools 关闭掉。

学习组合 API

选项/组合 API

目标

理解什么是 Options API 写法,什么是 Composition API 写法。

需求

Vue2

  • 优点:易于学习和使用,写代码的位置已经约定好。

  • 缺点:对于大型项目,不利于代码的复用、不利于管理和维护。

  • 解释:同一功能的数据和业务逻辑分散在同一个文件的 N 个地方,随着业务复杂度的上升,可能会出现动图左侧的代码组织方式。

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
<template>
<div class="container">
<p>X 轴:{{ x }} Y 轴:{{ y }}</p>
<hr />
<div>
<p>{{ count }}</p>
<button @click="add()">自增</button>
</div>
</div>
</template>
<script>
export default {
name: 'App',
data() {
return {
// !#Fn1
x: 0,
y: 0,
// ?#Fn2
count: 0,
}
},
mounted() {
// !#Fn1
document.addEventListener('mousemove', this.move)
},
methods: {
// !#Fn1
move(e) {
this.x = e.pageX
this.y = e.pageY
},
// ?#Fn2
add() {
this.count++
},
},
destroyed() {
// !#Fn1
document.removeEventListener('mousemove', this.move)
},
}
</script>

Vue3

  • 优点:可以把同一功能的数据业务逻辑组织到一起,方便复用和维护。

  • 缺点:需要有良好的代码组织和拆分能力,相对没有 Vue2 容易上手。

  • 注意:为了能让大家较好的过渡到 Vue3.0 版本,目前也是支持 Vue2.x 选项 API 的写法。

  • 链接:why-composition-apicomposition-api-doc

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
<template>
<div class="container">
<p>X 轴:{{ x }} Y 轴:{{ y }}</p>
<hr />
<div>
<p>{{ count }}</p>
<button @click="add()">自增</button>
</div>
</div>
</template>
<script>
import { onMounted, onUnmounted, reactive, ref, toRefs } from 'vue'
export default {
name: 'App',
setup() {
// !#Fn1
const mouse = reactive({
x: 0,
y: 0,
})
const move = (e) => {
mouse.x = e.pageX
mouse.y = e.pageY
}
onMounted(() => {
document.addEventListener('mousemove', move)
})
onUnmounted(() => {
document.removeEventListener('mousemove', move)
})

// ?Fn2
const count = ref(0)
const add = () => {
count.value++
}

// 统一返回数据供模板使用
return {
...toRefs(mouse),
count,
add,
}
},
}
</script>

小结

Vue3 Composition API 可以把 __ 和 __ 组合到一起?

setup 入口函数

目标

掌握 setup 函数的基本使用。

内容

  • 是什么:setup 是 Vue3 中新增的组件配置项,作为组合 API 的入口函数。

  • 执行时机:实例创建前调用,甚至早于 Vue2 中的 beforeCreate。

  • 注意点:由于执行 setup 的时候实例还没有 created,所以在 setup 中是不能直接使用 data 和 methods 中的数据的,所以 Vue3 setup 中的 this 也被绑定为了 undefined。

  • 虽然 Vue2 中的 data 和 methods 配置项虽然在 Vue3 中也能使用,但不建议了,建议数据和方法都写在 setup 函数中,并通过 return 进行返回可在模版中直接使用(一般情况下 setup 不能为异步函数)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<template>
<h1 @click="say()">{{ msg }}</h1>
</template>
<script>
export default {
setup() {
const msg = 'Hello Vue3'
const say = () => {
console.log(msg)
}
return { msg, say }
},
}
</script>

面试

setup 中 return 的一定只能是一个对象吗?(setup 也可以返回一个渲染函数)

1
2
3
4
5
6
7
8
9
<script>
import { h } from 'vue'
export default {
name: 'App',
setup() {
return () => h('h2', 'Hello Vue3')
},
}
</script>

小结

  • setup 的执行时机是什么?

  • setup 中的 this 指向是什么?

  • 想在模板中使用 setup 中定义的数据,该怎么做?

reactive

reactive 包装数组

目标

掌握使用 reactive 函数包装数组为响应式数据。

内容

reactive 是一个函数,用来将普通对象/数组包装成响应式式数据使用,无法直接处理基本数据类型(因为它是基于 Proxy 的,而 Proxy 只能代理的是对象)。

需求

📝 点击删除当前行信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<template>
<ul>
<li v-for="(item, index) in arr" :key="item" @click="removeItem(index)">{{ item }}</li>
</ul>
</template>

<script>
export default {
name: 'App',
setup() {
const arr = ['a', 'b', 'c']
const removeItem = (index) => {
arr.splice(index, 1)
}
return {
arr,
removeItem,
}
},
}
</script>
问题

数据确实是删了,但视图没有更新(不是响应式的)!

解决

使用 reactive 包装数组使变成响应式数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<template>
<ul>
<li v-for="(item, index) in arr" :key="item" @click="removeItem(index)">{{ item }}</li>
</ul>
</template>

<script>
import { reactive } from 'vue'
export default {
name: 'App',
setup() {
const arr = reactive(['a', 'b', 'c'])
const removeItem = (index) => {
arr.splice(index, 1)
}
return {
arr,
removeItem,
}
},
}
</script>
小结

reactive 的作用是什么?

reactive 包装对象

目标

掌握使用 reactive 函数包装对象为响应式数据。

需求

📝 列表渲染、删除功能、添加功能。

列表删除
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
<template>
<ul>
<li v-for="(item, index) in state.arr" :key="item.id" @click="removeItem(index)">{{ item.name }}</li>
</ul>
</template>

<script>
import { reactive } from 'vue'
export default {
name: 'App',
setup() {
const state = reactive({
arr: [
{
id: 0,
name: 'ifer',
},
{
id: 1,
name: 'elser',
},
{
id: 2,
name: 'xxx',
},
],
})
const removeItem = (index) => {
// 默认是递归监听的,对象里面任何一个数据的变化都是响应式的
state.arr.splice(index, 1)
}
return {
state,
removeItem,
}
},
}
</script>
添加功能
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
<template>
<form @submit.prevent="handleSubmit">
<input type="text" v-model="user.id" />
<input type="text" v-model="user.name" />
<input type="submit" />
</form>
<ul>
<li v-for="(item, index) in state.arr" :key="item.id" @click="removeItem(index)">{{ item.name }}</li>
</ul>
</template>

<script>
import { reactive } from 'vue'
export default {
name: 'App',
setup() {
const state = reactive({
arr: [
{
id: 0,
name: 'ifer',
},
{
id: 1,
name: 'elser',
},
{
id: 2,
name: 'xxx',
},
],
})
const removeItem = (index) => {
// 默认是递归监听的,对象里面任何一个数据的变化都是响应式的
state.arr.splice(index, 1)
}

const user = reactive({
id: '',
name: '',
})
const handleSubmit = () => {
state.arr.push({
id: user.id,
name: user.name,
})
user.id = ''
user.name = ''
}
return {
state,
removeItem,
user,
handleSubmit,
}
},
}
</script>
抽离函数

优化:将同一功能的数据和业务逻辑抽离为一个函数,代码更易读,更容易复用

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
<template>
<form @submit.prevent="handleSubmit">
<input type="text" v-model="user.id" />
<input type="text" v-model="user.name" />
<input type="submit" />
</form>
<ul>
<li v-for="(item, index) in state.arr" :key="item.id" @click="removeItem(index)">{{ item.name }}</li>
</ul>
</template>

<script>
import { reactive } from 'vue'
function useRemoveItem() {
const state = reactive({
arr: [
{
id: 0,
name: 'ifer',
},
{
id: 1,
name: 'elser',
},
{
id: 2,
name: 'xxx',
},
],
})
const removeItem = (index) => {
state.arr.splice(index, 1)
}
return { state, removeItem }
}
function useAddItem(state) {
const user = reactive({
id: '',
name: '',
})
const handleSubmit = () => {
state.arr.push({
id: user.id,
name: user.name,
})
user.id = ''
user.name = ''
}
return {
user,
handleSubmit,
}
}
export default {
name: 'App',
setup() {
const { state, removeItem } = useRemoveItem()
const { user, handleSubmit } = useAddItem(state)
return {
state,
removeItem,
user,
handleSubmit,
}
},
}
</script>
拆分文件

remove.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { reactive } from 'vue'
export default function userRemoveItem() {
const state = reactive({
arr: [
{
id: 0,
name: 'ifer',
},
{
id: 1,
name: 'elser',
},
{
id: 2,
name: 'xxx',
},
],
})
const removeItem = (index) => {
state.arr.splice(index, 1)
}
return { state, removeItem }
}

add.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { reactive } from 'vue'
export default function useAddItem(state) {
const user = reactive({
id: '',
name: '',
})
const handleSubmit = () => {
state.arr.push({
id: user.id,
name: user.name,
})
user.id = ''
user.name = ''
}
return {
user,
handleSubmit,
}
}

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
<template>
<form @submit.prevent="handleSubmit">
<input type="text" v-model="user.id" />
<input type="text" v-model="user.name" />
<input type="submit" />
</form>
<ul>
<li v-for="(item, index) in state.arr" :key="item.id" @click="removeItem(index)">{{ item.name }}</li>
</ul>
</template>

<script>
import userRemoveItem from './hooks/remove'
import useAddItem from './hooks/add'
export default {
name: 'App',
setup() {
const { state, removeItem } = userRemoveItem()
const { user, handleSubmit } = useAddItem(state)
return {
state,
removeItem,
user,
handleSubmit,
}
},
}
</script>

ref

目标

掌握 ref 函数的使用。

基本使用

ref 函数,可以把简单数据类型包裹为响应式数据(复杂类型也可以),注意 JS 中操作值的时候,需要加 .value 属性,模板中正常使用即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<template>
<div class="container">
<div>{{ name }}</div>
<button @click="updateName">修改数据</button>
</div>
</template>
<script>
import { ref } from 'vue'
export default {
name: 'App',
setup() {
const name = ref('ifer')
const updateName = () => {
name.value = 'xxx'
}
return { name, updateName }
},
}
</script>

点击计数

  1. 定义一个简单数据类型的响应式数据。

  2. 定义一个修改数字的方法。

  3. 在 setup 返回数据和函数,供模板中使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<template>
<h3>{{ count }}</h3>
<button @click="add">累加1</button>
</template>
<script>
import { ref } from 'vue'
export default {
name: 'App',
setup() {
const count = ref(0)
const add = () => {
count.value++
}
return { count, add }
},
}
</script>

包装复杂数据类型

注意:ref 其实也可以包裹复杂数据类型为响应式数据,一般对于数据类型未确定的情况下推荐使用 ref,例如后端返回的数据。

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
<template>
<div class="container">
<div>{{ data?.name }}</div>
<button @click="updateName">修改数据</button>
</div>
</template>
<script>
import { ref } from 'vue'
export default {
name: 'App',
setup() {
// 初始值是 null
const data = ref(null)
setTimeout(() => {
// 右边的对象可能是后端返回的
data.value = {
name: 'ifer',
}
}, 1000)
const updateName = () => {
data.value.name = 'xxx'
}
return { data, updateName }
},
}
</script>

如何选择

  • 当你明确知道需要包裹的是一个对象,那么推荐使用 reactive,其他情况使用 ref 即可。

  • Vue3.2 之后,更推荐使用 ref,性能得到了很大的提升。

小结

  • ref 函数的作用是什么?

  • ref 包装简单数据类型后变成了一个对象,在模板中需要 .value 吗?在 setup 中呢?

toRef

目标

掌握 toRef 函数的使用。

内容

toRef 函数的作用:转换响应式对象中某个属性为单独响应式数据,并且转换后的值和之前是关联的(ref 函数也可以转换,但值非关联)。

需求

📝 需求:在模板中渲染 name 和 age。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<template>
<div class="container">
<h2>name: {{ obj.name }} age: {{obj.age}}</h2>
<button @click="updateName">修改数据</button>
</div>
</template>
<script>
import { reactive } from 'vue'
export default {
name: 'App',
setup() {
const obj = reactive({
name: 'ifer',
age: 10,
address: '河南',
sex: '男',
})
const updateName = () => {
obj.name = 'xxx'
}
return { obj, updateName }
},
}
</script>
  • 问题 1:模板中都要使用 obj. 进行获取数据,麻烦。

  • 问题 2:明明模板中只用到了 name 和 age,却把整个 obj 进行了导出,没必要,性能浪费。

问题

修改数据,发现视图并没有更新,也就是上面的操作导致数据丢失了响应式,丢失响应式的操作,常见的还有解构赋值等,如下。

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
<template>
<div class="container">
<h2>{{ name }}</h2>
<button @click="updateName">修改数据</button>
</div>
</template>
<script>
import { reactive } from 'vue'
export default {
name: 'App',
setup() {
const obj = reactive({
name: 'ifer',
age: 10,
address: '河南',
sex: '男',
})
// !解构出简单数据类型会失去响应式
let { name } = obj
const updateName = () => {
// obj.name = 'xxx' // 不响应
name = 'xxx' // 不响应
}
return { name, updateName }
},
}
</script>

解决

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
<template>
<div class="container">
<h2>{{ name }}</h2>
<button @click="updateName">修改数据</button>
</div>
</template>
<script>
import { reactive, toRef } from 'vue'
export default {
name: 'App',
setup() {
const obj = reactive({
name: 'ifer',
age: 10,
})
const name = toRef(obj, 'name')
const updateName = () => {
// 注意:需要使用 name.value 进行修改
name.value = 'xxx'
// 对 obj.name 的修改也会影响视图的变化,即值是关联的
// obj.name = 'xxx' // ok
}
return { name, updateName }
},
}
</script>

toRefs

目标

掌握 toRefs 函数的使用。

内容

toRefs 函数的作用:转换响应式对象中所有属性为单独响应式数据,并且转换后的值和之前是关联的。

需求

📝 模板中需要写 obj.name、obj.age …很麻烦,期望能够直接能使用 name、age 属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<template>
<div class="container">
<h2>{{ name }} {{ age }}</h2>
<button @click="updateName">修改数据</button>
</div>
</template>
<script>
import { reactive, toRefs } from 'vue'
export default {
name: 'App',
setup() {
const obj = reactive({
name: 'ifer',
age: 10,
})
const updateName = () => {
obj.name = 'xxx'
obj.age = 18
}
return { ...toRefs(obj), updateName }
},
}
</script>

computed

目标

掌握 computed 函数的使用。

基本

作用:computed 函数用来定义计算属性。

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
<template>
<p>firstName: {{ person.firstName }}</p>
<p>lastName: {{ person.lastName }}</p>
<p>fullName: {{ person.fullName }}</p>
</template>
<script>
import { computed, reactive } from 'vue'
export default {
name: 'App',
setup() {
const person = reactive({
firstName: '朱',
lastName: '逸之',
})
person.fullName = computed(() => {
return person.firstName + ' ' + person.lastName
})
// 也可以传入对象,目前和上面等价
/* person.fullName = computed({
get() {
return person.firstName + ' ' + person.lastName
},
}) */
return {
person,
}
},
}
</script>

高级

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
<template>
<p>firstName: {{ person.firstName }}</p>
<p>lastName: {{ person.lastName }}</p>
<input type="text" v-model="person.fullName" />
</template>
<script>
import { computed, reactive } from 'vue'
export default {
name: 'App',
setup() {
const person = reactive({
firstName: '朱',
lastName: '逸之',
})
// 也可以传入对象,目前和上面等价
person.fullName = computed({
get() {
return person.firstName + ' ' + person.lastName
},
set(value) {
const newArr = value.split(' ')
person.firstName = newArr[0]
person.lastName = newArr[1]
},
})
return {
person,
}
},
}
</script>

小结

  • 给 computed 传入函数,返回值就是计算属性的值。

  • 给 computed 传入对象,get 获取计算属性的值,set 监听计算属性改变。

watch

目标

掌握 watch 函数的使用。

监听 reactive 内部数据

注意 1:监听 reactive 内部数据时,强制开启了深度监听,且配置无效;监听对象的时候 newValue 和 oldValue 是全等的。

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
<template>
<p>{{ obj.hobby.eat }}</p>
<button @click="obj.hobby.eat = '面条'">click</button>
</template>

<script>
import { watch, reactive } from 'vue'
export default {
name: 'App',
setup() {
const obj = reactive({
name: 'ifer',
hobby: {
eat: '西瓜',
},
})
watch(obj, (newValue, oldValue) => {
// 注意1:监听对象的时候,新旧值是相等的
// 注意2:强制开启深度监听,配置无效
console.log(newValue === oldValue) // true
})

return { obj }
},
}
</script>

注意 2:reactive 的【内部对象】也是一个 reactive 类型的数据。

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
<template>
<p>{{ obj.hobby.eat }}</p>
<button @click="obj.hobby.eat = '面条'">click</button>
</template>

<script>
import { watch, reactive, isReactive } from 'vue'
export default {
name: 'App',
setup() {
const obj = reactive({
name: 'ifer',
hobby: {
eat: '西瓜',
},
})
// reactive 的【内部对象】也是一个 reactive 类型的数据
// console.log(isReactive(obj.hobby))
watch(obj.hobby, (newValue, oldValue) => {
console.log(newValue === oldValue) // true
})

return { obj }
},
}
</script>

注意 3:对 reactive 自身的修改则不会触发监听。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<template>
<p>{{ obj.hobby.eat }}</p>
<button @click="obj.hobby = { eat: '面条' }">click</button>
</template>

<script>
import { watch, reactive } from 'vue'
export default {
name: 'App',
setup() {
const obj = reactive({
name: 'ifer',
hobby: {
eat: '西瓜',
},
})
watch(obj.hobby, (newValue, oldValue) => {
// obj.hobby = { eat: '面条' }
console.log('对 reactive 自身的修改不会触发监听')
})
return { obj }
},
}
</script>

监听 ref 数据

监听一个 ref 数据

📝 监听 age 的变化,做一些操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template>
<p>{{ age }}</p>
<button @click="age++">click</button>
</template>

<script>
import { watch, ref } from 'vue'
export default {
name: 'App',
setup() {
const age = ref(18)
// 监听 ref 数据 age,会触发后面的回调,不需要 .value
watch(age, (newValue, oldValue) => {
console.log(newValue, oldValue)
})

return { age }
},
}
</script>
监听多个 ref 数据

📝 可以通过数组的形式,同时监听 age 和 num 的变化。

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
<template>
<p>age: {{ age }} num: {{ num }}</p>
<button @click="handleClick">click</button>
</template>

<script>
import { watch, ref } from 'vue'
export default {
name: 'App',
setup() {
const age = ref(18)
const num = ref(0)

const handleClick = () => {
age.value++
num.value++
}
// 数组里面是 ref 数据
watch([age, num], (newValue, oldValue) => {
console.log(newValue, oldValue)
})

return { age, num, handleClick }
},
}
</script>
立即触发监听
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
<template>
<p>{{ age }}</p>
<button @click="handleClick">click</button>
</template>

<script>
import { watch, ref } from 'vue'
export default {
name: 'App',
setup() {
const age = ref(18)

const handleClick = () => {
age.value++
}

watch(
age,
(newValue, oldValue) => {
console.log(newValue, oldValue) // 18 undefined
},
{
immediate: true,
}
)

return { age, handleClick }
},
}
</script>
开启深度监听 ref 数据

📝 问题:修改 ref 对象里面的数据并不会触发监听,说明 ref 并不是默认开启 deep 的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<template>
<p>{{ obj.hobby.eat }}</p>
<button @click="obj.hobby.eat = '面条'">修改 obj.hobby.eat</button>
</template>

<script>
import { watch, ref } from 'vue'
export default {
name: 'App',
setup() {
const obj = ref({
hobby: {
eat: '西瓜',
},
})
// 注意:ref 监听对象,默认监听的是这个对象地址的变化
watch(obj, (newValue, oldValue) => {
console.log(newValue === oldValue)
})

return { obj }
},
}
</script>
  1. 解决 1:当然直接修改整个对象的话肯定是会被监听到的(注意模板中对 obj 的修改,相当于修改的是 obj.value)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<template>
<p>{{ obj.hobby.eat }}</p>
<button @click="obj = { hobby: { eat: '面条' } }">修改 obj</button>
</template>

<script>
import { watch, ref } from 'vue'
export default {
name: 'App',
setup() {
const obj = ref({
hobby: {
eat: '西瓜',
},
})
watch(obj, (newValue, oldValue) => {
console.log(newValue, oldValue)
console.log(newValue === oldValue)
})

return { obj }
},
}
</script>
  1. 解决 2:开启深度监听 ref 数据。
1
2
3
4
5
6
7
8
9
10
watch(
obj,
(newValue, oldValue) => {
console.log(newValue, oldValue)
console.log(newValue === oldValue)
},
{
deep: true,
}
)
  1. 解决 3:还可以通过监听 ref.value 来实现同样的效果。

🧐 因为 ref 内部如果包裹对象的话,其实还是借助 reactive 实现的,可以通过 isReactive 方法来证明。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<template>
<p>{{ obj.hobby.eat }}</p>
<button @click="obj.hobby.eat = '面条'">修改 obj</button>
</template>

<script>
import { watch, ref } from 'vue'
export default {
name: 'App',
setup() {
const obj = ref({
hobby: {
eat: '西瓜',
},
})
watch(obj.value, (newValue, oldValue) => {
console.log(newValue, oldValue)
console.log(newValue === oldValue)
})

return { obj }
},
}
</script>

监听普通数据

监听响应式对象中的某一个普通属性值,要通过函数返回的方式进行(如果返回的是对象/响应式对象,修改内部的数据需要开启深度监听)。

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
<template>
<p>{{ obj.hobby.eat }}</p>
<button @click="obj.hobby.eat = '面条'">修改 obj</button>
</template>

<script>
import { watch, reactive } from 'vue'
export default {
name: 'App',
setup() {
const obj = reactive({
hobby: {
eat: '西瓜',
},
})
// 把 obj.hobby 作为普通值去进行监听,只能监听到 obj.hobby 自身的变化
/* watch(
() => obj.hobby,
(newValue, oldValue) => {
console.log(newValue, oldValue)
console.log(newValue === oldValue)
}
) */
// 如果开启了深度监听,则能监听到 obj.hobby 和内部数据的所有变化
/* watch(
() => obj.hobby,
(newValue, oldValue) => {
console.log(newValue, oldValue)
console.log(newValue === oldValue)
},
{
deep: true,
}
) */
// 能监听影响到 obj.hobby.eat 变化的操作,例如 obj.hobby = { eat: '面条' } 或 obj.hobby.eat = '面条',如果是 reactive 直接对 obj 的修改则不会被监听到(ref 可以)
watch(
() => obj.hobby.eat,
(newValue, oldValue) => {
console.log(newValue, oldValue)
console.log(newValue === oldValue)
}
)
return { obj }
},
}
</script>

小结

watch 监听 ref 类型的数据是递归监听的吗?监听 reactive 类型的数据呢?

vue3 生命周期

目标

掌握组合 API 中生命周期钩子函数的写法。

内容

  • 组合 API生命周期写法,其实 选项 API 的写法在 Vue3 中也是支持。

  • Vue3(组合 API)常用的生命周期钩子有 7 个,可以多次使用同一个钩子,执行顺序和书写顺序相同。

  • setup、onBeforeMount、onMounted、onBeforeUpdate、onUpdated、onBeforeUnmount、onUnmounted。

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
<template>
<hello-world v-if="state.bBar" />
<button @click="state.bBar = !state.bBar">destroy cmp</button>
</template>

<script>
import HelloWorld from './components/HelloWorld.vue'
import { reactive } from 'vue'
export default {
name: 'App',
components: {
HelloWorld,
},
setup() {
const state = reactive({
bBar: true,
})
return {
state,
}
},
}
</script>

HelloWorld.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
<template>
<p>{{ state.msg }}</p>
<button @click="state.msg = 'xxx'">update msg</button>
</template>

<script>
import { onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted, reactive } from 'vue'
export default {
name: 'HelloWorld',
setup() {
const state = reactive({
msg: 'Hello World',
})

onBeforeMount(() => {
console.log('onBeforeMount')
})
onMounted(() => {
console.log('onMounted')
})
onBeforeUpdate(() => {
console.log('onBeforeUpdate')
})
onUpdated(() => {
console.log('onUpdated')
})
onBeforeUnmount(() => {
console.log('onBeforeUnmount')
})
onUnmounted(() => {
console.log('onUnmounted')
})
return {
state,
}
},
}
</script>

小结

Vue3 把 Vue2 中的哪两个钩子换成了 setup?

setup 函数参数

目标

掌握 setup 中参数的使用。

需求

父传子

App.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<template>
<h1>父组件</h1>
<p>{{ money }}</p>
<hr />
<!-- 1. 父组件通过自定义属性提供数据 -->
<Son :money="money" />
</template>
<script>
import { ref } from 'vue'
import Son from './Son.vue'
export default {
name: 'App',
components: {
Son,
},
setup() {
const money = ref(100)
return { money }
},
}
</script>

Son.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template>
<h1>子组件</h1>
<p>{{ money }}</p>
</template>
<script>
export default {
name: 'Son',
// 2. 子组件通过 props 进行接收,在模板中就可以使用啦
props: {
money: {
type: Number,
default: 0,
},
},
setup(props) {
// 3. setup 中也可以通过形参 props 来获取传递的数据
console.log(props.money)
},
}
</script>

子传父

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
<template>
<h1>父组件</h1>
<p>{{ money }}</p>
<hr />
<Son :money="money" @change-money="updateMoney" />
</template>
<script>
import { ref } from 'vue'
import Son from './Son.vue'
export default {
name: 'App',
components: {
Son,
},
setup() {
const money = ref(100)
// #1 父组件准备修改数据的方法并提供给子组件
const updateMoney = (newMoney) => {
money.value -= newMoney
}
return { money, updateMoney }
},
}
</script>

Son.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
<template>
<h1>子组件</h1>
<p>{{ money }}</p>
<button @click="changeMoney(1)">花 1 元</button>
</template>
<script>
export default {
name: 'Son',
props: {
money: {
type: Number,
default: 0,
},
},
emits: ['change-money'],
setup(props, { emit }) {
// attrs 捡漏、slots 插槽
const changeMoney = (m) => {
// #2 子组件通过 emit 进行触发
emit('change-money', m)
}
return { changeMoney }
},
}
</script>

小结

  • setup 第一个参数的是什么?

  • 第二个参数 context 中包含什么信息?

provide/inject

目标

掌握使用 provide 函数和 inject 函数完成跨层级组件通讯。

内容

image-20220213110153307

需求

📝 把 App.vue 中的数据传递给孙组件,Child.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
<template>
<div class="container">
<h2>App {{ money }}</h2>
<button @click="money = 1000">发钱</button>
<hr />
<Parent />
</div>
</template>
<script>
import { provide, ref } from 'vue'
import Parent from './Parent.vue'
export default {
name: 'App',
components: {
Parent,
},
setup() {
// 提供数据
const money = ref(100)
provide('money', money)
// 提供修改数据的方法
const changeMoney = (m) => (money.value -= m)
provide('changeMoney', changeMoney)
return { money }
},
}
</script>

Parent.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<template>
<div>
Parent
<hr />
<Child />
</div>
</template>

<script>
import Child from './Child.vue'
export default {
components: {
Child,
},
}
</script>

Child.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
<div>
Child
<p>{{ money }}</p>
<button @click="changeMoney(1)">花 1 块钱</button>
</div>
</template>

<script>
import { inject } from 'vue'
export default {
setup() {
const money = inject('money')
const changeMoney = inject('changeMoney')
return { money, changeMoney }
},
}
</script>

小结

script setup 语法

文档链接

初体验

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script setup>
import { ref } from 'vue'

const count = ref(18)

const increment = () => {
count.value++
}
</script>
<template>
<p>{{ count }}</p>
<p>
<button @click="increment">+1</button>
</p>
</template>

defineProps

App.vue

1
2
3
4
5
6
7
8
9
10
11
<script setup>
import { ref } from 'vue'
import Child from './Child.vue'
const car = ref('奔驰')
</script>
<template>
App
<hr />
<!-- 1. 父组件通过自定义属性提供数据 -->
<Child :car="car" />
</template>

Child.vue

1
2
3
4
5
6
7
8
9
10
11
<script setup>
const props = defineProps({
car: String,
})
// 模板中可以直接使用 car
// setup 中要通过下面的方式
console.log(props.car)
</script>
<template>
<div>car: {{ car }}</div>
</template>

defineEmits

App.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script setup>
import { reactive } from 'vue'
import Hello from './Hello.vue'
const person = reactive({
name: 'ifer',
age: 18,
})
const updateAge = () => {
person.age++
}
</script>
<template>
<Hello v-bind="person" @updateAge="updateAge" />
</template>

Hello.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<script setup>
// defineProps 无需引用,可以在 script setup 中直接使用
const props = defineProps({
name: String,
age: Number,
})
const emit = defineEmits(['updateAge'])

const updateAge = () => {
emit('updateAge')
}
</script>
<template>
<div>name: {{ props.name }} age: {{ age }}</div>
<button @click="emit('updateAge')">update name</button>
<button @click="updateAge">update name</button>
<button @click="$emit('updateAge')">update name</button>
</template>

Vue3 其他变更

v-model

目标

掌握 Vue3 中 v-model 的用法。

基本操作

在 Vue2 中 v-mode 指令语法糖简写的代码。

1
<Son :value="msg" @input="msg=$event" />

在 Vue3 中 v-model 语法糖有所调整。

1
<Son :modelValue="msg" @update:modelValue="msg=$event" />

App.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template>
<h2>count: {{ count }}</h2>
<hr />
<Son :modelValue="count" @update:modelValue="count = $event" />
<!-- <Son v-model="count" /> -->
</template>
<script>
import { ref } from 'vue'
import Son from './Son.vue'
export default {
name: 'App',
components: {
Son,
},
setup() {
const count = ref(10)
return { count }
},
}
</script>

Son.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<template>
<h2>子组件 {{ modelValue }}</h2>
<button @click="$emit('update:modelValue', 100)">改变 count</button>
</template>
<script>
export default {
name: 'Son',
props: {
modelValue: {
type: Number,
default: 0,
},
},
}
</script>

传递多个

App.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template>
<h2>count: {{ count }} age: {{ age }}</h2>
<hr />
<Son v-model="count" v-model:age="age" />
</template>
<script>
import { ref } from 'vue'
import Son from './Son.vue'
export default {
name: 'App',
components: {
Son,
},
setup() {
const count = ref(10)
const age = ref(18)
return { count, age }
},
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template>
<h2>子组件 {{ modelValue }} {{ age }}</h2>
<button @click="$emit('update:modelValue', 100)">改变 count</button>
<button @click="$emit('update:age', 19)">改变 age</button>
</template>
<script>
export default {
name: 'Son',
props: {
modelValue: {
type: Number,
default: 0,
},
age: {
type: Number,
default: 18,
},
},
}
</script>

小结

ref 属性

目标

能够通过 ref 属性获取 DOM 或组件。

内容

获取单个 DOM。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template>
<!-- #3 -->
<div ref="dom">我是box</div>
</template>
<script>
import { onMounted, ref } from 'vue'
export default {
name: 'App',
setup() {
// #1
const dom = ref(null)
onMounted(() => {
// #4
console.log(dom.value)
})
// #2
return { dom }
},
}
</script>

获取组件实例。

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
<template>
<!-- #4 -->
<button @click="changeName">修改子组件的 Name</button>
<hr />
<!-- #3 -->
<Test ref="test" />
</template>
<script>
import { ref } from 'vue'
import Test from './Test.vue'
export default {
name: 'App',
components: {
Test,
},
setup() {
// #1
const test = ref(null)
const changeName = () => {
test.value.changeName('elser')
}
// #2
return { test, changeName }
},
}
</script>

Test.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<template>
<div>
<p>{{ o.name }}</p>
</div>
</template>

<script>
import { reactive } from 'vue'
export default {
setup() {
const o = reactive({ name: 'ifer' })

const changeName = (name) => {
o.name = name
}
return {
o,
changeName,
}
},
}
</script>

小结

Fragment

  • Vue2 中组件必须有一个跟标签。

  • Vue3 中组件可以没有根标签,其内部会将多个标签包含在一个 Fragment 虚拟元素中。

  • 好处:减少标签层级和内存占用。

Teleport

作用

传送,能将特定的 HTML 结构(一般是嵌套很深的)移动到指定的位置,解决 HTML 结构嵌套过深造成的样式影响或不好控制的问题。

需求

在 Child 组件点击按钮进行弹框。

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
<template>
<div class="child">
<dialog v-if="bBar" />
<button @click="handleDialog">显示弹框</button>
</div>
</template>

<script>
import { ref } from 'vue'
import Dialog from './Dialog.vue'
export default {
name: 'Child',
components: {
Dialog,
},
setup() {
const bBar = ref(false)
const handleDialog = () => {
bBar.value = !bBar.value
}
return {
bBar,
handleDialog,
}
},
}
</script>

解决

1
2
3
4
5
6
7
8
<template>
<div class="child">
<teleport to="body">
<dialog v-if="bBar" />
</teleport>
<button @click="handleDialog">显示弹框</button>
</div>
</template>

其他细节

参考 Vue3 迁移指南

  1. 全局 API 的变更,链接

  2. data 只能是函数,链接

  3. 自定义指令 API 和组件保持一致,链接

  4. keyCode 作为 v-on 修饰符被移除、移除 v-on.native 修饰符、filters 被移除链接

  5. $on、$off、$once 被移除,链接

  6. 过渡类名的更改,链接

Todos

静态结构

1
yarn create vite-app todos

main.js

1
2
3
4
5
6
import { createApp } from 'vue'
import './styles/base.css'
import './styles/index.css'
import App from './App.vue'

createApp(App).mount('#app')

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
<template>
<section class="todoapp">
<header class="header">
<h1>todos</h1>
<input class="new-todo" placeholder="What needs to be done?" 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 class="completed">
<div class="view">
<input class="toggle" type="checkbox" checked />
<label>Taste JavaScript</label>
<button class="destroy"></button>
</div>
<input class="edit" value="Create a TodoMVC template" />
</li>
<li>
<div class="view">
<input class="toggle" type="checkbox" />
<label>Buy a unicorn</label>
<button class="destroy"></button>
</div>
<input class="edit" value="Rule the web" />
</li>
</ul>
</section>

<footer class="footer">
<span class="todo-count"><strong>0</strong> item left</span>
<ul class="filters">
<li>
<a class="selected" href="#/">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>
</template>

styles/base.css

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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
hr {
margin: 20px 0;
border: 0;
border-top: 1px dashed #c5c5c5;
border-bottom: 1px dashed #f7f7f7;
}

.learn a {
font-weight: normal;
text-decoration: none;
color: #b83f45;
}

.learn a:hover {
text-decoration: underline;
color: #787e7e;
}

.learn h3,
.learn h4,
.learn h5 {
margin: 10px 0;
font-weight: 500;
line-height: 1.2;
color: #000;
}

.learn h3 {
font-size: 24px;
}

.learn h4 {
font-size: 18px;
}

.learn h5 {
margin-bottom: 0;
font-size: 14px;
}

.learn ul {
padding: 0;
margin: 0 0 30px 25px;
}

.learn li {
line-height: 20px;
}

.learn p {
font-size: 15px;
font-weight: 300;
line-height: 1.3;
margin-top: 0;
margin-bottom: 0;
}

#issue-count {
display: none;
}

.quote {
border: none;
margin: 20px 0 60px 0;
}

.quote p {
font-style: italic;
}

.quote p:before {
content: '“';
font-size: 50px;
opacity: 0.15;
position: absolute;
top: -20px;
left: 3px;
}

.quote p:after {
content: '”';
font-size: 50px;
opacity: 0.15;
position: absolute;
bottom: -42px;
right: 3px;
}

.quote footer {
position: absolute;
bottom: -40px;
right: 0;
}

.quote footer img {
border-radius: 3px;
}

.quote footer a {
margin-left: 5px;
vertical-align: middle;
}

.speech-bubble {
position: relative;
padding: 10px;
background: rgba(0, 0, 0, 0.04);
border-radius: 5px;
}

.speech-bubble:after {
content: '';
position: absolute;
top: 100%;
right: 30px;
border: 13px solid transparent;
border-top-color: rgba(0, 0, 0, 0.04);
}

.learn-bar > .learn {
position: absolute;
width: 272px;
top: 8px;
left: -300px;
padding: 10px;
border-radius: 5px;
background-color: rgba(255, 255, 255, 0.6);
transition-property: left;
transition-duration: 500ms;
}

@media (min-width: 899px) {
.learn-bar {
width: auto;
padding-left: 300px;
}

.learn-bar > .learn {
left: 8px;
}
}

styles/index.css

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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
html,
body {
margin: 0;
padding: 0;
}

button {
margin: 0;
padding: 0;
border: 0;
background: none;
font-size: 100%;
vertical-align: baseline;
font-family: inherit;
font-weight: inherit;
color: inherit;
-webkit-appearance: none;
appearance: none;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

body {
font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
line-height: 1.4em;
background: #f5f5f5;
color: #111111;
min-width: 230px;
max-width: 550px;
margin: 0 auto;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-weight: 300;
}

:focus {
outline: 0;
}

.hidden {
display: none;
}

.todoapp {
background: #fff;
margin: 130px 0 40px 0;
position: relative;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
}

.todoapp input::-webkit-input-placeholder {
font-style: italic;
font-weight: 300;
color: rgba(0, 0, 0, 0.4);
}

.todoapp input::-moz-placeholder {
font-style: italic;
font-weight: 300;
color: rgba(0, 0, 0, 0.4);
}

.todoapp input::input-placeholder {
font-style: italic;
font-weight: 300;
color: rgba(0, 0, 0, 0.4);
}

.todoapp h1 {
position: absolute;
top: -140px;
width: 100%;
font-size: 80px;
font-weight: 200;
text-align: center;
color: #b83f45;
-webkit-text-rendering: optimizeLegibility;
-moz-text-rendering: optimizeLegibility;
text-rendering: optimizeLegibility;
}

.new-todo,
.edit {
position: relative;
margin: 0;
width: 100%;
font-size: 24px;
font-family: inherit;
font-weight: inherit;
line-height: 1.4em;
color: inherit;
padding: 6px;
border: 1px solid #999;
box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
box-sizing: border-box;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

.new-todo {
padding: 16px 16px 16px 60px;
border: none;
background: rgba(0, 0, 0, 0.003);
box-shadow: inset 0 -2px 1px rgba(0, 0, 0, 0.03);
}

.main {
position: relative;
z-index: 2;
border-top: 1px solid #e6e6e6;
}

.toggle-all {
width: 1px;
height: 1px;
border: none; /* Mobile Safari */
opacity: 0;
position: absolute;
right: 100%;
bottom: 100%;
}

.toggle-all + label {
width: 60px;
height: 34px;
font-size: 0;
position: absolute;
top: -52px;
left: -13px;
-webkit-transform: rotate(90deg);
transform: rotate(90deg);
}

.toggle-all + label:before {
content: '❯';
font-size: 22px;
color: #e6e6e6;
padding: 10px 27px 10px 27px;
}

.toggle-all:checked + label:before {
color: #737373;
}

.todo-list {
margin: 0;
padding: 0;
list-style: none;
}

.todo-list li {
position: relative;
font-size: 24px;
border-bottom: 1px solid #ededed;
}

.todo-list li:last-child {
border-bottom: none;
}

.todo-list li.editing {
border-bottom: none;
padding: 0;
}

.todo-list li.editing .edit {
display: block;
width: calc(100% - 43px);
padding: 12px 16px;
margin: 0 0 0 43px;
}

.todo-list li.editing .view {
display: none;
}

.todo-list li .toggle {
text-align: center;
width: 40px;
/* auto, since non-WebKit browsers doesn't support input styling */
height: auto;
position: absolute;
top: 0;
bottom: 0;
margin: auto 0;
border: none; /* Mobile Safari */
-webkit-appearance: none;
appearance: none;
}

.todo-list li .toggle {
opacity: 0;
}

.todo-list li .toggle + label {
/*
Firefox requires `#` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433
IE and Edge requires *everything* to be escaped to render, so we do that instead of just the `#` - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/
*/
background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E');
background-repeat: no-repeat;
background-position: center left;
}

.todo-list li .toggle:checked + label {
background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E');
}

.todo-list li label {
word-break: break-all;
padding: 15px 15px 15px 60px;
display: block;
line-height: 1.2;
transition: color 0.4s;
font-weight: 400;
color: #4d4d4d;
}

.todo-list li.completed label {
color: #cdcdcd;
text-decoration: line-through;
}

.todo-list li .destroy {
display: none;
position: absolute;
top: 0;
right: 10px;
bottom: 0;
width: 40px;
height: 40px;
margin: auto 0;
font-size: 30px;
color: #cc9a9a;
margin-bottom: 11px;
transition: color 0.2s ease-out;
}

.todo-list li .destroy:hover {
color: #af5b5e;
}

.todo-list li .destroy:after {
content: '×';
}

.todo-list li:hover .destroy {
display: block;
}

.todo-list li .edit {
display: none;
}

.todo-list li.editing:last-child {
margin-bottom: -1px;
}

.footer {
padding: 10px 15px;
height: 20px;
text-align: center;
font-size: 15px;
border-top: 1px solid #e6e6e6;
}

.footer:before {
content: '';
position: absolute;
right: 0;
bottom: 0;
left: 0;
height: 50px;
overflow: hidden;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 0 8px 0 -3px #f6f6f6, 0 9px 1px -3px rgba(0, 0, 0, 0.2), 0 16px 0 -6px #f6f6f6, 0 17px 2px -6px rgba(0, 0, 0, 0.2);
}

.todo-count {
float: left;
text-align: left;
}

.todo-count strong {
font-weight: 300;
}

.filters {
margin: 0;
padding: 0;
list-style: none;
position: absolute;
right: 0;
left: 0;
}

.filters li {
display: inline;
}

.filters li a {
color: inherit;
margin: 3px;
padding: 3px 7px;
text-decoration: none;
border: 1px solid transparent;
border-radius: 3px;
}

.filters li a:hover {
border-color: rgba(175, 47, 47, 0.1);
}

.filters li a.selected {
border-color: rgba(175, 47, 47, 0.2);
}

.clear-completed,
html .clear-completed:active {
float: right;
position: relative;
line-height: 20px;
text-decoration: none;
cursor: pointer;
}

.clear-completed:hover {
text-decoration: underline;
}

.info {
margin: 65px auto 0;
color: #4d4d4d;
font-size: 11px;
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
text-align: center;
}

.info p {
line-height: 1;
}

.info a {
color: inherit;
text-decoration: none;
font-weight: 400;
}

.info a:hover {
text-decoration: underline;
}

/*
Hack to remove background from Mobile Safari.
Can't use it globally since it destroys checkboxes in Firefox
*/
@media screen and (-webkit-min-device-pixel-ratio: 0) {
.toggle-all,
.todo-list li .toggle {
background: none;
}

.todo-list li .toggle {
height: 40px;
}
}

@media (max-width: 430px) {
.footer {
height: 50px;
}

.filters {
bottom: 10px;
}
}

列表展示

  1. 准备数据并遍历。

  2. 处理 li 上的 completed class,处理 input 上的选中状态(v-model)。

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
<template>
<section class="todoapp">
<header class="header">
<h1>todos</h1>
<input class="new-todo" placeholder="What needs to be done?" 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">
<!-- #1 class 在处理 -->
<li v-for="item in list" :key="item.id" :class="{ completed: item.flag }">
<div class="view">
<!-- #2 选中状态的处理 -->
<input class="toggle" type="checkbox" v-model="item.flag" />
<!-- #3 name -->
<label>{{ item.name }}</label>
<button class="destroy"></button>
</div>
<input class="edit" value="Create a TodoMVC template" />
</li>
</ul>
</section>

<footer class="footer">
<span class="todo-count"><strong>0</strong> item left</span>
<ul class="filters">
<li>
<a class="selected" href="#/">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>
</template>

<script>
import { reactive, toRefs } from 'vue'
export default {
setup() {
const state = reactive({
list: [
{ id: 1, name: '吃饭', flag: true },
{ id: 2, name: '睡觉', flag: false },
{ id: 3, name: '打豆豆', flag: true },
],
})
return {
...toRefs(state),
}
},
}
</script>

删除功能

  1. 准备根据 id 删除的方法并 return(可以使用 filter 删除,或根据 id 找索引,根据索引去 splice)。

  2. 给删除按钮绑定点击事件,调用方法并传递 id。

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
<template>
<section class="todoapp">
<header class="header">
<h1>todos</h1>
<input class="new-todo" placeholder="What needs to be done?" 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 v-for="item in list" :key="item.id" :class="{ completed: item.flag }">
<div class="view">
<input class="toggle" type="checkbox" v-model="item.flag" />
<label>{{ item.name }}</label>
<!-- #3 -->
<button @click="delTodo(item.id)" class="destroy"></button>
</div>
<input class="edit" value="Create a TodoMVC template" />
</li>
</ul>
</section>

<footer class="footer">
<span class="todo-count"><strong>0</strong> item left</span>
<ul class="filters">
<li>
<a class="selected" href="#/">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>
</template>

<script>
import { reactive, toRefs } from 'vue'
export default {
setup() {
const state = reactive({
list: [
{ id: 1, name: '吃饭', flag: true },
{ id: 2, name: '睡觉', flag: false },
{ id: 3, name: '打豆豆', flag: true },
],
})
// #1
const delTodo = (id) => {
state.list = state.list.filter((item) => item.id !== id)
}
return {
...toRefs(state),
// #2
delTodo,
}
},
}
</script>

添加功能

  1. 在 state 中准备状态 todoName,通过 v-model 和 input 框进行绑定,收集数据。

  2. 监听 input 框的 @keyup.enter 事件,在事件回调中进行添加的操作。

  3. 添加完毕后清空输入的内容。

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
75
<template>
<section class="todoapp">
<header class="header">
<h1>todos</h1>
<!-- #2: v-model='todoName' -->
<!-- #4: @keyup.enter -->
<input class="new-todo" placeholder="What needs to be done?" autofocus v-model="todoName" @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="item in list" :key="item.id" :class="{ completed: item.flag }">
<div class="view">
<input class="toggle" type="checkbox" v-model="item.flag" />
<label>{{ item.name }}</label>
<button @click="delTodo(item.id)" class="destroy"></button>
</div>
<input class="edit" value="Create a TodoMVC template" />
</li>
</ul>
</section>

<footer class="footer">
<span class="todo-count"><strong>0</strong> item left</span>
<ul class="filters">
<li>
<a class="selected" href="#/">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>
</template>

<script>
import { reactive, toRefs } from 'vue'
export default {
setup() {
const state = reactive({
list: [
{ id: 1, name: '吃饭', flag: true },
{ id: 2, name: '睡觉', flag: false },
{ id: 3, name: '打豆豆', flag: true },
],
// #1
todoName: '',
})
const delTodo = (id) => {
state.list = state.list.filter((item) => item.id !== id)
}
// #3
const addTodo = () => {
state.list.unshift({
id: +new Date(),
name: state.todoName,
flag: false,
})
state.todoName = ''
}
return {
...toRefs(state),
delTodo,
addTodo,
}
},
}
</script>

底部功能

  1. 利用计算属性,统计左侧剩余数量leftCounts

  2. 利用计算属性,根据 state.list 的长度是否大于 0,来控制底部栏的显示与否,isShowFooter

  3. 利用计算属性,有已完成数据时,才显示清除已完成按钮(考虑使用 some 方法),isShowClear

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
75
76
77
78
79
80
81
82
83
84
85
86
<template>
<section class="todoapp">
<header class="header">
<h1>todos</h1>
<input class="new-todo" placeholder="What needs to be done?" autofocus v-model="todoName" @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="item in list" :key="item.id" :class="{ completed: item.flag }">
<div class="view">
<input class="toggle" type="checkbox" v-model="item.flag" />
<label>{{ item.name }}</label>
<button @click="delTodo(item.id)" class="destroy"></button>
</div>
<input class="edit" value="Create a TodoMVC template" />
</li>
</ul>
</section>
<!-- #1 -->
<footer class="footer" v-if="isShowFooter">
<!-- #2 -->
<span class="todo-count"><strong>{{ leftCounts }}</strong> item left</span>
<ul class="filters">
<li>
<a class="selected" href="#/">All</a>
</li>
<li>
<a href="#/active">Active</a>
</li>
<li>
<a href="#/completed">Completed</a>
</li>
</ul>
<!-- #3 -->
<button v-if="isShowClear" class="clear-completed">Clear completed</button>
</footer>
</section>
</template>

<script>
import { computed, reactive, toRefs } from 'vue'
export default {
setup() {
const state = reactive({
list: [
{ id: 1, name: '吃饭', flag: true },
{ id: 2, name: '睡觉', flag: false },
{ id: 3, name: '打豆豆', flag: true },
],
todoName: '',
})
const delTodo = (id) => {
state.list = state.list.filter((item) => item.id !== id)
}
const addTodo = () => {
state.list.unshift({
id: +new Date(),
name: state.todoName,
flag: false,
})
state.todoName = ''
}

const leftCounts = computed(() => {
return state.list.filter((item) => item.flag === false).length
})
const isShowFooter = computed(() => {
return state.list.length > 0
})
const isShowClear = computed(() => {
return state.list.some((item) => item.flag === true)
})
return {
...toRefs(state),
delTodo,
addTodo,
leftCounts,
isShowFooter,
isShowClear,
}
},
}
</script>

代码优化

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
export default {
setup() {
const state = reactive({
list: [
{ id: 1, name: '吃饭', flag: true },
{ id: 2, name: '睡觉', flag: false },
{ id: 3, name: '打豆豆', flag: true },
],
todoName: '',
})
const delTodo = (id) => {
state.list = state.list.filter((item) => item.id !== id)
}
const addTodo = () => {
state.list.unshift({
id: +new Date(),
name: state.todoName,
flag: false,
})
state.todoName = ''
}
const computedData = {
leftCounts: computed(() => {
return state.list.filter((item) => item.flag === false).length
}),
isShowFooter: computed(() => {
return state.list.length > 0
}),
isShowClear: computed(() => {
return state.list.some((item) => item.flag === true)
}),
}
return {
...toRefs(state),
delTodo,
addTodo,
...computedData,
}
},
}

清除已完成功能

1
2
3
const clearCompleted = () => {
state.list = state.list.filter((item) => !item.flag)
}

全选反选

  1. 利用计算属性,确定全选的状态(考虑使用 every 方法),isAll

  2. 通过 v-model 把 isAll 和 全选框进行绑定。

  3. 监听 isAll 计算属性的 set 操作,根据新值来控制所有单选按钮的状态。

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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
<template>
<section class="todoapp">
<header class="header">
<h1>todos</h1>
<input class="new-todo" placeholder="What needs to be done?" autofocus v-model="todoName" @keyup.enter="addTodo" />
</header>

<section class="main">
<!-- #2 -->
<input id="toggle-all" class="toggle-all" type="checkbox" v-model="isAll" />
<label for="toggle-all">Mark all as complete</label>
<ul class="todo-list">
<li v-for="item in list" :key="item.id" :class="{ completed: item.flag }">
<div class="view">
<input class="toggle" type="checkbox" v-model="item.flag" />
<label>{{ item.name }}</label>
<button @click="delTodo(item.id)" class="destroy"></button>
</div>
<input class="edit" value="Create a TodoMVC template" />
</li>
</ul>
</section>
<footer class="footer" v-if="isShowFooter">
<span class="todo-count"><strong>{{ leftCounts }}</strong> item left</span>
<ul class="filters">
<li>
<a class="selected" href="#/">All</a>
</li>
<li>
<a href="#/active">Active</a>
</li>
<li>
<a href="#/completed">Completed</a>
</li>
</ul>
<button @click="clearCompleted" v-if="isShowClear" class="clear-completed">Clear completed</button>
</footer>
</section>
</template>

<script>
import { computed, reactive, toRefs } from 'vue'
export default {
setup() {
const state = reactive({
list: [
{ id: 1, name: '吃饭', flag: true },
{ id: 2, name: '睡觉', flag: false },
{ id: 3, name: '打豆豆', flag: true },
],
todoName: '',
})
const delTodo = (id) => {
state.list = state.list.filter((item) => item.id !== id)
}
const addTodo = () => {
state.list.unshift({
id: +new Date(),
name: state.todoName,
flag: false,
})
state.todoName = ''
}
const computedData = {
leftCounts: computed(() => {
return state.list.filter((item) => item.flag === false).length
}),
isShowFooter: computed(() => {
return state.list.length > 0
}),
isShowClear: computed(() => {
return state.list.some((item) => item.flag === true)
}),
// #1
isAll: computed({
get() {
// 必须每一项都选中,才选中
return state.list.every((item) => item.flag === true)
},
set(val) {
// 一旦设置了全选的状态,无论改成true/false, 让下面所有的小选框都要同步
state.list.forEach((item) => (item.flag = val))
},
}),
}
const clearCompleted = () => {
state.list = state.list.filter((item) => !item.flag)
}
return {
...toRefs(state),
delTodo,
addTodo,
...computedData,
clearCompleted,
}
},
}
</script>

Tab 切换

  1. 在 state 中准备 Tabs 数据(['all', 'active', 'completed'])并动态渲染出底部按钮。

  2. 在 state 中准备 active 数据,默认是 ‘all’,和循环时候的 tab 进行比较,如果一样则应用 selected class。

  3. 给每一个 Tab 绑定点击事件,并修改默认的 active 为当前点击的 tab。

  4. 利用计算属性,根据 active 的值,计算出 renderList,把之前循环的 list 改为 renderList。

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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
<template>
<section class="todoapp">
<header class="header">
<h1>todos</h1>
<input class="new-todo" placeholder="What needs to be done?" autofocus v-model="todoName" @keyup.enter="addTodo" />
</header>

<section class="main">
<input id="toggle-all" class="toggle-all" type="checkbox" v-model="isAll" />
<label for="toggle-all">Mark all as complete</label>
<ul class="todo-list">
<!-- #5 -->
<li v-for="item in renderList" :key="item.id" :class="{ completed: item.flag }">
<div class="view">
<input class="toggle" type="checkbox" v-model="item.flag" />
<label>{{ item.name }}</label>
<button @click="delTodo(item.id)" class="destroy"></button>
</div>
<input class="edit" value="Create a TodoMVC template" />
</li>
</ul>
</section>
<footer class="footer" v-if="isShowFooter">
<span class="todo-count"><strong>{{ leftCounts }}</strong> item left</span>
<ul class="filters">
<!-- #2: 循环 -->
<!-- #3: 绑定事件 -->
<li v-for="tab in tabs" :key="tab" @click="active = tab">
<a :class="tab === active ? 'selected' : ''" href="#/">{{ tab }}</a>
</li>
</ul>
<button @click="clearCompleted" v-if="isShowClear" class="clear-completed">Clear completed</button>
</footer>
</section>
</template>

<script>
import { computed, reactive, toRefs, watch } from 'vue'
export default {
setup() {
const state = reactive({
list: [
{ id: 1, name: '吃饭', flag: true },
{ id: 2, name: '睡觉', flag: false },
{ id: 3, name: '打豆豆', flag: true },
],
todoName: '',
// #1
tabs: ['all', 'active', 'completed'],
active: 'all',
})
const delTodo = (id) => {
state.list = state.list.filter((item) => item.id !== id)
}
const addTodo = () => {
state.list.unshift({
id: +new Date(),
name: state.todoName,
flag: false,
})
state.todoName = ''
}
const computedData = {
leftCounts: computed(() => {
return state.list.filter((item) => item.flag === false).length
}),
isShowFooter: computed(() => {
return state.list.length > 0
}),
isShowClear: computed(() => {
return state.list.some((item) => item.flag === true)
}),
isAll: computed({
get() {
// 必须每一项都选中,才选中
return state.list.every((item) => item.flag === true)
},
set(val) {
// 一旦设置了全选的状态,无论改成true/false, 让下面所有的小选框都要同步
state.list.forEach((item) => (item.flag = val))
},
}),
// #4
renderList: computed(() => {
if (state.active === 'active') {
return state.list.filter((item) => !item.flag)
} else if (state.active === 'completed') {
return state.list.filter((item) => item.flag)
} else {
return state.list
}
}),
}
const clearCompleted = () => {
state.list = state.list.filter((item) => !item.flag)
}

return {
...toRefs(state),
delTodo,
addTodo,
...computedData,
clearCompleted,
}
},
}
</script>

存储本地

  1. 深度监听 () => state.list 的变化,在回调函数中对新数据进行序列化后并存储到本地。

  2. 初始化 list 的时候,从本地获取,并反序列化,没有获取到给一个默认值,防止循环的时候报错。

  3. 监听 () => state.active 的变化,回调函数中把变化后的新值存储到本地。

  4. 初始哈 active 的时候,从本地获取,没有获取到给一个默认的 ‘all’。

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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
<template>
<section class="todoapp">
<header class="header">
<h1>todos</h1>
<input class="new-todo" placeholder="What needs to be done?" autofocus v-model="todoName" @keyup.enter="addTodo" />
</header>

<section class="main">
<input id="toggle-all" class="toggle-all" type="checkbox" v-model="isAll" />
<label for="toggle-all">Mark all as complete</label>
<ul class="todo-list">
<li v-for="item in renderList" :key="item.id" :class="{ completed: item.flag }">
<div class="view">
<input class="toggle" type="checkbox" v-model="item.flag" />
<label>{{ item.name }}</label>
<button @click="delTodo(item.id)" class="destroy"></button>
</div>
<input class="edit" value="Create a TodoMVC template" />
</li>
</ul>
</section>
<footer class="footer" v-if="isShowFooter">
<span class="todo-count"><strong>{{ leftCounts }}</strong> item left</span>
<ul class="filters">
<li v-for="tab in tabs" :key="tab" @click="active = tab">
<a :class="tab === active ? 'selected' : ''" href="#/">{{ tab }}</a>
</li>
</ul>
<button @click="clearCompleted" v-if="isShowClear" class="clear-completed">Clear completed</button>
</footer>
</section>
</template>

<script>
import { computed, reactive, toRefs, watch } from 'vue'
export default {
setup() {
const state = reactive({
// #2
list: JSON.parse(localStorage.getItem('todoList')) || [
{ id: 1, name: '吃饭', flag: true },
{ id: 2, name: '睡觉', flag: false },
{ id: 3, name: '打豆豆', flag: true },
],
todoName: '',
tabs: ['all', 'active', 'completed'],
// #4
active: localStorage.getItem('active') || 'all',
})
const delTodo = (id) => {
state.list = state.list.filter((item) => item.id !== id)
}
const addTodo = () => {
state.list.unshift({
id: +new Date(),
name: state.todoName,
flag: false,
})
state.todoName = ''
}
const computedData = {
leftCounts: computed(() => {
return state.list.filter((item) => item.flag === false).length
}),
isShowFooter: computed(() => {
return state.list.length > 0
}),
isShowClear: computed(() => {
return state.list.some((item) => item.flag === true)
}),
isAll: computed({
get() {
// 必须每一项都选中,才选中
return state.list.every((item) => item.flag === true)
},
set(val) {
// 一旦设置了全选的状态,无论改成true/false, 让下面所有的小选框都要同步
state.list.forEach((item) => (item.flag = val))
},
}),
renderList: computed(() => {
if (state.active === 'active') {
return state.list.filter((item) => !item.flag)
} else if (state.active === 'completed') {
return state.list.filter((item) => item.flag)
} else {
return state.list
}
}),
}
const clearCompleted = () => {
state.list = state.list.filter((item) => !item.flag)
}
// #1,注意是 () => state.list
watch(
() => state.list,
(newValue) => {
localStorage.setItem('todoList', JSON.stringify(newValue))
},
{
deep: true,
}
)
// #3
watch(
() => state.active,
(newValue) => {
localStorage.setItem('active', newValue)
}
)
return {
...toRefs(state),
delTodo,
addTodo,
...computedData,
clearCompleted,
}
},
}
</script>