原稿:语雀 · 【Demo】Todo App · 原目录:Vue › 【Demo】Todo App

通过一个todo app的开发迭代,熟悉vue中各种基本语法的使用以及组件化的思想

v1.0 朴素的 todo

实现一个待办清单应用,首先只使用到vue的声明式渲染,即不与html交互,而是将数据存入vue实例,由实例接管选中的DOM,对其进行响应式渲染

<div id="app">
  {{message}}
  <ul>
    <li>{{todolist[0].time}}&emsp;{{todolist[0].todo}}&emsp;{{todolist[0].done}}</li>
    <li>{{todolist[1].time}}&emsp;{{todolist[1].todo}}&emsp;{{todolist[1].done}}</li>
    <li>{{todolist[2].time}}&emsp;{{todolist[2].todo}}&emsp;{{todolist[2].done}}</li>
  </ul>
</div>
<script>
  class TodoItem {
    constructor(time, todo, done = "未完成") {
      this.time = time;
      this.todo = todo;
      this.done = done;
    }
  }
  var app = new Vue({
    el: "#app",
    data: {
      message: "待办事项:",
      todolist: [
        new TodoItem("7:00", "起床", "已完成"),
        new TodoItem("8:00", "吃饭"),
        new TodoItem("9:00", "打豆豆"),
      ],
    },
  });
</script>

得益于vue.js的模板语法,使用双大括号{{}}和js表达式绑定的数据都能被直接渲染到视图中

image.png

v1.1 列表渲染:v-for

如果每一条待办都要手动使用<li>罗列,非常没有必要,vue提供列表渲染,允许使用 <v-for> 指令把一个数组对应为一组元素。该指令还支持第二个参数index,可以获取到当前元素的索引

<div id="app">
  {{message}}
  <ul>
    <li v-for="item in todolist">
      {{item.time}}&emsp;{{item.todo}}&emsp;{{item.done}}
    </li>
  </ul>
</div>

渲染效果不变


v1.2 条件渲染:v-if

上面的todo app中,是否完成任务的视觉区分并不明显,如果实现划线删除效果会更好:7:00 起床,借助条件渲染的 v-if/v-else/v-else-if 指令,可以根据不同情况渲染出不同效果

<div id="app">
  {{message}}
  <ul>
    <li v-for="item in todolist">
      <span v-if="item.done"><del>{{item.time}}&emsp;{{item.todo}}</del></span>
      <span v-else>{{item.time}}&emsp;{{item.todo}}</span>
    </li>
  </ul>
</div>
var app = new Vue({
  el: "#app",
  data: {
    message: "待办事项:",
    todolist: [
      new TodoItem("7:00", "起床", true),
      new TodoItem("8:00", "吃饭"),
      new TodoItem("9:00", "打豆豆"),
    ],
  }
});

通过条件渲染,将done的truthy值作为渲染的判断条件,已完成的项目被包裹在<del>标签中

image.png

v1.3 事件处理:v-on

现在把这个待办app变得更灵活,使用者可以自行勾选是否完成任务

使用vue.js的事件处理机制,为<input>元素添加监听事件,监听事件需要声明在Vue实例的 methods 对象中,在元素上使用 v-on 指令添加监听,v-on 可以被简写为 @

<ul>  <li v-for="item, index in todolist">    <input type="checkbox" @change="doIt(index)">    <span v-if="item.done"><del>{{item.time}}&emsp;{{item.todo}}</del></span>    <span v-else>{{item.time}}&emsp;{{item.todo}}</span>  </li></ul>
var app = new Vue({  el: "#app",  data: {    message: "待办事项:",    todolist: [      new TodoItem("7:00", "起床"),      new TodoItem("8:00", "吃饭"),      new TodoItem("9:00", "打豆豆"),    ],  },  methods: {    doIt(index) {      this.todolist[index].done = !this.todolist[index].done;    },  },});

通过 @change=“doIt(index)” 每次选中checkbox时,index会被传入doIt(),当前item的done属性值会取非,从而改换选中状态,done的变化又会立刻响应到渲染结果上,在 v-if 和 v-else 中变换,这是一种双向的信息传递

image.png

v1.4 表单输入绑定:v-model

  • 事件处理本质上是监听用户的操作并作出反应,其中包括监听表单的输入(文字输入、选中radio或checkbox等),v1.3中就是手动添加了对表单输入的监听。处理程序会根据监听的内容改变data,继而再引发渲染内容的改变
  • vue.js针对表单输入绑定有天生的加持:v-model 指令会自动在 <input>、<textarea> 及 <select> 上创建双向数据绑定:view层输入值影响data的属性值,data属性值发生改变会更新view层的数值变化

修改<input>,删除监听,直接使用v-model将输入绑定到item.done上,这样checkbox是否选中会直接更新到vue实例的data中

<input type="checkbox" v-model="item.done">
<!-- 等价于 -->
<input type="checkbox" v-bind:checked="item.done" @change="item.done=$event.target.checked">

v-model 原理:v-model 实际上是一个语法糖: 

  1. 使用 v-on 添加监听语句,规定触发时修改指定的 data property 为 $event.target.value/checked(这一步等同于 v1.3 手动设置监听函数)
  2. 使用 v-bind 将表单元素的 value/checked 属性绑定到指定 data property 上,因为v-bind的使用,为元素设置初始属性 value/checked 也会被忽略,必须通过在 data 中给 property 赋值初始化(这一步的意义,当修改 data property 时,表单元素的 value/checked 也会被改变,表现为输入值或者选中状态的改变,这种从 data 逆向更新渲染是 v1.3 做不到的)

