likes
comments
collection
share

Vue3项目起步,冲冲冲!!!

作者站长头像
站长
· 阅读数 17

“我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第1篇文章,点击查看活动详情

今日目标

✔ 能够掌握 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 进行大型项目的开发,对于个人来说,学习流行的技术提升竞争力,加薪!

Vite 创建项目

Vite 基本使用

目标

  • 了解 Vite 是什么?

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

内容

  • 是什么:下一代前端开发与构建工具,热更新、打包构建速度更快,但目前周边生态还不如 Webpack 成熟,目前实际开发中还是以 Webpack 为主,但目前就学习 Vue3 语法来说,我们可以使用更轻量的 Vite

  • 对比 Webpack 和 Vite 如下。

    • Webpack:会将所有模块提前编译、打包,不管这个模块是否被用到,随着项目越来越大,打包启动速度自然越来越慢。
    • Vite:瞬间开启一个服务,并不会先编译所有文件,当浏览器用到某个文件时,Vite 服务会收到请求然后编译后响应到客户端。

Vue3项目起步,冲冲冲!!!

使用

(1)使用 Vite 创建项目。

npm create vite
# or
yarn create vite

(2)输入项目名字,默认为 vite-project。

(3)选择创建的项目类型,选择 vue 即可。

(4)选择创建的 Vue 项目类型,选择 vue。

Vue3项目起步,冲冲冲!!! 了解 Vite 快捷使用。

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

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

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

编写 Vue 应用

步骤

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

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

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

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

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

main.js

// 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

<template>
    <div class="container">我是根组件</div>
</template>
<script>
    export default {
        name: 'App',
    }
</script>

index.html

<!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>

安装开发工具

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

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

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

学习组合 API

选项/组合 API

需求

Vue3项目起步,冲冲冲!!!

Vue2

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

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

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

<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

Vue3项目起步,冲冲冲!!!

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

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

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

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

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

setup 入口函数

内容

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

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

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

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

<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 也可以返回一个渲染函数)

<script>
    import { h } from 'vue'
    export default {
        name: 'App',
        setup() {
            return () => h('h2', 'Hello Vue3')
        },
    }
</script>

小结

  • setup 的执行时机是什么?

早于created

  • setup 中的 this 指向是什么?

undefined

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

通过 return 进行返回可在模版中直接使用

reactive

reactive 包装数组

内容

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

需求

📝 点击删除当前行信息。

Vue3项目起步,冲冲冲!!!

<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 包装数组使变成响应式数据。

<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 包装对象

需求

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

Vue3项目起步,冲冲冲!!!

列表删除
<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>
添加功能
<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>
抽离函数

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

<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

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

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

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

<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 返回数据和函数,供模板中使用。

<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,例如后端返回的数据。

<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 中呢?

模板中使用不需要.value,但在setup中需要

toRef

内容

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

需求

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

<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 进行了导出,没必要,性能浪费。

问题

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

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

解决

<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 函数的作用:转换响应式对象中所有属性为单独响应式数据,并且转换后的值和之前是关联的。

需求

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

<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 函数用来定义计算属性。

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

高级

<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

监听 reactive 内部数据

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

<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 类型的数据。

<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 自身的修改则不会触发监听。

<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 的变化,做一些操作。

<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 的变化。

<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>
立即触发监听
<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 的。

<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)。
<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 数据。
watch(
    obj,
    (newValue, oldValue) => {
        console.log(newValue, oldValue)
        console.log(newValue === oldValue)
    },
    {
        deep: true,
    }
)
  1. 解决 3:还可以通过监听 ref.value 来实现同样的效果。

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

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

监听普通数据

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

<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 的写法在 Vue3 中也是支持。

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

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

Vue3项目起步,冲冲冲!!! App.vue

<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

<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 函数参数

需求

Vue3项目起步,冲冲冲!!!

父传子

App.vue

<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

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

子传父

Vue3项目起步,冲冲冲!!! App.vue

<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

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

provide/inject

内容

Vue3项目起步,冲冲冲!!!

需求

Vue3项目起步,冲冲冲!!! 📝 把 App.vue 中的数据传递给孙组件,Child.vue。

App.vue

<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

<template>
    <div>
        Parent
        <hr />
        <Child />
    </div>
</template>

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

Child.vue

<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 语法

文档链接

初体验

<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

<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

<script setup>
    const props = defineProps({
        car: String,
    })
    // 模板中可以直接使用 car
    // setup 中要通过下面的方式
    console.log(props.car)
</script>
<template>
    <div>car: {{ car }}</div>
</template>

Vue3 其他变更

v-model

基本操作

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

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

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

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

App.vue

<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

<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

<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>
<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 属性

内容

获取单个 DOM。

<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

<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

<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 组件点击按钮进行弹框。

Vue3项目起步,冲冲冲!!!

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

解决

<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. 过渡类名的更改,链接

  7. ...

Todos

静态结构

yarn create vite-app todos

main.js

import { createApp } from 'vue'
import './styles/base.css'
import './styles/index.css'
import App from './App.vue'

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

App.vue

<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

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

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)。

<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。

<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. 添加完毕后清空输入的内容。
<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。

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

代码优化

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,
        }
    },
}

清除已完成功能

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

全选反选

  1. 利用计算属性,确定全选的状态(考虑使用 every 方法),isAll。
  2. 通过 v-model 把 isAll 和 全选框进行绑定。
  3. 监听 isAll 计算属性的 set 操作,根据新值来控制所有单选按钮的状态。
<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。
<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'。
<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>