今日目标 ✔ 能够掌握 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 创建项目。
1 2 3 npm create vite yarn create vite
输入项目名字,默认为 vite-project。
选择创建的项目类型,选择 vue 即可。
选择创建的 Vue 项目类型,选择 vue。
了解使用 Vite 快速创建项目。
1 2 yarn create vite vite-demo --template vue
1 2 yarn create vite vite-demo-ts --template vue-ts
下面是旧版本的写法,不建议。
1 2 3 4 5 6 npm init vite-app <project-name> cd <project-name>npm install npm run dev
小结 Vite 是什么?
编写 Vue 应用 目标 掌握如何创建 Vue3 应用实例。
步骤
清空 src 里面的所有内容。
在 src/main.js
中按需导入 createApp
函数。
定义 App.vue
根组件,导入到 main.js
。
使用 createApp
函数基于 App.vue
根组件创建应用实例。
挂载至 index.html
的 #app
容器。
main.js
1 2 3 4 5 6 7 8 9 10 11 import { createApp } from 'vue' import App from './App.vue' const app = createApp(App)app.mount('#app' )
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 > <div id ="app" > </div > <script type ="module" src ="/src/main.js" > </script > </body > </html >
小结 如何创建 Vue 应用实例?
安装开发工具
学习组合 API 选项/组合 API 目标 理解什么是 Options API 写法,什么是 Composition API 写法。
需求
Vue2
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 { x : 0 , y : 0 , count : 0 , } }, mounted ( ) { document .addEventListener('mousemove' , this .move) }, methods : { move (e ) { this .x = e.pageX this .y = e.pageY }, add ( ) { this .count++ }, }, destroyed ( ) { document .removeEventListener('mousemove' , this .move) }, } </script >
Vue3
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 ( ) { 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) }) 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 >
小结
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 >
点击计数
定义一个简单数据类型的响应式数据。
定义一个修改数字的方法。
在 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 ( ) { const data = ref(null ) setTimeout (() => { data.value = { name : 'ifer' , } }, 1000 ) const updateName = () => { data.value.name = 'xxx' } return { data, updateName } }, } </script >
如何选择
小结
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 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 = () => { 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 = 'xxx' } 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 }) 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 >
小结
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 ) => { console .log(newValue === oldValue) }) 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 : '西瓜' , }, }) watch(obj.hobby, (newValue, oldValue ) => { console .log(newValue === oldValue) }) 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 ) => { 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 ) 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++ } 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) }, { 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 : '西瓜' , }, }) watch(obj, (newValue, oldValue ) => { console .log(newValue === oldValue) }) return { obj } }, } </script >
解决 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 >
解决 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 , } )
解决 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 : '西瓜' , }, }) 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 /> <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' , props : { money : { type : Number , default : 0 , }, }, 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 ) 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 } ) { const changeMoney = (m ) => { emit('change-money' , m) } return { changeMoney } }, } </script >
小结
setup 第一个参数的是什么?
第二个参数 context 中包含什么信息?
provide/inject 目标 掌握使用 provide 函数和 inject 函数完成跨层级组件通讯。
内容
需求
📝 把 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 /> <Child :car ="car" /> </template >
Child.vue
1 2 3 4 5 6 7 8 9 10 11 <script setup > const props = defineProps({ car : String , }) 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 > 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" /> </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 > <div ref ="dom" > 我是box</div > </template > <script > import { onMounted, ref } from 'vue' export default { name : 'App' , setup ( ) { const dom = ref(null ) onMounted(() => { console .log(dom.value) }) 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 > <button @click ="changeName" > 修改子组件的 Name</button > <hr /> <Test ref ="test" /> </template > <script > import { ref } from 'vue' import Test from './Test.vue' export default { name : 'App' , components : { Test, }, setup ( ) { const test = ref(null ) const changeName = () => { test.value.changeName('elser' ) } 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
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 迁移指南
全局 API 的变更,链接 。
data 只能是函数,链接 。
自定义指令 API 和组件保持一致,链接 。
keyCode 作为 v-on 修饰符被移除、移除 v-on.native 修饰符、filters 被移除 ,链接 。
$on、$off、$once 被移除,链接 。
过渡类名的更改,链接 。
…
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; 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 ; height : auto; position : absolute; top : 0 ; bottom : 0 ; margin : auto 0 ; border : none; -webkit-appearance: none; appearance: none; } .todo-list li .toggle { opacity : 0 ; } .todo-list li .toggle + 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%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; } @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 ; } }
列表展示
准备数据并遍历。
处理 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" > <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 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 >
删除功能
准备根据 id 删除的方法并 return(可以使用 filter 删除,或根据 id 找索引,根据索引去 splice)。
给删除按钮绑定点击事件,调用方法并传递 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 > <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 }, ], }) const delTodo = (id ) => { state.list = state.list.filter((item ) => item.id !== id) } return { ...toRefs(state), delTodo, } }, } </script >
添加功能
在 state 中准备状态 todoName,通过 v-model 和 input 框进行绑定,收集数据。
监听 input 框的 @keyup.enter
事件,在事件回调中进行添加的操作。
添加完毕后清空输入的内容。
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 > <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 }, ], 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 = '' } return { ...toRefs(state), delTodo, addTodo, } }, } </script >
底部功能
利用计算属性,统计左侧剩余数量 ,leftCounts 。
利用计算属性,根据 state.list 的长度是否大于 0,来控制底部栏的显示与否,isShowFooter 。
利用计算属性,有已完成数据时,才显示清除已完成按钮(考虑使用 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 > <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 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) }
全选反选
利用计算属性,确定全选的状态(考虑使用 every 方法),isAll 。
通过 v-model 把 isAll 和 全选框进行绑定。
监听 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" > <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 ) }), isAll : computed({ get ( ) { return state.list.every((item ) => item.flag === true ) }, set (val ) { 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 切换
在 state 中准备 Tabs 数据(['all', 'active', 'completed']
)并动态渲染出底部按钮。
在 state 中准备 active 数据,默认是 ‘all’,和循环时候的 tab 进行比较,如果一样则应用 selected class。
给每一个 Tab 绑定点击事件,并修改默认的 active 为当前点击的 tab。
利用计算属性,根据 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" > <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({ list : [ { id : 1 , name : '吃饭' , flag : true }, { id : 2 , name : '睡觉' , flag : false }, { id : 3 , name : '打豆豆' , flag : true }, ], todoName : '' , 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 ) { 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) } return { ...toRefs(state), delTodo, addTodo, ...computedData, clearCompleted, } }, } </script >
存储本地
深度监听 () => state.list
的变化,在回调函数中对新数据进行序列化后并存储到本地。
初始化 list 的时候,从本地获取,并反序列化,没有获取到给一个默认值,防止循环的时候报错。
监听 () => state.active
的变化,回调函数中把变化后的新值存储到本地。
初始哈 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({ 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' ], 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 ) { 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) } watch( () => state.list, (newValue ) => { localStorage .setItem('todoList' , JSON .stringify(newValue)) }, { deep : true , } ) watch( () => state.active, (newValue ) => { localStorage .setItem('active' , newValue) } ) return { ...toRefs(state), delTodo, addTodo, ...computedData, clearCompleted, } }, } </script >