六、Vue3视图渲染技术
6.1 模版语法
Vue 使用一种基于 HTML 的模板语法,使我们能够声明式地将其组件实例的数据绑定到呈现的 DOM 上。所有的 Vue 模板都是语法层面合法的 HTML,可以被符合规范的浏览器和 HTML 解析器解析。在底层机制中,Vue 会将模板编译成高度优化的 JavaScript 代码。结合响应式系统,当应用状态变更时,Vue 能够智能地推导出需要重新渲染的组件的最少数量,并应用最少的 DOM 操作。
6.1.1 插值表达式和文本渲染
插值表达式:最基本的数据绑定形式是文本插值,它使用的是“Mustache”语法 ,即双大括号
{{}}
- 插值表达式是将数据渲染到元素的指定位置的手段之一
- 插值表达式不绝对依赖标签,其位置相对自由
- 插值表达式中支持javascript的运算表达式
- 插值表达式中也支持函数的调用
<script setup type="module"> let msg ="hello vue3" let getMsg= ()=>{ return 'hello vue3 message' } let age = 19 let bee = '蜜 蜂' // 购物车 const carts = [{name:'可乐',price:3,number:10},{name:'薯片',price:6,number:8}]; //计算购物车总金额 function compute(){ let count = 0; for(let index in carts){ count += carts[index].price*carts[index].number; } return count; } </script> <template> <div> <h1>{{ msg }}</h1> msg的值为: {{ msg }} <br> getMsg返回的值为:{{ getMsg() }} <br> 是否成年: {{ age>=18?'true':'false' }} <br> 反转: {{ bee.split(' ').reverse().join('-') }} <br> 购物车总金额: {{ compute() }} <br/> 购物车总金额: {{carts[0].price*carts[0].number + carts[1].price*carts[1].number}} <br> </div> </template> <style scoped> </style>
为了渲染双标中的文本,我们也可以选择使用v-text
和v-html
命令
- v-*** 这种写法的方式使用的是vue的命令
- v-***的命令必须依赖元素,并且要写在元素的开始标签中
- v-***指令支持ES6中的字符串模板
- 插值表达式中支持javascript的运算表达式
- 插值表达式中也支持函数的调用
- v-text可以将数据渲染成双标签中间的文本,但是不识别html元素结构的文本
- v-html可以将数据渲染成双标签中间的文本,识别html元素结构的文本
<script setup type="module"> let msg ='hello vue3' let getMsg= ()=>{ return msg } let age = 19 let bee = '蜜 蜂' let redMsg ='<font color=\'red\'>msg</font>' let greenMsg =`<font color=\'green\'>${msg}</font>` </script> <template> <div> <span v-text='msg'></span> <br> <span v-text='redMsg'></span> <br> <span v-text='getMsg()'></span> <br> <span v-text='age>18?"成年":"未成年"'></span> <br> <span v-text='bee.split(" ").reverse().join("-")'></span> <br> <span v-html='msg'></span> <br> <span v-html='redMsg'></span> <br> <span v-html='greenMsg'></span> <br> <span v-html="`<font color='green'>${msg}</font>`"></span> <br> </div> </template> <style scoped> </style>
6.1.2 Attribute属性渲染
想要渲染一个元素的 attribute,应该使用
v-bind
指令
- 由于插值表达式不能直接放在标签的属性中,所有要渲染元素的属性就应该使用v-bind
- v-bind可以用于渲染任何元素的属性,语法为
v-bind:属性名='数据名'
, 可以简写为:属性名='数据名'
<script setup type="module"> const data = { name:'尚硅谷', url:"http://www.atguigu.com", logo:"http://www.atguigu.com/images/index_new/logo.png" } </script> <template> <div> <a v-bind:href='data.url' target="_self"> <img :src="data.logo" :title="data.name"> <br> <input type="button" :value="`点击访问${data.name}`"> </a> </div> </template> <style scoped> </style>
6.1.3 事件的绑定
我们可以使用
v-on
来监听 DOM 事件,并在事件触发时执行对应的 Vue的JavaScript代码。
- 用法:
v-on:click="handler"
或简写为@click="handler"
- vue中的事件名=原生事件名去掉
on
前缀 如:onClick --> click
- handler的值可以是方法事件处理器,也可以是内联事件处理器
- 绑定事件时,可以通过一些绑定的修饰符,常见的事件修饰符如下
.once:只触发一次事件。[重点]
.prevent:阻止默认事件。[重点]
- .stop:阻止事件冒泡。
- .capture:使用事件捕获模式而不是冒泡模式。
- .self:只在事件发送者自身触发时才触发事件。
<script setup type="module"> import {ref} from 'vue' // 响应式数据 当发生变化时,会自动更新 dom树 let count=ref(0) let addCount= ()=>{ count.value++ } let incrCount= (event)=>{ count.value++ // 通过事件对象阻止组件的默认行为 event.preventDefault(); } </script> <template> <div> <h1>count的值是:{{ count }}</h1> <!-- 方法事件处理器 --> <button v-on:click="addCount()">addCount</button> <br> <!-- 内联事件处理器 --> <button @click="count++">incrCount</button> <br> <!-- 事件修饰符 once 只绑定事件一次 --> <button @click.once="count++">addOnce</button> <br> <!-- 事件修饰符 prevent 阻止组件的默认行为 --> <a href="http://www.atguigu.com" target="_blank" @click.prevent="count++">prevent</a> <br> <!-- 原生js方式阻止组件默认行为 (推荐) --> <a href="http://www.atguigu.com" target="_blank" @click="incrCount($event)">prevent</a> <br> </div> </template> <style scoped> </style>
6.2 响应式基础
此处的响应式是指 : 数据模型发生变化时,自动更新DOM树内容,页面上显示的内容会进行同步变化,vue3的数据模型不是自动响应式的,需要我们做一些特殊的处理
6.2.1 响应式需求案例
需求:实现 + - 按钮,实现数字加一减一
<script type="module" setup> let counter = 0; function show(){ alert(counter); } </script> <template> <div> <button @click="counter--">-</button> {{ counter }} <button @click="counter++">+</button> <hr> <!-- 此案例,我们发现counter值,会改变,但是页面不改变! 默认Vue3的数据是非响应式的!--> <button @click="show()">显示counter值</button> </div> </template> <style scoped> </style>
6.2.2 响应式实现关键字ref
ref
可以将一个基本类型的数据(如字符串,数字等)转换为一个响应式对象。ref
只能包裹单一元素
<script type="module" setup> /* 从vue中引入ref方法 */ import {ref} from 'vue' let counter = ref(0); function show(){ alert(counter.value); } /* 函数中要操作ref处理过的数据,需要通过.value形式 */ let decr = () =>{ counter.value--; } let incr = () =>{ counter.value++; } </script> <template> <div> <button @click="counter--">-</button> <button @click="decr()">-</button> {{ counter }} <button @click="counter++">+</button> <button @click="incr()">+</button> <hr> <button @click="show()">显示counter值</button> </div> </template> <style scoped> </style>
- 在上面的例子中,我们使用
ref
包裹了一个数字,在代码中给这个数字加 1 后,视图也会跟着动态更新。需要注意的是,由于使用了ref
,因此需要在访问该对象时使用.value
来获取其实际值。
6.2.3 响应式实现关键字reactive
我们可以使用 reactive() 函数创建一个响应式对象或数组:
<script type="module" setup> /* 从vue中引入reactive方法 */ import {ref,reactive} from 'vue' let data = reactive({ counter:0 }) function show(){ alert(data.counter); } /* 函数中要操作reactive处理过的数据,需要通过 对象名.属性名的方式 */ let decr = () =>{ data.counter--; } let incr = () =>{ data.counter++; } </script> <template> <div> <button @click="data.counter--">-</button> <button @click="decr()">-</button> {{ data.counter }} <button @click="data.counter++">+</button> <button @click="incr()">+</button> <hr> <button @click="show()">显示counter值</button> </div> </template> <style scoped> </style>
对比ref和reactive:
- 使用
ref
适用于以下开发场景:
- 包装基本类型数据:
ref
主要用于包装基本类型数据(如字符串、数字等),即只有一个值的数据,如果你想监听这个值的变化,用ref
最为方便。在组件中使用时也很常见。
- 访问方式简单:
ref
对象在访问时与普通的基本类型值没有太大区别,只需要通过.value
访问其实际值即可。
- 使用
reactive
适用于以下开发场景: - 包装复杂对象:
reactive
可以将一个普通对象转化为响应式对象,这样在数据变化时会自动更新界面,特别适用于处理复杂对象或者数据结构。 - 需要递归监听的属性:使用
reactive
可以递归追踪所有响应式对象内部的变化,从而保证界面的自动更新。 - 综上所述,
ref
适用与简单情形下的数据双向绑定,对于只有一个字符等基本类型数据或自定义组件等情况,建议可以使用ref
;而对于对象、函数等较为复杂的数据结构,以及需要递归监听的属性变化,建议使用reactive
。当然,在实际项目中根据需求灵活选择也是十分必要的。
6.2.4 扩展响应式关键字toRefs 和 toRef
toRef基于reactive响应式对象上的一个属性,创建一个对应的 ref响应式数据。这样创建的 ref 与其源属性保持同步:改变源属性的值将更新 ref 的值,反之亦然。toRefs将一个响应式对象多个属性转换为一个多个ref数据,这个普通对象的每个属性都是指向源对象相应属性的 ref。每个单独的 ref 都是使用 toRef() 创建的。
案例:响应显示reactive对象属性
<script type="module" setup> /* 从vue中引入reactive方法 */ import {ref,reactive,toRef,toRefs} from 'vue' let data = reactive({ counter:0, name:"test" }) // 将一个reactive响应式对象中的某个属性转换成一个ref响应式对象 let ct =toRef(data,'counter'); // 将一个reactive响应式对象中的多个属性转换成多个ref响应式对象 let {counter,name} = toRefs(data) function show(){ alert(data.counter); // 获取ref的响应对象,需要通过.value属性 alert(counter.value); alert(name.value) } /* 函数中要操作ref处理过的数据,需要通过.value形式 */ let decr = () =>{ data.counter--; } let incr = () =>{ /* ref响应式数据,要通过.value属性访问 */ counter.value++; } </script> <template> <div> <button @click="data.counter--">-</button> <button @click="decr()">-</button> {{ data.counter }} & {{ ct }} <button @click="data.counter++">+</button> <button @click="incr()">+</button> <hr> <button @click="show()">显示counter值</button> </div> </template> <style scoped> </style>
6.3 条件和列表渲染
6.3.1 条件渲染
v-if
条件渲染
v-if='表达式'
只会在指令的表达式返回真值时才被渲染
- 也可以使用
v-else
为v-if
添加一个“else 区块”。 - 一个
v-else
元素必须跟在一个v-if
元素后面,否则它将不会被识别。
<script type="module" setup> import {ref} from 'vue' let awesome = ref(true) </script> <template> <div> <h1 v-if="awesome">Vue is awesome!</h1> <h1 v-else>Oh no 😢</h1> <button @click="awesome = !awesome">Toggle</button> </div> </template> <style scoped> </style>
v-show
条件渲染扩展:
- 另一个可以用来按条件显示一个元素的指令是
v-show
。其用法基本一样: - 不同之处在于
v-show
会在 DOM 渲染中保留该元素;v-show
仅切换了该元素上名为display
的 CSS 属性。
v-show
不支持在<template>
元素上使用,也不能和v-else
搭配使用。
<script type="module" setup> import {ref} from 'vue' let awesome = ref(true) </script> <template> <div> <h1 id="ha" v-show="awesome">Vue is awesome!</h1> <h1 id="hb" v-if="awesome">Vue is awesome!</h1> <h1 id="hc" v-else>Oh no 😢</h1> <button @click="awesome = !awesome">Toggle</button> </div> </template> <style scoped> </style>
v-if
vs v-show
v-if
是“真实的”按条件渲染,因为它确保了在切换时,条件区块内的事件监听器和子组件都会被销毁与重建。v-if
也是惰性的:如果在初次渲染时条件值为 false,则不会做任何事。条件区块只有当条件首次变为 true 时才被渲染。- 相比之下,
v-show
简单许多,元素无论初始条件如何,始终会被渲染,只有 CSSdisplay
属性会被切换。 - 总的来说,
v-if
有更高的切换开销,而v-show
有更高的初始渲染开销。因此,如果需要频繁切换,则使用v-show
较好;如果在运行时绑定条件很少改变,则v-if
会更合适。
6.3.2 列表渲染
我们可以使用
v-for
指令基于一个数组来渲染一个列表。
v-for
指令的值需要使用item in items
形式的特殊语法,其中items
是源数据的数组,而item
是迭代项的别名:- 在
v-for
块中可以完整地访问父作用域内的属性和变量。v-for
也支持使用可选的第二个参数表示当前项的位置索引。
<script type="module" setup> import {ref,reactive} from 'vue' let parentMessage= ref('产品') let items =reactive([ { id:'item1', message:"薯片" }, { id:'item2', message:"可乐" } ]) </script> <template> <div> <ul> <!-- :key不写也可以 --> <li v-for='item in items' :key='item.id'> {{ item.message }} </li> </ul> <ul> <!-- index表示索引,当然不是非得使用index这个单词 --> <li v-for="(item, index) in items" :key="index"> {{ parentMessage }} - {{ index }} - {{ item.message }} </li> </ul> </div> </template> <style scoped> </style>
- 案例:实现购物车显示和删除购物项
<script type="module" setup> //引入模块 import { reactive} from 'vue' //准备购物车数据,设置成响应数据 const carts = reactive([{name:'可乐',price:3,number:10},{name:'薯片',price:6,number:8}]) //计算购物车总金额 function compute(){ let count = 0; for(let index in carts){ count += carts[index].price*carts[index].number; } return count; } //删除购物项方法 function removeCart(index){ carts.splice(index,1); } </script> <template> <div> <table> <thead> <tr> <th>序号</th> <th>商品名</th> <th>价格</th> <th>数量</th> <th>小计</th> <th>操作</th> </tr> </thead> <tbody v-if="carts.length > 0"> <!-- 有数据显示--> <tr v-for="cart,index in carts" :key="index"> <th>{{ index+1 }}</th> <th>{{ cart.name }}</th> <th>{{ cart.price + '元' }}</th> <th>{{ cart.number }}</th> <th>{{ cart.price*cart.number + '元'}}</th> <th> <button @click="removeCart(index)">删除</button> </th> </tr> </tbody> <tbody v-else> <!-- 没有数据显示--> <tr> <td colspan="6">购物车没有数据!</td> </tr> </tbody> </table> 购物车总金额: {{ compute() }} 元 </div> </template> <style scoped> </style>