发布时间:2025-12-10 19:31:22 浏览次数:6
使用Vue3开发TodoMVC[通俗易懂]最近在学习Vue3.0的一些新特性,就想着使用Vue3来编写一个todoMVC的示例。本示例是模仿官网的TodoMVC,但是本示例中所有代码都是使用了Vue3的语法。功能上基本上实现了,不过官方的示例上使用了LocalStorage本地缓存来缓存数据,我在本示例中没有使用。另外ui样式我没有完全
最近在学习Vue3.0的一些新特性,就想着使用Vue3来编写一个todoMVC的示例。本示例是模仿官网的TodoMVC,但是本示例中所有代码都是使用了Vue3的语法。
功能上基本上实现了,不过官方的示例上使用了Local Storage本地缓存来缓存数据,我在本示例中没有使用。另外ui样式我没有完全还原,也算是偷下懒吧。
官网示例:https://cn.vuejs.org/v2/examples/todomvc.html
先来看一下效果
主要用到Vue3的conposition API有:ref, reactive, computed, watchEffect, watch, toRefs, nextTick,
功能我就不细讲了,后面会附上完整代码,主要讲几点在开发过程中遇到的问题,也是Vue3中的一些小改动的问题。
在官网示例中是使用了自定义指令去完成的,先自己定义一个自定义指令,之后再在input标签中去使用
<input type="text" v-model="todo.title" v-todo-focus="todo == editedTodo" @blur="doneEdit(todo)" @keyup.enter="doneEdit(todo)" @keyup.esc="cancelEdit(todo)"/>希望我今天分享的这篇文章可以帮到您。
directives: { "todo-focus": function(el, binding) { if (binding.value) { el.focus(); } }}而我在本例中想使用ref来获取dom元素,从而触发input的onfocus事件。
我们知道在Vue2.x中可以使用this.$refs.xxx来获取到对应的dom元素,可是Vue3.0中是没办法使用这种方法去获取的。
查阅了Vue3.0官方文档之后,发现Vue3对ref的使用做了修改。
Vue2:<p ref="myRef"></p><script>this.$refs.myRef</script>Vue3:<template> <p ref="myRef">获取DOM元素</p></template><script>import { ref, onMounted, nextTick } from 'vue';export default {//方式1: setup() { const myRef = ref(null); onMounted(() => { console.dir(myRef); }) return { myRef }; } //方式2: setup() { let myRef = ''; const setRef = el => { myRef = el; } nextTick(() => { console.dir(myRef); }) return { setRef }; } };</script>而对在v-for中使用ref,Vue3不再在 $ref 中自动创建数组,而是需要用一个函数来绑定。(参考文档:https://composition-api.vuejs.org/zh/api.html#模板-refs)
<p v-for="item in list" :ref="setItemRef"></p>//Vue2export default { data() { return { itemRefs: [] } }, methods: { setItemRef(el) { this.itemRefs.push(el) } }}//Vue3import { ref } from 'vue'export default { setup() { let itemRefs = [] const setItemRef = el => { itemRefs.push(el) } onBeforeUpdate(() => { itemRefs = [] }) onUpdated(() => { console.log(itemRefs) }) return { itemRefs, setItemRef } }}在本例中使用了另一种写法,也是一样。
<input v-show="item.isEdit" :ref="(el) => (editRefList[item.id] = el)" type="text" v-model="itemInputValue" @blur="handleBlur(item)"/>setup() { const editRefList = ref([]); watchEffect(async () => { if (state.itemInputValue) { await nextTick(); editRefList.value[state.currentTodoId].focus(); } }); return {editRefList}}在Vue2中我们会这样使用nextTick
this.$nextTick(()=> { //获取更新后的DOM})而在Vue3中这样使用
import { createApp, nextTick } from 'vue'const app = createApp({ setup() { const message = ref('Hello!') const changeMessage = async newMessage => { message.value = newMessage // 这里获取DOM的value是旧值 await nextTick() // nextTick 后获取DOM的value是更新后的值 console.log('Now DOM is updated') } }})vue3中新增了watchEffect的方法,也是可以用来监听数据。watchEffect() 会立即执行传入的函数,并响应式侦听其依赖,并在其依赖变更时重新运行该函数。
const count = ref(0)// 初次直接执行,打印出 0watchEffect(() => console.log(count.value))setTimeout(() => { // 被侦听的数据发生变化,触发函数打印出 1 count.value++}, 1000)停止侦听watchEffect() 使用时返回一个函数,当执行这个返回的函数时,就停止侦听。
const stop = watchEffect(() => { /* ... */})// 停止侦听stop()watch的写法与vue2稍稍有点不同
watch 侦听单个数据源侦听的数据可以是个 reactive 创建出的响应式数据(拥有返回值的 getter 函数),也可以是个 ref
watch接收三个参数:参数1:监听的数据源,可以是一个ref获取是一个函数参数2:回调函数(val, oldVal)=> {}参数3:额外的配置 是一个是对象时进行深度监听,添加 { deep:true, immediate: true}// 侦听一个 getterconst state = reactive({ count: 0 })watch( () => state.count, (count, prevCount) => { /* ... */ }, { deep:true, immediate: true})// 直接侦听一个 refconst count = ref(0)watch(count, (count, prevCount) => { /* ... */})watch 侦听多个数据源在侦听多个数据源时,把参数以数组的形式给 watch
watch([ref1, ref2], ([newRef1, newRef2], [prevRef1, prevRef2]) => { /* ... */})本例也是自己刚接触vue3之后写的,可能写的并不是很好,如果有哪里有错误或者可优化的请多多指导。
完整代码
<template><p ><h1>todos</h1><p ><section ><inputtype="text"autofocusautocomplete="off"placeholder="What needs to be done?"v-model="inputValue"@keyup.enter="handleAddTodo($event.target)"/></section><inputv-show="todoList.length"type="checkbox"v-model="isSelectAll":/><ul v-show="filterTodoList.length"><li@mouseenter="mouseEnter(item)"@mouseleave="mouseLeave(item)"v-for="(item,index) in filterTodoList":key="item.id"><p v-show="!item.isEdit" ><inputtype="checkbox":checked="item.isCompleted"@change="handleChangeCheckbox(item)"/><p:@dblclick="dbClick(item)">{{ item.content }}</p><spanv-show="item.isActive && !item.isEdit"@click="handleDelete(index)">X</span></p><!-- 使用v-for循环时, 使用ref总会获取到的是最后的元素, 必须使用函数, 手动赋值 --><inputv-show="item.isEdit":ref="(el) => (editRefList[item.id] = el)"type="text"v-model="itemInputValue"@blur="handleBlur(item)"/></li></ul><section v-show="todoList.length"><span>{{ isActiveTodos.length }} items left</span><p ><button:v-for="(button, index) in statusButtons"@click="handleChange(button)":key="index">{{ button }}</button></p><pv-show="isCompletedTodos.length"@click="handleClear">Clear completed</p></section></p></p></template><script lang="ts">import {ref,reactive,computed,watchEffect,watch,toRefs,nextTick,} from "vue";export default {name: "todoList",setup() {const state = reactive({inputValue: "",todoList: [],itemInputValue: "",todoId: 0,currentTodoId: 0,status: "All",statusButtons: ["All", "Active", "Completed"],});// 自动获取input焦点// 因为在循环里,所以要定义一个ref数组,然后根据id来获取当前input的焦点const editRefList = ref([]);watchEffect(async () => {if (state.itemInputValue) {await nextTick();editRefList.value[state.currentTodoId].focus();}});//或者用watch也可以// watch(// () => state.itemInputValue,// async (val) => {// if(val) {// await nextTick();// editRefList.value[state.currentTodoId].focus();// }// },// {// immediate: true// }// );//vue3.0去除了filter过滤器,官方建议用计算属性或方法代替过滤器。const filterTodoList = computed(() => {switch (state.status) {case "All":return state.todoList;break;case "Active":return isActiveTodos.value;break;case "Completed":return isCompletedTodos.value;break;}});const isActiveTodos = computed(() =>state.todoList.filter((item) => !item.isCompleted));const isCompletedTodos = computed(() =>state.todoList.filter((item) => item.isCompleted));const isSelectAll = computed({get: () => isActiveTodos.value.length === 0 && !!state.todoList.length,set: (val) => {state.todoList.forEach((todo) => {todo.isCompleted = val;});},});// 添加todoconst handleAddTodo = (e) => {//如果输入内容为空则立即返回if (e.value === "") {return;}state.todoList.push({id: state.todoId++,content: state.inputValue,isCompleted: false, //是否已完成isActive: false, //是否正在进行isEdit: false, //是否在编辑状态});state.inputValue = "";};// 删除单条const handleDelete = (index) => {state.todoList.splice(index, 1);};// 鼠标进入const mouseEnter = (item) => {item.isActive = true;};// 点击按钮改变todoList显示const handleChange = (status) => {state.status = status;};// 清空completed状态的todoconst handleClear = () => {state.todoList = isActiveTodos.value;};// 鼠标移出const mouseLeave = (item) => {item.isActive = false;};// 双击item编辑const dbClick = (item) => {state.itemInputValue = item.content;state.currentTodoId = item.id;item.isEdit = true;};// 失焦事件const handleBlur = (item) => {item.content = state.itemInputValue;item.isEdit = false;state.itemInputValue = "";};// 点击checkbox切换状态const handleChangeCheckbox = (item) => {item.isCompleted = !item.isCompleted;};return {...toRefs(state),handleAddTodo,handleDelete,handleClear,handleChangeCheckbox,mouseLeave,mouseEnter,dbClick,handleBlur,editRefList,isActiveTodos,isCompletedTodos,handleChange,filterTodoList,isSelectAll,};},};</script><style>* {padding: 0;margin: 0;box-sizing: border-box;}input {outline: none;}ul,li,ol {list-style: none;}::-webkit-input-placeholder {color: #d5d5d5;font-size: 25px;}.todo-list {display: flex;flex-direction: column;align-items: center;background-color: #f5f5f5;width: 100%;height: 500px;}h1 {margin: 10px;font-size: 100px;color: rgba(175, 47, 47, 0.15);}/* content部分样式 */.todo-list .todo-list-content {position: relative;width: 600px;box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);}.todo-list-content .content-input-box {display: flex;align-items: center;box-shadow: inset 0 -2px 1px rgba(0, 0, 0, 0.03);}.toggle-all {position: absolute;left: 42px;top: 27px;width: 0px;height: 0px;transform: rotate(90deg);cursor: pointer;}.toggle-all:before {content: "❯";font-size: 22px;color: #e6e6e6;}.toggle-all-active:before {color: #737373;}.todo-list-content .todo-input {font-size: 24px;width: 100%;padding: 16px 16px 16px 60px;border: 1px solid transparent;background: rgba(0, 0, 0, 0.003);}.content .list-item {width: 100%;display: flex;justify-content: space-between;align-items: center;font-size: 24px;border-bottom: 1px solid #ececec;}.list-item .edit-input {width: 100%;padding: 16px;margin-left: 42px;font-size: 24px;box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);border: 1px solid #999;}.list-item .list-item-box {display: flex;flex-direction: row;align-items: center;width: 100%;padding: 16px;}.list-item .checkbox {cursor: pointer;width: 20px;height: 20px;}.list-item .text {margin-left: 30px;width: 100%;text-align: left;}.list-item .delete-icon {color: red;cursor: pointer;}.complete {color: #d9d9d9;text-decoration: line-through;}/* footer部分样式 */.footer {padding: 12px 15px;display: flex;justify-content: space-between;}.footer .status-buttons {position: absolute;left: 50%;transform: translateX(-50%);}.footer .status-buttons button {padding: 2px 8px;margin-left: 5px;}.footer .clear-button {cursor: pointer;}.active-status-button {background-color: #777;outline: -webkit-focus-ring-color auto 1px;}</style>