10x3 精读Vue官方文档 - 示例 - TODO MVC 代码分析

422 阅读3分钟

精读 Vue 官方文档系列 🎉


个人认为一个 Todo 应用的核心在于数据的结构的定义:

{
    id:Number
    title:String,
    completed:Boolean
}

然后围绕这个数据结构进行 增、删、改、存就可。 先看 存储,官方示例封装了一个简易的 todoStorage 对象,并提供了 fetchsave 两个方法。


var todoStorage = {
    key:'TODOS_STORAGE_KEY',
    fetch(){
        var todos = JSON.parse(localStorage.getItem(this.key) || '[]');
        todos.forEach((todo, index)=>{
            todo.id = index;
        });
        todoStorage.uid = todos.length;
        return todos;
    },
    save(todos){
        return localStorage.setItem(this.key, JSON.stringify(todos));
    }
}

数据的存储,可以直接深度监听数据源本身的改变来自动处理,这样就不需要开发者考虑更多的交互场景了。

{
    data(){
        return {
            todos:todoStorage.fetch(),
            newTodo:'',
            editTodo:null,
            status:'all'
        }
    },
    watch:{
        todos:{
            handler(newValue){
                todoStorage.save(newValue);
            },
            deep:true
        }
    }
}

再看增、删、改中的**“增”**

在文本输入框中使用 v-model 来双向更新 this.newTodo 的值,再点击添加时,再对 this.newTodo 的值进行包装,然后将其加入到 this.todos 中,接着便是触发深度监听,进行数据的自动存储,最后再将 this.newTodo = ‘’ 置空,用于添加完成后,清空文本框中的输入记录。

{
    methods:{
        addTodo(){
            if (this.newTodo && this.newTodo.trim()) {
                var todo = {
                    id: todoStorage.uid++,
                    title: this.newTodo,
                    completed: false
                };
                this.todos.push(todo);
                this.newTodo = '';
            }
        }
    }
}

至于就很简单了,直接在 todos 的循环列表中,将当前 todo 对象传入到删除的方法中。

this.todos.splice(this.todos.indexOf(todo), 1)

接着,便是,如果是对 todos 循环列表中的每个 todo 选项都提供修改功能,则可以直接将当前的 todo 选项的 title 双向绑定到输入框中,这样更简洁,优雅。

<input v-if="editTodo === todo" v-todo-focus="editTodo === todo" v-model="todo.title" @keydown.enter="doneEditTodo(todo)" @keyup.esc="cancelEditTodo(todo)" />
<span v-if="!editTodo" @dbclick="editTodo(todo)">{{todo.title}}</span>

为了以防用户修改标题后,但是没有保存,而是又取消了,所以在触发修改操作的时候,还得事先提前保存当前 todo 选项的 title,因为存储值的变量不需要是响应式对象,所以这里我们直接在组件实例上动态新增一个属性。

{
    methods:{
        editTodo(todo){
            this.cacheTodoTitle = todo.title;
            this.editTodo = todo;
        }
    }
}

当用户触发修改,但是输入的过程中又点击取消时,则从 cacheTodoTitle 中还原之前的值。

{
    methods:{
        cancelEditTodo(todo){
            todo.title = this.cacheTodoTitle;
            this.editTodo = null;
        }
    }
}

若是用户修改完成,点击保存,触发 doneEditTodo(todo) 方法即可,因为数据的保存是深度监听的,都是自动完成的,不过我们可以在这个方法里面做一些完善操作。

{
    methods:{
        doneEditTodo(todo){
            todo.title = todo.title.trim();
            if(!todo.title){
                this.removeTodo(todo)
            }
            this.editTodo = null;
        }
    }
}

最后,还有两点值得仔细讨论。

todos 列表循环渲染以及状态筛选

就像前面的示例那样,我们更推荐在计算属性中进行条件或状态的筛选,而且循环渲染列表的数据也不是数据源本身,而是用计算属性来作循环渲染列表。

var filters = {
    all(todos){
        return todos;
    },
    active(todos){
        return todos.filter(todo=>!todo.completed);
    },
    completed(todos){
        return todos.filter(todo=>todo.completed);
    }
};

{
    computed:{
        filtered({todos}){
            return filters[this.visibility](todos);
        }
    }
}

visibility 是一个变量,它的值是 filters 对象的 key

由于这里的筛选条件是互斥的,不能聚合或者是并列处理,所以官方实例中将它们封装到一个 filters 对象中,然后在计算属性中通过响应式变量 this.visibility 来间接触发,这样,如果筛选条件变化了,就会触发计算属性来筛选数据,而数据源变化了,也会触发计算属性,重新进行筛选。

<li v-for="todo in filtered"  :key="todo.id">
    <input v-if="editTodo === todo" v-model="todo.title" @keydown.enter="doneEditTodo(todo)"                        @keyup.esc="cancelEditTodo(todo)" />
    <span v-if="!editTodo" @dbclick="editTodo(todo)">{{todo.title}}</span>
</li>

全部完成功能

全部完成功能的实现方式有很多:

  • 监听事件、然后执行方法、将所有 todocompleted 置为 true
  • v-model 更新一个响应式变量的状态,然后监听这个响应式数据,执行相应的处理。

这里,官方示例给出了更简洁的解决方案,完整的只使用一个计算属性就可以完成全部完成的功能。

{
    computed:{
        remaining(){
            return filters.active(this.todos).length;
        },
        allDone:{
            set(value){
                this.todos.forEach(todo=>{
                    todo.completed = value;
                });
            },
            get(){
                return this.remaining === 0;
            }
        }
    }
}
<input type="checkbox" v-model="allDone" />

最后,还有一个自定义指令:

{
 directives: {
      "todo-focus": function(el, binding) {
        if (binding.value) {
          el.focus();
        }
      }
    }
}