第3章〓Vue 3基本指令 指令是Vue 3 模板中常用的功能之一,它们是带有 v前缀的特殊属性。指令的主要职责是在其值发生改变时,将相应的影响作用于DOM对象。Vue 3的指令在HTML中以页面元素的属性的方式使用,指令属性的值是JavaScript表达式。Vue 3的指令数量相对较少,本章将逐一介绍这些指令。 视频讲解 3.1条件渲染指令 条件渲染指令的主要功能是根据指令的值为true或false进而触发组件不同的表现形式。 3.1.1vif、velseif、velse vif、velseif和velse这三个指令用于实现条件判断。vif根据其值有条件地渲染元素,当vif的值在 true 和 false 之间切换时,元素或组件将被销毁或重建。在组件被销毁或重建的过程中,会执行该组件相应的钩子函数,示例代码如下: <div id="app"> <h1 v-if="display">Display</h1> <h1 v-if="hide">Hide</h1> <h1 v-if="age >= 25">Age: {{ age }}</h1> <h1 v-if="name.indexOf('Tom')>=0">Name:{{name}}</h1> </div> <script> const vm = Vue.createApp({ data() { return { display: true, hide: false, age: 28, name: 'Tom Cruise' } } }).mount('#app'); </script> 在浏览器中打开上述代码组成的页面,其渲染结果如图3.1所示。 图3.1vif渲染结果 当vif的值被设置为hide(即为 false)时,对应的<h1>元素并没有实际生成,而其他vif的值为 true 的<h1>元素正常生成。也就是说,当vif的值为 false 时,vif不会创建该元素; 当vif的值为 true 时,vif才会真正创建该元素。 切换到控制台窗口,将age属性的值修改为 20(即vm.age=20),然后切换回元素窗口,渲染结果如图3.2所示。 图3.2修改age属性值后vif页面的渲染结果 如果需要控制多个元素的创建或删除,可以使用<template>元素将这些元素包装起来,然后在<template>元素上使用vif,代码如下: <div id="app"> <template v-if="!isIogin"> <form> <p>username:<input type="text"></p> <p>password:<input type="password"></p> </form> </template> </div> <script> const vm = Vue.createApp({ data() { return { isLogin: false } } }).mount('#app'); </script> velseif和velse是vif的逻辑补充。示例代码如下: <div id="app"> <div v-if="Math.random() > 0.5"> 随机数大于0.5时可以看到这个元素 </div> <div v-else> 随机数小于0.5时可以看到这个元素 </div> </div> <script> const vm = Vue.createApp({ data() { return {} } }).mount('#app'); </script> velseif与vif一起使用,可以实现互斥的条件判断,代码如下: <div id="app"> <span v-if="score>=85">优秀</span> <span v-else-if="score>=75">良好</span> <span v-else-if="score>=60">及格</span> <span v-else>不及格 </div> <script> const vm = Vue.createApp({ data() { return { score: 90 } } }).mount('#app'); </script> 需要注意的是,当一个条件被满足时,后续的条件判断都不会再执行,velseif和velse需要紧跟在vif或velseif之后。 3.1.2vshow vshow根据其值切换元素的 CSS 样式中的 display 属性,当条件变化时,vshow会触发过渡效果,代码如下: <div id="app"> <h1 v-show="display">Display</h1> <h1 v-show="hide">Hide</h1> <h1 v-show="age >= 25">Age: {{ age }}</h1> <h1 v-show="name.indexOf('Tom')>=0">Name:{{name}}</h1> </div> <script> const vm = Vue.createApp({ data() { return { display: true, hide: false, age: 28, name: 'Tom Cruise' } } }).mount('#app'); </script> 除了指令不同外,本节代码与3.1.1节中的vif代码完全相同。接下来观察 DOM 结构在执行之后有何不同,vshow测试页面的渲染结果如图3.3所示。 图3.3vshow测试页面的渲染结果 对比图3.1和图3.3的展示效果,vshow与vif似乎没有不同,但在页面结构中可以发现,vshow并没有根据条件不同而改变页面结构,它在HTML元素是否显示的实现机制上与vif不同。无论vshow的值是true还是false,vshow都会创建元素,它通过CSS样式中的display属性来控制元素是否显示。 3.1.3vshow与vif的选择 一般来说,vif有更高的切换开销,因为在切换时需要销毁和重新创建元素及其子组件,而vshow只需要改变CSS样式属性,因此在需要频繁地切换元素的显示或隐藏时,使用vshow更好。但在初始渲染时,vshow存在更高的开销,因为它需要先创建元素,然后再根据其值设置CSS样式属性,而vif只有在值为true时才会创建元素。因此,在条件改变较少的情况下,使用vif更好。 3.2列表渲染指令vfor 3.2.1基本用法 在 Vue 3 中,可以使用 vfor基于一个数组来渲染一个列表。vfor需要使用 item in items 形式的特殊语法,其中 items 是源数据数组,item 是被迭代的数组元素的别名,示例代码如下: <ul id="array-rendering"> <li v-for="item in items"> {{ item.message }} </li> </ul> <script> const vm = Vue.createApp({ data() { return { items: [{ message: 'Foo' }, { message: 'Bar' }] } } }).mount('#app'); </script> 可以看到,组件实例的数据对象中定义了一个数组items,然后在<li>元素上使用vfor遍历数组,这将循环渲染<li>元素。在vfor块中,可以访问所有父作用域的属性,在每次循环时,item的值为数组当前索引的值,在<li>元素内部,可以通过Mustache语法引用变量item。 最终渲染结果如图3.4所示。 图3.4vfor测试页面的渲染结果 除此之外,vfor 还支持一个可选的第二个参数,即当前项的索引,代码如下: <ul id="array-with-index"> <li v-for="(item, index) in items"> {{ index }} - {{ item.message }} </li> </ul> 在Vue 3中,开发者不仅可以使用vfor遍历数组,也可以用 vfor 来遍历一个对象的所有可枚举属性。具体的使用方法就是使用of替代in作为分隔符,示例代码如下: <ul id="v-for-object" class="demo"> <li v-for="value in myObject"> {{ value }} </li> </ul> const vm = Vue.createApp({ data() { return { myObject: { title: 'How to do lists in Vue', author: 'Jane Doe', publishedAt: '2016-04-10' } } } }).mount('#app'); 可以增加第二个参数来获取属性的名称 (即键名),代码如下: <li v-for="(value, name) in myObject"> {{ name }}: {{ value }} </li> 还可以增加第三个参数来获取索引,代码如下: <li v-for="(value, name, index) in myObject"> {{ index }}. {{ name }}: {{ value }} </li> 在Vue 3中,当使用 vfor 渲染元素列表时,默认采用“就地更新”策略。如果数据项的顺序被更改,Vue 3 将不会移动页面元素来匹配数据项的顺序,而是就地更新每个元素,并确保它们在每个索引位置都被正确渲染。为了告知 Vue 3 每个节点的身份,以便能够重用和重新排序现有元素,需要为每个项提供一个唯一的 key 值,建议在使用 vfor时尽可能提供 key 值,这样可以提高 vfor 的渲染效率,代码如下: <div v-for="item in items" :key="item.id"> <!-- 内容 --> </div> 3.2.2数组更新 Vue 3的核心是数据与视图的双向绑定,为了监测数组中元素的变化并及时将变化反映到视图中,Vue 3对以下7个数组变更函数进行了封装。 (1) push()。 (2) pop()。 (3) shift()。 (4) unshift()。 (5) splice()。 (6) sort()。 (7) reverse()。 使用浏览器打开3.2.1节中的页面,在开发者工具中切换到控制台窗口,然后输入以下命令: vm.items.push({ message: 'Baz' }); 数组更新的结果如图3.5所示。 图3.5数组更新的结果 上述的push()函数会改变参数中的原始数组。此外,JavaScript语言也有原生数组的非变更函数,如filter()、concat()和slice(),它们不会改变原始数组,而是返回一个新数组。当使用非变更函数时,可以用新数组替换旧数组,代码如下: vm.items = vm.items.filter(item => item.message.match(/Foo/)); 数组变更的结果如图3.6所示。 图3.6数组变更的结果 有些开发者担心这种操作会造成性能问题,实际上这种操作并不会导致Vue 3丢弃现有的页面元素并重新渲染整个列表。Vue 3为了使页面元素得到最大范围的重用而进行了针对性的优化,用一个含有相同元素的数组去替换原来的数组是非常高效的操作。 3.2.3vfor的其他操作 vfor可以用来显示数组过滤或排序后的结果,如果要显示一个数组经过过滤或排序后的版本,而不改变原始数据,可以创建一个计算属性来返回处理后的数组,代码如下: <div id="app"> <li v-for="n in evenNumbers" :key="n">{{ n }}</li> </div> <script> const vm = Vue.createApp({ data() { return { numbers: [1, 2, 3, 4, 5] } }, computed: { evenNumbers() { return this.numbers.filter(number => number % 2 === 0) } } }).mount('#app'); </script> 如果在嵌套的 vfor 循环中无法使用计算属性,可以使用methods()函数来解决,代码如下: <div id="app"> <ul v-for="numbers in sets"> <li v-for="n in even(numbers)" :key="n">{{ n }}</li> </ul> </div> <script> const vm = Vue.createApp({ data() { return { sets: [[1, 2, 3, 4, 5], [6, 7, 8, 9, 10]] } }, methods: { even(numbers) { return numbers.filter(number => number%2 === 0) } } }).mount('#app'); </script> vfor 也可以接受整数n作为迭代参数,在这种情况下模板会重复循环n次,代码如下: <div id="range" class="demo"> <span v-for="n in 10" :key="n">{{ n }} </span> </div> 页面渲染结果如图3.7所示。 图3.7vfor接受整数的页面渲染结果 和 vif类似,开发者也可以利用带有 vfor 的 <template> 来循环渲染一段包含多个元素的内容,代码如下: <ul> <template v-for="item in items" :key="item.msg"> <li>{{ item.msg }}</li> <li class="divider" role="presentation"></li> </template> </ul> 当vfor与vif同时使用时,需要注意当它们处于同一节点时,vif的优先级比vfor更高,这意味着vif将没有权限访问vfor中的变量,代码如下: <!-- 这将抛出一个错误,因为"todo" property 没有在实例上定义 --> <li v-for="todo in todos" v-if="!todo.isComplete"> {{ todo.name }} </li> 为了解决这个问题,可以把 vfor 移动到 <template> 标签中来修正,代码如下: <template v-for="todo in todos" :key="todo.name"> <li v-if="!todo.isComplete"> {{ todo.name }} </li> </template> 在自定义组件上,开发者可以像在任何普通元素上一样使用 vfor,代码如下: <my-component v-for="item in items" :key="item.id"></my-component> 然而,任何数据都不会被自动传递到组件里,因为组件有自己独立的作用域。为了把迭代数据传递到组件里,需要使用如“: props名称”的组件属性来传递数据,代码如下: <my-component v-for="(item, index) in items" :item="item" :index="index" :key="item.id" ></my-component> 视频讲解 3.3数据绑定指令vbind vbind的主要作用是动态更新HTML元素上的属性和动态绑定组件的props属性,也可以使用简写的符号“:”来代替它。 3.3.1参数与属性绑定 下面示例中链接的href属性通过vbind动态地设置,当数据发生变化时,组件会被重新渲染,代码如下: <div id="app"> <a v-bind:href="url">前往百度</a> </div> <script> const vm = Vue.createApp({ data() { return { url: 'https://www.baidu.com' } } }).mount('#app'); </script> 3.3.2动态绑定 示例代码如下: <div id="app"> <a v-bind:[attribute]="url">前往百度</a> </div> <script> const vm = Vue.createApp({ data() { return { attribute:'href', url: 'https://www.baidu.com' } } }).mount('#app'); </script> 与3.3.1节中的代码相比,此处将在HTML中的属性变成了动态获取。vbind还可以直接绑定一个包含属性名和值的对象。在这种情况下,vbind指令不需要接收参数就可以直接使用,代码如下: <div id="app"> <!--绑定一个有属性的对象 --> <form v-bind="form0bj"> <input type="text"> </form> </div> <script> const vm = Vue.createApp({ data() { return { form0bj: { method: 'get', action: '#' } } } }).mount('#app'); </script> 最终的渲染结果如图3.8所示。 图3.8vbind绑定对象的渲染结果 3.3.3vbind的缩写及合并行为 Vue 3提供了一个简写方式“:bind”,如果一个元素同时定义了 vbind="object" 和一个相同的独立属性,后定义的属性值会覆盖之前定义的同名属性值。因此开发者可以通过控制它们的合并行为以满足开发需求,代码如下: <!-- 模板 --> <div id="red" v-bind="{ id: 'blue' }"></div> <!-- 结果 --> <div id="blue"></div> <!-- 模板 --> <div v-bind="{ id: 'blue' }" id="red"></div> <!-- 结果 --> <div id="red"></div> 3.4vmodel与表单 3.4.1基本用法 vmodel用于在表单中的<input>、<textarea>和<select>元素上创建双向数据绑定,根据控件类型自动选取正确的方法来更新元素,负责监听用户的输入事件从而更新数据,并对一些极端场景进行特殊处理,代码如下: <div id="app"> <input type="text" v-model="message"> </div> <script> const vm = Vue.createApp({ data() { return { message: 'Hello World' } } }).mount(' #app'); </script> 渲染效果如图3.9所示。 图3.9vmodel渲染结果 在开发者工具的控制台窗口中,输入vm.message ='Welcome to the Vue world',可以看到vmodel绑定的表达式数据发生改变,导致页面元素的值随之改变,如图3.10所示。 图3.10改变表达式数据导致页面元素值改变 接下来在页面控件元素中随意输入一些内容,然后在控制台中输入vm.message,可以看到表达式数据message的值也发生了变化,如图3.11所示。 图3.11改变页面元素值导致表达式数据改变 3.4.2值绑定 针对不同的表单控件,vmodel绑定的值都有默认的约定。例如,单个复选框绑定的是布尔值,多个复选框绑定的是一个数组,选中的复选框value属性的值被保存到数组中。如果要改变默认的绑定规则,可以使用vbind把值绑定到当前活动实例的一个动态属性上,这个属性的值可以不是字符串。 下面介绍3种常用的表单元素是如何绑定值的。 (1) 复选框。在使用单个复选框时,在<input>元素上可以使用两个特殊的属性truevalue和falsevalue来指定选中状态下和未选中状态下vmodel绑定的值,代码如下: <div id="app"> <input id="agreement" type="checkbox" v-model="isAgree" true-value="yes" false-value="no"> <label for="agreement">{{isAgree}}</label> </div> <script> const vm = Vue.createApp({ data() { return { isAgree: false } } }).mount(' #app'); </script> 数据属性isAgree的初始值为false,当选中复选框时,其值为truevalue的属性值yes,当取消选中复选框时,其值为falsevalue的属性值no。truevalue属性和falsevalue属性也可以使用vbind绑定到data选项中的某个数据属性上。 (2) 单选按钮。单选按钮被选中时,vmodel绑定的数据属性的值默认被设置为该单选按钮的value值。可以使用vbind将<input>元素的value属性再绑定到另一个数据属性上,选中后的值就是这个value属性绑定的数据属性的值,代码如下: <div id="app"> <input id="male" type="radio" v-model="gender" :value="genderVal[0]"> <label for="male">男</label> <br> <input id="female" type="radio" v-model="gender" :value="genderVal[1]"> <label for="female">女</label> <br> <span>性别: {{gender}}</span> </div> <script> const vm = Vue.createApp({ data() { return { gender: '', genderVal: ['男', '女'] 图3.12使用vbind后单选按钮选中的值 } } }).mount(' #app'); </script> 运行效果如图3.12所示。 (3) 选择框选项。通过选择框选择内容后,其值是选项的值,即<option>元素的value属性的值,选项的value属性也可以使用vbind指令绑定到一个数据属性上,代码如下: <option v-for="option in options" v-bind:value="option. value"></option> 或者将value属性绑定到一个对象字面量上,当选项被选中时,vm.selected.number的值会变更为2023,代码如下: <select v-model="selected" title="select"> <!-- 内联对象字面量--> <option v-bind:value="{number:2022}">2023</option> </select> 3.4.3修饰符 修饰符主要有以下3种。 (1) trim修饰符。它用于自动过滤用户输入内容首尾两端的空格,使用vmodel时,代码如下: <input type="text" v-model="inputValue"> <p>{{ inputValue }}</p> <input type="text" v-model.trim="inputValue"> <p>{{ inputValue }}</p> 运行效果如图3.13所示。 图3.13trim修饰符应用 可以看到,当使用trim修饰符后,<p>标签的前后空格和中间多余的空格都被去除了,只显示输入框中的实际内容。 (2) lazy修饰符。它用于将vmodel的默认触发方式由input事件更改为change事件。例如,在使用<input>元素时,每次输入内容都会立即更新数据,使用lazy修饰符后,vmodel的双向数据绑定触发方式就变为失去焦点时进行内容检测,从而减少了频繁更新数据的操作,代码如下: <input type="text" v-model.lazy="inputValue"> <p>{{ inputValue }}</p> (3) number修饰符。它用于自动将用户输入的数据转换为数值类型,如果无法被 parseFloat() 转换,则返回原始内容,代码如下: <input type="text" v-model.number="inputValue"> <p>{{ inputValue }}</p> 视频讲解 3.5方法、计算属性与监听属性 3.5.1Vue 3中的方法 Vue 3方法是与 Vue 3实例关联的对象,本书后续提到的Vue 3方法特指methods对象。当需要对元素的某些事件做出响应时,可以通过 von来绑定相应的Vue 3方法,开发者可以在Vue 3方法内定义函数来执行事件响应的操作,下面演示Vue 3方法的工作原理,代码如下: <div id="app"> <!-- 渲染DOM树 --> <h1 style="color: seagreen;">{{title}}</h1> <h2>Title : {{name}}</h2> <h2>Topic : {{topic}}</h2> <!-- 调用Vue 3方法中的函数 --> <h2>{{show()}}</h2> </div> <script> const vm = Vue.createApp({ data() { return { title: "Geeks for Geeks", name: "Vue.js", topic: "Instances" } }, // 创建组件中的Vue 3方法 methods: { // 创建函数 show: function () { 图3.14Vue 3方法工作原理运行效果 return "欢迎尝试这个Vue例子 " + this.name + " - " + this.topic; } } }).mount(' #app'); </script> 运行效果如图3.14所示。 3.5.2计算属性 在模板中使用表达式非常方便,但如果表达式的逻辑比较复杂,使用计算属性会大大降低模板的复杂度,代码如下: <div id="app"> <p>{{message. split("). reverse(). join(")}}</p> </div> Mustache语法中的表达式调用了3个函数来最终实现字符串的反转,逻辑过于复杂,如果在模板中还需要多次引用这个功能,会让模板变得更加复杂并难以处理,这时就可以使用计算属性,它以函数形式在computed选项中定义,代码如下: <div id="app"> <div id="app"> <p>原始字符串: {{message }}</p> <p>计算后的反转字符串: {{reversedMessage}}</p> </div> </div> <script> const vm = Vue.createApp({ data() { return { message: 'Hello!, welcome to Vue!' } }, // 创建计算属性 computed: { //计算属性的getter reversedMessage() { return this.message.split('').reverse().join(''); } } }).mount(' #app'); </script> 在上述示例中,声明了一个计算属性reversedMessage,它可以像普通的属性一样在模板中绑定数据。在浏览器中的渲染结果如图3.15所示。 图3.15计算属性绑定数据的渲染结果 当message属性的值改变时,reversedMessage的值会自动更新,并且会自动同步更新相应DOM对象。在浏览器中修改vm.message的值,reversedMessage的值也会随之改变。 由于计算属性默认只有getter方法,因此不能直接修改计算属性,如需修改可以参考以下代码: <div id="app"> <div id="app"> <p>First name:<input type="text" v-model="firstName"></p> <p>Last name:<input type="text" v-model="lastName"></p> <p>{{ fullName }}</p> </div> </div> <script> const vm = Vue.createApp({ data() { return { firstName: 'Smith', lastName: "Will" } }, // 创建计算属性 computed: { fullName: { // getter()函数 get() { return this.firstName + ' ' + this.lastName; }, // setter()函数 set(newValue) { let names = newValue.split(' '); this.firstName = names[0]; this.lastName = names[names, length1]; } } } }).mount(' #app'); </script> 任意修改firstName或lastName的值,fullName的值也会自动更新,这是调用fullName的get()实现的。在浏览器的Console窗口中输入vm.fullName="Bruce Willis",可以看到firstName和lastName的值也同时发生了改变,这是调用fullName的set()实现的。 3.5.3监听属性 为了观察和响应实例上的数据变化,当需要一些数据随着其他数据变化而变化时,可以使用监听属性。尽管这听起来和计算属性的作用很相似,但在实际应用中两者是有很大差别的。Vue 3实例的选项对象通常是在watch选项中定义监听属性。下面的代码演示了如何使用监听属性来实现千米和米之间的换算。 <div id="app"> <div id="app"> 千米:<input type="text" v-model="kilometers"> 米:<input type="text" v-model="meters"> </div> </div> <script> const vm = Vue.createApp({ data() { return { kilometers: 0, meters: 0 } }, // 监听属性 watch: { kilometers(val) { this.meters = val * 1000; }, meters(val, oldVal) { this.kilometers = val / 1000; } } }).mount(' #app'); </script> 在这个例子中编写了两个监听函数,分别监听数据属性kilometers和meters的变化。当其中一个数据属性的值发生改变时,对应的监听函数就会被调用进而经过计算得到另一个数据属性的值。注意,不要在监听函数中使用箭头函数,这样会造成Vue 3的上下文发生错乱。 当需要在数据变化时执行异步或开销较大的操作时,使用监听属性是最合适的。例如在一个在线问答系统中,用户输入的问题需要到服务器端获取答案,就可以考虑对问题属性进行监听,在异步请求答案的过程中,可以设置中间状态,向用户提示“请稍候...”,而这样的功能使用计算属性无法做到。 下面给出一个使用监听属性实现斐波那契数列的计算的例子,该计算比较耗时,因此采用HTML 5新增的WebWorker来计算,将斐波那契数列的计算放到一个单独的fibonacci.js文件中,代码如下: function fibonacci(n) { return n < 2 ? n : arguments.callee(n-1) + arguments.callee(n-2); } onmessage = function (event) { var num = parseInt(event.data, 10); postMessage(fibonacci(num)); } 主页面代码如下: <div id="app"> <span>请输入要计算斐波那契数列的第几个数: </span> <input type="text" v-model="num"> <p v-show="result">{{result}}</p> </div> <script> const vm = Vue.createApp({ data() { return { num: 0, result: '' } }, // 监听属性 watch: { num(val) { this.result = "请稍候..."; if (val > 0) { const worker = new Worker("fibonacci.js"); worker.onmessage = (event) => this.result = event.data; worker.postMessage(val); } else { this.result = ''; } } } }).mount(' #app'); </script> 在这个例子中,为了防止用户在等待计算时以为程序卡死,为result数据属性设置了一个中间状态,从而给用户一个提示。worker实例是异步执行的,当后台线程完成任务后通过postMessage()函数来调用,并通过调用创建者线程的onmessage()回调函数来通知。在此回调函数中,可以通过event对象的data属性来获取数据。在这种异步回调执行的过程中,this的指向会发生变化。如果onmessage()回调函数写成以下形式: worker.onmessage = function (event) { this.result = event.data }; 那么在执行onmessage()函数时,this实际上指向的是worker对象,因此this.result的值是undefined。为了解决这个问题,需要在onmessage()函数处使用箭头函数,因为箭头函数绑定的是父级作用域的上下文,这里绑定的是vm对象。在使用Vue 3进行开发时,经常会遇到this指向的问题,合理地使用箭头函数可以避免很多问题,后面还会遇到类似的情况。在浏览器中打开页面,输入40,会看到提示信息“请稍候...”,过一段时间后会显示斐波那契数列的第40个数。 当定义监听属性时,除直接编写一个函数外还可以指定一个Vue 3方法名,代码如下: <div id="app"> 年龄: <input type="text" v-model="age"> <p v-if="info">{{info}}</p> </div> <script> const vm = Vue.createApp({ data() { return { age: 0, info: " } }, methods: { checkAge() { if (this.age >= 18) this.info = '已成年'; else this.info = '未成年'; } }, watch: { age: 'checkAge' } }).mount('#app'); </script> 监听属性还可以监听一个对象的属性变化,代码如下所示: <div id="app"> 年龄:<input type="text" v-model="person.age"> <p v-if="info">{{info}}</p> </div> <script> const vm = Vue.createApp({ data() { return { person: { name: 'lisi', age: 0 }, info: " } }, watch: { //该回调会在person对象的属性改变时被调用,无论该属性被嵌套得多深 person: { handler(val, oldVal) { if (val.age >= 18) this.info = '已成年'; else this.info = '未成年'; }, deep: true } } }).mount('#app'); </script> 需要注意的是,在监听对象属性变化时,使用了两个新选项: handler和deep。handler用于定义数据变化时调用的监听属性函数,而deep主要用于监听对象属性的变化。如果将deep的值设置为true,无论对象属性在对象中的层级有多深,只要该属性的值发生变化,都会被监测到。 监听属性函数在初始渲染时不会被调用,只有在后续监听的属性发生变化时才会被调用。如果要在开始监听后立即执行监听属性函数,可以使用immediate选项,并将其值设置为true,代码如下: watch: { //该回调会在person对象的属性改变时被调用,无论该属性被嵌套得多深 person: { handler(val, oldVal) { if (val.age >= 18) this.info = '已成年'; else this.info = '未成年'; }, deep: true, immediate: true } } 如果仅需要监听对象的一个属性变化,且变化不影响对象的其他属性,那么可以直接监听该对象的属性,代码如下: watch: { 'person.age':function(val,oldVal){ ... } } 除了在watch选项中定义监听属性,还可以使用组件实例的$watch()函数观察组件实例上响应式属性或计算属性的更改。注意,对于顶层数据属性、prop和计算属性,只能以字符串形式传递名字,代码如下: vm.$watch('kilometers', (newValue, oldValue) => { //这个回调将在vm.kilometers 改变后调用 document.getElementById("info").innerHTML = "修改前值为: " + old Value + "修改后值为: " + newValue; }) 对于更复杂的表达式或嵌套属性,则需要使用函数形式,代码如下: const app = Vue.createApp({ data() { return { a: 1, b: 2, c: { d: 3, e: 4 } } }, created() { // 顶层属性名 this.$watch('a', (newVal, old Val) => { }) //用于监听单个嵌套属性的函数 this.$watch( () => this.c.d, (newVal, oldVal) => { } ) // 用于监听复杂表达式的函数 this.$watch( // 每次表达式"this.a+this.b"产生不同的结果时 // 都会调用处理程序,就好像在观察一个计算属性而没有定义计算属性本身 () => this.a + this.b, (newVal, oldVal) => { } ) } }) $watch()函数还可以接受一个可选的选项对象作为其第3个参数。例如,要监听对象内部嵌套属性的变化,可以在选项参数中传入deep:true,代码如下: vm.$watch('person', callback, { deep: true }); 3.5.4方法、计算属性与监听属性的区别 在Vue 3中,可以通过在表达式中调用Vue 3方法来达到与监听属性相同的效果,代码如下: <p>Reversed message: "{{ reversedMessage() }}"</p> // 在组件中 methods: { reversedMessage: function () { return this.message.split('').reverse().join('') } } Vue 3方法和计算属性的区别在于,计算属性是基于响应式依赖进行缓存的,只有在相关响应式依赖发生改变时才会重新求值,只要 message 还没有发生改变,多次访问 reversedMessage计算属性就会立即返回之前的计算结果,而不会再次执行函数。这意味着下面代码中的计算属性now将不再更新,因为 Date.now() 不是响应式依赖: computed: { now: function () { return Date.now() } } 假设有一个计算属性 A需要遍历一个庞大的数组并进行大量的计算,且可能有其他的计算属性依赖A,缓存的意义在于系统可避免多次执行A的getter()函数。当某些数据需要随着其他数据的变化而变化时,建议使用计算属性,代码如下: <div id="demo">{{ fullName }}</div> var vm = new Vue({ el: '#demo', data: { firstName: 'Foo', lastName: 'Bar', fullName: 'Foo Bar' }, watch: { firstName: function (val) { this.fullName = val + ' ' + this.lastName }, lastName: function (val) { this.fullName = this.firstName + ' ' + val } } }) 上述代码是命令式的且存在重复代码,采用计算属性的代码如下: var vm = new Vue({ el: '#demo', data: { firstName: 'Foo', lastName: 'Bar' }, computed: { fullName: function () { return this.firstName + ' ' + this.lastName } } }) 可以看出,计算属性的版本在可读性和逻辑性上更好。 3.6事件处理 3.6.1监听事件von von用于绑定事件监听器,也可以使用简写的符号“@”来代替它。von后面的参数指定了监听的事件类型,参数值可以是一个函数的名称或内联语句,如果没有使用修饰符,那么可以省略。在普通元素上使用von时,只能监听原生的DOM事件。而在自定义元素组件上使用von时,可以监听子组件触发的自定义事件,示例代码如下: <div id="app"> <div> <button v-on:click="counter += 1">Add 1</button> <p>按钮已经点击了 {{ counter }} 次.</p> </div> </div> <script> const vm = Vue.createApp({ data() { return { counter: 0 } 图3.16von监听点击事件示例 } }).mount('#app'); </script> 当点击按钮时,点击次数都会加1,运行效果如图3.16所示。 3.6.2事件处理函数 在实际开发中由于事件处理逻辑通常比较复杂,因此不建议直接将JavaScript代码写在von中。von还可以接收一个用于调用的函数名称,示例代码如下: <div id="app"> <!-- greet是在下面定义的函数名 --> <button v-on:click="greet">Greet</button> </div> const vm = Vue.createApp({ data() { return { name: 'Vue.js' } }, // 在methods对象中定义函数 methods: { greet: function (event) { // this在函数里指向当前 Vue 实例 alert('Hello ' + this.name + '!') // event是原生 DOM 事件 if (event) { alert(event.target.tagName) } } } }).mount('#app'); 3.6.3内联处理函数 除了直接绑定到一个函数,还可以在内联 JavaScript 语句中调用函数,代码如下: <div id="app"> <button v-on:click="say('hi')">Say hi</button> <button v-on:click="say('what')">Say what</button> </div> const vm = Vue.createApp({ data() { return {} }, methods: { say: function (message) { alert(message) } } }).mount('#app'); 渲染结果如图3.17所示,这种方式不会在von所在元素上出现对应的 JavaScript 事件属性。 图3.17von内联渲染结果 3.6.4多事件监听 在Vue 3中,可以使用von来绑定事件,有时一个标签上需要绑定多个事件,可以逐一绑定,也可以使用von一次绑定多个不同的事件,代码如下: <input v-on="{focus:focus,blur:blur}"></input> const vm = Vue.createApp({ data() { return {} }, methods: { blur() { console.log("输入框失去焦点"); }, focus() { console.log("输入框获取焦点"); } } }).mount('#app'); input获取焦点和input失去焦点的运行效果分别如图3.18和图3.19所示。 图3.18input获取焦点的运行效果 图3.19input失去焦点的运行效果 3.6.5事件修饰符 在事件处理程序中调用event.preventDefault() 或 event.stopPropagation()是常见需求,虽然可以在函数中实现,但更好的方式是让函数只处理纯粹的数据逻辑,而不是去处理 DOM 事件细节。为此,Vue 3给von提供了事件修饰符,修饰符是由点开头的指令后缀来表示的,主要有以下6个。 (1) .stop。 (2) .prevent。 (3) .capture。 (4) .self。 (5) .once。 (6) .passive。 示例代码如下: <!-- 阻止点击事件继续传播 --> <a v-on:click.stop="doThis"></a> <!-- 提交事件不再重载页面 --> <form v-on:submit.prevent="onSubmit"></form> <!-- 修饰符可以串联 --> <a v-on:click.stop.prevent="doThat"></a> <!-- 只有修饰符 --> <form v-on:submit.prevent></form> <!-- 添加事件监听器时使用事件捕获模式 --> <!-- 即内部元素触发的事件先在此处理,然后才交由内部元素进行处理 --> <div v-on:click.capture="doThis">...</div> <!-- 只当在 event.target 是当前元素自身时触发处理函数 --> <!-- 即事件不是从内部元素触发的 --> <div v-on:click.self="doThat">...</div> <!-- 点击事件将只会触发一次 --> <a v-on:click.once="doThis"></a> <!-- 滚动事件的默认行为 (即滚动行为) 将会立即触发 --> <!-- 而不会等待onScroll完成 --> <!-- 这其中包含event.preventDefault()的情况 --> <div v-on:scroll.passive="onScroll">...</div> 使用事件修饰符时,顺序非常重要,因为相应的代码也会按照同样的顺序生成。使用 von:click.prevent.self 会阻止所有的点击事件,而 von:click.self.prevent 只会阻止对元素本身的点击事件。不要同时使用.prevent和.passive修饰符,因为.prevent修饰符会被忽略,同时浏览器可能会显示一个警告,.passive修饰符会告诉浏览器不要阻止事件的默认行为。 3.6.6按键修饰符 Vue 3允许为 von 在监听键盘事件时添加按键修饰符,代码如下: <!-- 只有在 key 是 Enter 时调用 vm.submit() --> <input v-on:keyup.enter="submit"> <input v-on:keyup.page-down="onPageDown"> 为了在必要的情况下支持旧浏览器,Vue 3提供了绝大多数常用的按键码的别名,主要有以下9个: (1) .enter。 (2) .tab。 (3) .delete。 (4) .esc。 (5) .space。 (6) .up。 (7) .down。 (8) .left。 (9) .right。 一些按键(如 .esc 和所有方向键)在IE 9中的key值与其他浏览器不同,如果需要支持IE 9,则应该使用这些内置的别名,还可以通过全局的 config.keyCodes 对象自定义按键修饰符的别名,代码如下: // 可以使用 v-on:keyup.f1 Vue.config.keyCodes.f1 = 112 3.6.7系统修饰键 在Vue 3中还可以使用以下4个系统修饰符来实现仅在按相应按键时才触发鼠标或键盘事件的监听器。 (1) .ctrl。 (2) .alt。 (3) .shift。 (4) .meta。 注意,在macOS 系统键盘上,.meta 对应 command 键()。在 Windows 系统键盘上,.meta对应Windows徽标键()。在 Sun 操作系统键盘上,meta 对应实心宝石键 (◆)。在其他特定键盘上,尤其是在 MIT 和 Lisp 机器的键盘及其后继产品(如 Knight 键盘、spacecadet 键盘),meta 被标记为“META”。在 Symbolics 键盘上,.meta 被标记为“META”或者“Meta”,示例代码如下: <!-- Alt + C --> <input v-on:keyup.alt.67="clear"> <!-- Ctrl + Click --> <div v-on:click.ctrl="doSomething">Do something</div> 在同时使用修饰键和 keyup事件时,只有在按Ctrl 键的情况下释放其他按键才能触发 keyup.ctrl 事件,如果只释放Ctrl 键不会触发事件。要实现这种行为,需要使用 keyCode将 keyup.ctrl替换为keyup.17。 Vue 3内有1个特别的.exact修饰符,可以和系统修饰符搭配进行更精确的控制。.exact 修饰符允许控制仅当精确的系统修饰符按键按下时触发事件,代码如下: <!-- 即使 Alt 键或 Shift键被一同按下时也会触发 --> <button v-on:click.ctrl="onClick">A</button> <!-- 有且只有 Ctrl 键被按下的时候才触发 --> <button v-on:click.ctrl.exact="onCtrlClick">A</button> <!-- 没有任何系统修饰符被按下的时候才触发 --> <button v-on:click.exact="onClick">A</button> 3.7其他基本指令 3.7.1首次渲染vonce vonce可以使元素或组件只被渲染一次,该指令不需要赋值,在之后的重新渲染中元素或组件及其所有子节点将被视为静态内容并跳过,可用于优化更新性能,代码如下: <div id="app"> <h1>{{title}}</h1> <a v-for="nav in navs" :href="nav. url" v-once>{{nav.name}}</a> </div> <script src=" https://unpkg.com/vue@next "></script> <script> const vm = Vue.createApp({ data() { return { title: 'v-once的用法', navs: [ { name: '首页', url: '/home' }, { name: '新闻', url: '/news' }, 图3.20vonce渲染结果 { name: '视频', url: '/video' }, ] } } }).mount(' #app'); </script> 渲染结果如图3.20所示。 虽然看起来和没有使用vonce的渲染结果是相同的,但是vonce在首次渲染时不会生成动态绑定的代码,这有助于提高渲染性能。可以在控制台中输入以下语句并按回车键: vm.navs.push({name:'论坛', url:'/bbs'}) 如图3.21所示,可以发现页面没有任何变化。 图3.21修改navs数组的内容的渲染结果 3.7.2使用vcloak避免渲染时闪烁 示例代码如下所示: <div id="app"> <h1 v-cloak>{{message}}</h1> </div> <script> const vm = Vue.createApp({ data() { return { message: '渲染结束可见' } } }).mount('#app'); </script> 可以发现,当浏览器加载页面时,如果网速较慢或者页面较大,会出现DOM树构建完成后直接显示{{message}}的情况,直到Vue 3的JavaScript文件加载完毕、组件实例创建和模板编译后,才会将{{message}}替换为数据对象中的内容。这个过程中,页面会出现闪烁,用户体验较差。 为了解决这个问题,可以使用vcloak,vcloak会一直保留在元素上,直到与其关联的 Vue 3实例完成编译。当与 CSS 规则 vcloak{ display: none } 一起使用时,该指令可以隐藏未编译的 Mustache语法,直到实例准备就绪。 在Vue 3独立版本的页面开发中,使用vcloak解决初始化慢所导致的页面闪烁非常有效。但是在较大的项目中,由于采用模块化开发,主页面只包含一个空的div元素,剩余的内容是由路由挂载不同的组件完成的,因此没有必要使用vcloak。 3.8自定义指令 除了Vue 3内置的核心功能指令(如vmodel和vshow)外,Vue 3还允许注册自定义指令。 3.8.1注册自定义指令 假设需要开发一个输入框,当页面加载时该输入框元素将获得焦点,只要用户在打开这个页面后还没点击过任何内容,这个输入框就仍处于聚焦状态。可以通过自定义指令来实现这个功能,代码如下: // 注册一个全局自定义指令 v-focus Vue.directive(focus, { // 当被绑定的元素插入DOM树中时...... inserted: function (el) { // 聚焦元素 el.focus() } }) 如果想要注册局部指令,可以在组件选项中添加 directives 属性,代码如下: directives: { focus: { // 指令的定义 inserted: function (el) { el.focus() } } } 然后开发者可以在模板中任何元素上使用新的 vfocus 属性。 3.8.2钩子函数 一个指令定义对象可以提供以下5个钩子函数(均为可选)。 (1) bind: 只调用一次,指令第一次绑定到元素时调用,可以进行一次性的初始化设置。 (2) inserted: 被绑定元素插入父节点时调用(仅保证父节点存在,但不一定已被插入文档中)。 (3) update: 所在组件的 VNode 更新时调用。 (4) componentUpdated: 指令所在组件的 VNode 及其子 VNode 全部更新后调用。 (5) unbind: 只调用一次,指令与元素解绑时调用。 3.8.3动态指令参数 指令的参数可以是动态的,例如在 vmydirective: [argument]="value" 中,argument 参数可以根据组件实例数据进行更新,这使得自定义指令可以在应用中被灵活使用。 例如,可以创建一个自定义指令,用于通过指令值更新垂直坐标,从而将元素以绝对坐标固定在页面上,代码如下: <div id="baseexample"> <p>向下滚动页面</p> <p v-pin="200">固定在距离页面顶部200px的位置</p> </div> Vue.directive('pin', { bind: function (el, binding, vnode) { el.style.position = 'fixed' el.style.top = binding.value + 'px' } }) new Vue({ el: '#baseexample' }) 这会把该元素固定在距离页面顶部 200px的位置,但如果场景是需要把元素固定在左侧或顶部,则需要使用动态参数根据每个组件实例来进行更新,代码如下: <div id="dynamicexample"> <h3>在此区域内向下滚动</h3> <p v-pin:[direction]="200">固定在左侧200px的地方</p> </div> Vue.directive('pin', { bind: function (el, binding, vnode) { el.style.position = 'fixed' var s = (binding.arg == 'left' ? 'left' : 'top') el.style[s] = binding.value + 'px' } }) new Vue({ el: '#dynamicexample', data: function () { return { direction: 'left' } } }) 3.8.4函数简写与对象字面量 在许多情况下,开发者可能希望在调用bind和update钩子函数时触发相同的行为,而不关心其他钩子函数。如果指令需要多个值,可以传入一个 JavaScript 对象字面量,指令函数可以接受所有合法的 JavaScript 表达式,代码如下: <div v-demo="{ color: 'white', text: 'hello!' }"></div> Vue.directive('demo', function (el, binding) { console.log(binding.value.color)// 输出white console.log(binding.value.text) // 输出hello! }) 3.9案例 3.9.1使用自定义指令实现随机背景色 有时需要在网页中将一张图片作为某个元素的背景图,但当网络状况较差或图片较大时,图片的加载速度可能会很慢。在这种情况下可以先使用随机的背景色填充该元素的区域,等待图片加载完成后再将元素的背景替换为图片。使用自定义指令可以很方便地实现上述功能,代码如下: <div id="app"> <div v-img="'images/bg.jpg'"></div> </div> <script> const app = Vue.createApp({}); app.directive('img', { mounted: function (el, binding) { let color = Math.floor(Math.random() * 1000000); el.style.backgroundColor = ' #' + color; let img = new Image(); img.src = binding.value; img.onload = function () { el.style.backgroundImage = 'url(' + binding.value + ')'; } } }) app.mount('#app'); </script> 3.9.2注册登录页面信息 在SPA中,用户注册时通常会使用Ajax将数据以JSON格式发送到服务器端。JSON是JavaScript对象字面量语法的子集。在表单提交前通常需要将要发送的数据组织为一个JavaScript对象或数组,然后将其转换为JSON字符串进行发送。使用Vue 3将数据组织为对象的过程非常简单。可以使用vmodel将输入控件直接绑定到某个对象的属性上,然后使用von绑定“注册”按钮的click事件,在事件处理函数中直接发送该对象即可,完整代码如下所示: <div id="app"> <form> <table border="0"> <tr> <td>用户名: </td> <td> <input type="text" name="username" v-model="user.username"> </td> </tr> <tr> <td>密码: </td> <td> <input type="password" name="password" v-model="user.password"> </td> </tr> <tr> <td>性别: </td> <td> <input type="radio" name="gender" value="1" v-model="user.gender">男 <input type="radio" name="gender" value="0" v-model="user.gender">女 </td> </tr> <tr> <td>邮件地址: </td> <td> <input type="text" name="email" v-model="user.email"> </td> </tr> <tr> <td>密码问题: </td> <td> <input type="text" name=" pwdQuestion " v-model="user.pwdQuestion"> </td> </tr> <tr> <td>密码答案: </td> <td> <input type="text" name="pwdAnswer" v-model="user.pwdAnswer"> </td> </tr> <tr> <td> <input type="submit" value="注册" @click.prevent="register"> </td> <td><input type="reset" value="重填"></td> </tr> </table> </form> </div> <script> const vm = Vue.createApp({ data() { return { user: { username: '', password: '', gender: '', email: '', pwdQuestion: '', pwdAnswer: '' } } }, methods: { register: function () { //直接发送this.user对象 // ... console.log(this.user); } } }).mount('#app'); </script 在“注册”按钮上绑定click事件时使用.prevent修饰符来阻止表单的默认提交行为,这是因为本案例是在click事件响应函数中完成用户注册数据的发送,并不希望发生表单的默认提交行为,浏览器中注册页面的显示效果如图3.22所示。 图3.22注册页面的显示效果 3.10本章小节 本章详细介绍了Vue 3的内置指令。其中,常用的指令包括vif、vfor、von、vbind和vmodel。读者应该重点掌握这5个指令的使用方法。此外,本章还介绍了自定义指令的用法,自定义指令只应用于对DOM对象的封装操作。在某些特殊需求下,通过自定义指令封装DOM对象操作可以简化代码编写,提高代码的重用性。 习题 1. 描述vonce的作用。 2. 描述计算属性和监听属性的异同。 3. 实现一个修改密码页面。