根据v-model可以逆向更新表单元素渲染状态的特性,设置一个重置按键,在绑定的方法中将每一个item的done置为false,点击后所有checkbox的选中状态也被取消

v1.4.gif
<div id="app">
  {{message}}
  <ul>
    <li v-for="item, index in todolist">
      <input type="checkbox" v-model="item.done">
      <span v-if="item.done"><del>{{item.time}}&emsp;{{item.todo}}</del></span>
      <span v-else>{{item.time}}&emsp;{{item.todo}}</span>
    </li>
  </ul>
  <button @click="reset">重置</button>
</div>
var app = new Vue({
  el: "#app",
  data: {
    message: "待办事项:",
    todolist: [
      new TodoItem("7:00", "起床"),
      new TodoItem("8:00", "吃饭"),
      new TodoItem("9:00", "打豆豆"),
    ],
  },
  methods: {
    reset(){
      this.todolist.forEach((item)=>item.done=false);
    }
  },
});

v1.5 组件化

组件是可复用的vue实例,如果我们想复用 todo app 又不想重写冗余代码,可以提炼为一个组件。作为一个 vue 实例,其中的属性和正常的vue基本一样,主要有这几个特点:

  1. 组件需要一个 name 进行注册,之后使用组件只需要在html中用一对 <name></name> 形式的标签
  2. 组件 data 由一个对象变成了一个函数,返回组件需要的 property ,这是为了保证每个组件实例有独立的 property ,不会操作同一块内存
  3. 需要在 <template> 标签中传入组件的html模板

v1.5.1 朴素的组件

通过以上信息,可以得到一个简单 todo app 组件,使用 Vue.component() ,将 name 为“my-todo-list”的组件进行了全局注册,即之后在任何使用 new Vue 创建的实例中都可以使用这个组件

 Vue.component("my-todo-list", {
      props: ["message", "todolist"],
      data() {
        return {
          message: "待办事项:",
          todolist: [
            new TodoItem("7:00", "起床"),
            new TodoItem("8:00", "吃饭"),
            new TodoItem("9:00", "打豆豆"),
          ],
        };
      },
      methods: {
        reset() {
          this.todolist.forEach((item) => (item.done = false));
        },
      },
      template: `<div class="todolist">
                 {{message}}
                    <ul>
                      <li v-for="item, index in todoList">
                        <input type="checkbox" v-model="item.done" />
                        <span v-if="item.done">
                                                <del>{{item.time}}&emsp;{{item.todo}}</del></span>
                        <span v-else>{{item.time}}&emsp;{{item.todo}}</span>
                      </li>
                    </ul>
                <button @click="reset">重置</button>
                </div>`,
});

使用时在 html 中用一对标签即可,为了美观,简单用 flex 布局分三列

<div id="app">
  <my-todo-list></my-todo-list>
  <my-todo-list></my-todo-list>
  <my-todo-list></my-todo-list>
</div>
<style>
  #app {
    display: flex;
  }
  .todolist {
    border: gray 0.5px solid;
    width: 200px;
  }
  .todolist button {
    display: block;
    margin: 5px auto;
  }
</style>

最终效果

image.png

v1.5.2 向组件传递数据:props

把一个 todo app 抽离为组件后,每次使用时必须传入不同的 todolist 才有意义,否则每个组件都是一模一样的。vue.js 为组件准备了 props 这个特殊的属性,它是一个数组,可以接收不定数量的字符串,每一个串都代表一个由父实例传递给组件的 attribute

  • 设置props:对于 todo app,让每一个待办的 message 和 todolist 都应被灵活的传入,因此将它们的固定值从 data 删除,在props 中增加两个 attribute :props: ["message", "todolist"]
  • 传递数据:组件有了 props 后如何传递值?直接在组件标签上使用 attribute 的 name 传值,可以直接使用等号赋值,或者使用:绑定父亲实例的 data :<my-todo-list :todolist="monList" message="星期一"></my-todo-list>

下面在父实例 data 对周一到周三创建三个list,传递给组件

<div id="app">
  <my-todo-list :todolist="monList" message="星期一"></my-todo-list>
  <my-todo-list :todolist="tueList" message="星期二"></my-todo-list>
  <my-todo-list :todolist="wedList" message="星期三"></my-todo-list>
</div>
image.png

v1.5.3 再探 props:单向数据流

props 可以组件上创建自定义 attribute 即 prop,用以接收父实例数据。

所有的 prop 都使得其父子 prop 之间形成了一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外变更父级组件的状态,从而导致你的应用的数据流向难以理解。(这里的更新指的是)

如果把 prop 理解为指针,那么组件使用 prop 时对父组件的数据只有读取的权限,没有修改的权限

比如修改 reset(),在其中更改 props

props: ["message", "todolist"],
reset() {
    this.message = "工作日";
    this.todolist = [];
}

当点击重置后,虽然修改成功,但浏览器会给出 warn。无论 props 是原始类型值还是引用值,修改都会报错,但是对于引用值来说,通过引用修改对象不会报错(比如增删数组元素)

image.png

如果一定要将父组件传递的数据作为子组件自身的 prop,需要在子组件的 data() 中对来自 prop 的数据进行拷贝