Javaweb 14.4 Vue3 视图渲染技术
目录
模板语法
插值表达式和文本渲染
属性渲染
事件的绑定
响应式基础
响应式需求案例
响应式实现关键字 ref
响应式实现关键字reactive
条件和列表渲染
条件渲染
列表渲染
双向绑定
属性计算
数据监听器
Vue 生命周期
简介
Vue 组件
组件基础
组件化入门案例
组件之间传递数据 - 兄弟传参
完!
模板语法
Vue 使用一种基于 HTML 的模板语法,是我们能够声明式地将其组件实例的数据绑定到呈现的 DOM 树中。在底层机制中,Vue 会将模板编译成高度优化的 JavaScript 代码。结合响应式系统,当应用状态发生变更,Vue 能够智能地推导出需要重新渲染的组件的最少数量,并应用最少的 DOM 操作。
仍然如同之前一般,利用 npm 命令在终端创建一个项目,并完成相关初始化。
插值表达式和文本渲染
插值表达式:最基本的数据绑定形式,语法格式为: {{ }}
是将数据渲染到元素的指定位置的手段之一;
不绝对依赖标签,其位置相对自由;
支持 Javascript 的运算表达式
支持函数的调用
举例如下:
<script setup>let msg = "hello vue3"let getMsg = () => {return "hello vue3 message"}let age = 19let bee = "蜜 蜂"// 购物车const carts = [{name:'可乐',price:3,number:10},{name:'薯片',price:6,number:8}]// 计算购物车总金额function compute() {let count = 0for (let index in carts) {count += carts[index].price * carts[index].number;}return count}
</script><template><div><h1>{{ msg }}</h1><br>通过 getMsg 函数来打印 msg {{ getMsg() }}<br>是否成年:{{ age >= 18 ? '是' : '否'}}<br>反转:{{ bee.split(' ').reverse().join('-') }} <br>购物车总金额:{{ compute() }}<br>购物车总金额:{{carts[0].price*carts[0].number + carts[1].price*carts[1].number}}</div>
</template><style scoped></style>
我们可以使用 v-text 和 v-html 命令,来对文字进行渲染
v-*** 这种写法的方式,使用的是 vue 命令
v-*** 的命令必须依赖元素,并且要写在元素的开始标签中
v-*** 指令支持 ES6 中的字符串模板
v-texxt 可以将数据渲染成双标签中间的文本,但不识别 html 元素结构的文本
v-html 可以将数据渲染成双标签中间的文本,识别 html 元素结构的文本
举例如下:
<script setup>let msg = "hello vue3"let getMsg = () => {return msg}let age = 19let 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 = 'yellow'>${msg}</font>`"></span><br></div>
</template><style scoped></style>
属性渲染
当我们想要渲染一个元素的 attribute,应该使用 v-bind 指令
由于插值表达式不能直接放在标签的属性中,要渲染元素的属性就需要使用 v-bind
v-bind 命令可以用于渲染任何元素的属性,语法为:v-bind:属性名='数据名',可以简写
:属性名 = '数据名'
举例如下:
<script setup>const data = {name:'百度',url:'https://www.baidu.com/',logo:'https://www.baidu.com/img/flexible/logo/pc/result.png'}
</script><template><div><av-bind:href='data.url'target='_self'><imgv-bind:src='data.logo'v-bind:title='data.name'><br><input type = "button"v-bind:value="`点击访问${data.name}`"></a></div>
</template><style scoped></style>
事件的绑定
我们可以使用 v-on 来监听 DOM 事件,并在事件触发的时候执行对应的 Vue 的 Javascript 代码
用法:v-on:click = "handler",可以简写为 @click="handler"
vue 中的事件名 = 原生事件名去掉 on 前缀,如 onClick --> click
handler 的值可以是方法事件处理器(逻辑定义在方法中,模板中仅引用方法名),也可以是内联事件处理器(逻辑直接写在模板中)
绑定事件时候,可以增加一些绑定的修饰符,常见的事件修饰符如下:
.once:只触发一次
.prevent:阻止默认事件
举例如下:
<script setup>import {ref} from 'vue'// 响应式数据 当发生变化,会自动更新 DOM 树let count = ref(0)let addCount = () => {count.value++}let incrCount = () => {count.value--}function fun() {// alert("超链接被点击了")let flag = confirm("确认要访问目标连接吗")if (!flag) {// 原生 js 编码方式阻止组件的默认行为event.preventDefault()}}function fun2() {alert("超链接被点击了")}</script><template><div><h1>count 的值是:{{count}}</h1><!-- 方法事件处理器 --><button v-on:click="addCount()">addCount</button><!-- 内联事件处理器 --><button @click="count++">addCount2</button><!-- 事件修饰符 once 只绑定一次事件 --><button @click.once="count--">incrCountOnce</button><br><a href = "https://www.baidu.com" v-on:click="fun($event)">百度</a><a href = "https://www.baidu.com" v-on:click.prevent="fun2()">百度</a></div>
</template><style scoped></style>
响应式基础
这里的响应式是指:数据模型(自定义的变量,对象)发生变化的时候,自动更新 DOM树的内容,页面上显示的内容会进行同步变化。vue3 的数据模型默认部署自动响应的,需要我们做一些特殊的处理。
响应式需求案例
需求:实现 + - 按钮,数字 +1 -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>
响应式实现关键字 ref
ref 可以将一个基本类型的数据(如字符串,数字等)转换成一个响应式对象。但,ref 只能包裹单一元素
<script 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 包裹了一个数字。但需要注意的是,在使用 ref 后,在 script 标签中,访问该对象时,需要使用 .value 来获取其实际的值。
响应式实现关键字reactive
我们可以使用 reactive() 函数创建一个响应式对象/数组
<script setup>/* 从 vue 中引入 reactive */import {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></div>
</template><style scoped></style>
对比 ref 和 reactive:
对于只有一个字符等基本类型数据或自定义组件情况,建议使用 ref
对于对象,函数等较为复杂的数据结构,建议使用 reactive
条件和列表渲染
条件渲染
v-if 条件渲染:
v-if = '表达式' 只会在指令的表达式返回真值时候被渲染
也可以使用 v-else 为 v-if 添加一个"else区块"
一个 v-else 元素必须跟在一个 v-if 元素后面,否则它不会被识别
<script setup>import {ref} from 'vue'let awesome = ref(true)
</script><template><div><h1 v-if="awesome">Vue is awesome!</h1><!-- v-else 自动和前一个 v-if 做一个取反操作 --><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 属性
<script 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 是"真实的"按条件渲染,它确保了在切换的时候,条件区块内的事件监听器和子组件都会被销毁与重建
v-if 也是惰性的:如果在初次渲染时,条件值为 false,则不会做任何事。只有当条件首次变为 true 的时候才会被渲染
v-show 简单许多,元素无论初始条件如何,始终会被渲染,只有 CSS 的 display 属性会被来回切换。
列表渲染
我们可以使用 v-for 指令基于一个数组来渲染一个列表
v-for 指令的值需要使用 item in items 形式的特殊语法,其中 items 是源数据的数据,而 item 是迭代项的别名
v-for 块中可以完整的访问父作用域内的属性和变量。v-for 也支持使用可选的第二个参数表示当前项的位置索引
<script setup>import {ref, reactive} from 'vue'let parentMessage = ref('产品')let items = reactive([{id:'item1',message:'薯片'},{id:'item2',message:'可乐'}])
</script><template><div><ul><li v-for = 'item in items' v-bind:key = 'item.id'>{{item.message}}</li></ul><ul><!-- index 表示索引 --><li v-for = "(item, index) in items" v-bind:key="index">{{parentMessage}} - {{index}} - {{item.message}}</li></ul></div>
</template><style scoped></style>
案例:实现购物车显示和删除购物项
<script 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].number*carts[index].price}return count}// 删除购物车的购物项的方法function removeCart(index) {carts.splice(index,1)}// 清空购物车function clearCart() {carts.splice(0,carts.length)}
</script><template><div><table style="border:1px solid"><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" v-bind:key="index"><td>{{index+1}}</td><td>{{cart.name}}</td><td>{{cart.price}}</td><td>{{cart.number}}</td><td>{{cart.price*cart.number}}</td><td><button @click="removeCart(index)">删除</button></td></tr></tbody><tbody v-else><tr><td colspan="6"> 购物车没有数据</td></tr></tbody><button @click="clearCart()">一键清空购物车</button></table>购物车总金额:{{compute()}}</div>
</template><style scoped></style>
双向绑定
单项绑定:响应式数据的变化会更新 DOM 树,但是 DOM 树上用户的操作造成的数据改变,不会同步更新到响应式数据
双向绑定:响应式数据的变化会更新 DOM 树,且 DOM 树上用户的操作造成的数据改变,会同步更新到响应式数据。
用户通过表单标签才能输入数据,所以双向绑定都是应用在表单标签上的,其他标签不可以
v-model 专门用于双向绑定表单标签的 value 属性。语法为:v-model :vale = "" 可以简写为
v-model=' '
v-model 还可以用于各种不同类型的输入 <textarea> <select> 元素
<script setup>import {ref, reactive} from 'vue'let user = reactive({username:'',userPwd:'',intro:'',pro:'',})let hbs = ref([])function clearForm() {user.username='',user.userPwd='',user.intro='',user.pro=''hbs.value.splice(0,hbs.value.length) // 注意 hbs 是 ref 类型的,需要加 value}
</script><template><div><input type = 'text' v-model = 'user.username'><br><input type = 'text' v-model = 'user.userPwd'><br>爱好:唱 <input type = 'checkbox' v-model = "hbs" value = 'sing'>跳 <input type = 'checkbox' v-model = 'hbs' value = 'dance'>rap <input type = 'checkbox' v-model = 'hbs' value = 'rap'><br>简介 <textarea v-model="user.intro"></textarea><br>籍贯:<select v-model = 'user.pro'><option value = '1'>京</option><option value = '2'>津</option><option value = '3'>冀</option></select><button @click="clearForm()">清空</button><br>{{user}} <br>{{hbs}}</div>
</template><style scoped></style>
属性计算
模板中的表达式虽然方便,但也只能用来做一些简单的操作。如果在模板中写太多逻辑,会让模板变得臃肿,难以维护。
我们可以使用计算属性来描述以来响应式状态的复杂逻辑:
<script setup>
import { reactive,computed } from "vue";const author = reactive({name: "zzzz",books: ["java", "javaweb", "数据结构与算法"],
});function hasBooks() {// 通过方法返回数据 每使用一次就执行一次return author.books.length>0?'yes':'no'
}
let bookMessage = computed(() => {// 计算属性,每次使用,如果和上次使用时,数据没有变化,则直接使用上一次的结果return author.books.length>0?'yes':'no'
})
</script><template><div><p>作者:{{author.name}}</p>是否出版过图书:{{hasBooks()}}是否出版过图书:{{bookMessage}}</div>
</template><style scoped></style>
注意:计算属性值会基于其响应式依赖被缓存,一个计算属性仅仅会在其响应式更新时候才重新计算,这意味着,只要 author.books 不改变,无论访问多少此 bookMessage 都会立即返回先前的计算结果
数据监听器
我们可以使用 watch 函数在每次响应式状态发生变化时,触发回调函数
watch 函数应用场景:
当数据发生变化时需要执行响应操作
监听数据变化,满足一定条件的时候触发响应操作
在异步操作前或操作后需要执行响应的操作
<script setup>
import { ref, reactive, watch } from "vue";
let firstname = ref("");
let lastname = reactive({ name: "" });
let fullname = ref("");// 监听一个 ref 响应式数据
watch(firstname, (newValue, oldValue) => {console.log(`${oldValue}变为${newValue}`);fullname.value = firstname.value + lastname.name;
});
// 监听一个 reactive 响应式数据的指定属性
watch(() => lastname.name,(newValue, oldValue) => {console.log(`${oldValue}变为${newValue}`);fullname.value = firstname.value+lastname.name}
);
// 监听 reactive 响应式数据的所有属性(深度监视)
// deep:true 深度监视
// immediate:true 深度监视在进入页面时立即实行一次// watch(()=>lastname,(newValue,oldValue)=>{// // 此时的 newValue 和 oldValue 都是一样的,都是 lastname// console.log(oldValue)// console.log(newValue)// fullname.value=firstname.value+lastname.name// },{deep:true,immediate:false})</script><template><div>全名:{{fullname}}<br>姓:<input type = "text" v-model='firstname'><br>名:<input type = "text" v-model="lastname.name"><br></div>
</template><style scoped></style>
监控响应式数据(watchEffect):
<script setup>
import { ref, reactive, watch, watchEffect } from "vue";let firstname = ref("");
let lastname = reactive({name:''});
let fullname = ref(" ");
// 监听所有响应式数据
watchEffect(() => {//直接在内部使用监听属性即可!// 默认初始化就加载,不需要回调设置console.log(firstname.value);console.log(lastname.name);fullname.value = `${firstname.value}${lastname.name}`;
});
</script><template><div>全名:{{ fullname }}<br />姓:<input type="text" v-model="firstname" /><br />名:<input type="text" v-model="lastname.name" /><br /></div>
</template><style scoped></style>
watch 和 watchEffect:
都能响应式地执行有副作用的回调。区别主要是追踪响应式依赖的方式:
watch 只追踪明确侦听的数据源,不会追踪任何在回调中访问的东西。仅在数据源确实发生变化的时候才会触发回调。
watchEffect 会在副作用发生期间追踪依赖。会在同步过程中,自动追踪所有能访问到的响应式数据。
Vue 生命周期
简介
每个 Vue 组件实例,在创建时都需要经历一系列的初始化步骤,比如设置好数据侦听,编译模板,挂在实例到 DOM,以及在数据改变时更新 DOM。在此过程中,它也会运行被称为生命周期钩子的函数,让开发者有机会在特定阶段运行自己的代码。
图解:
常见钩子函数:
onMounted() 注册一个回调函数,在组件挂载完成后执行。
onUpdated() 注册一个回调函数,在组件因为响应式状态变更而更新其 DOM 树之后调用
onUnmounted() 注册一个回调函数,在实例被携带之后调用
onBeforeMount() 注册一个钩子,在组件被挂载之前调用
onBeforeUpdate() 注册一个钩子,在组件即将因为响应式状态变更而更新其 DOM 树之前调用
onBeforeUnmount() 注册一个钩子,在组件实例被卸载之前调用
生命周期案例:
Vue 组件
组件基础
组件允许我们将 UI 划分为独立的,可重用的部分,并且可以对每个部分进行单独的设置。软件就是实现应用中局部功能代码和资源的集合。在实际应用中,组件常常被组织为层层嵌套的树状结构。
这与我们嵌套 HTML 元素的方式类似,Vue 实现了自己的组件模型
传统方式编写应用:
组件方式编写应用:
组件化入门案例
创建一个页面,包含头部,菜单以及内容显示区域,每个区域使用独立组件
准备 vue 项目:Header.vue Navigator.vue Content.vue
Header.vue:
<script setup></script><template><div>欢迎:xxx <a href = "#">退出登录</a></div>
</template><style scoped></style>
Navigator.vue:
<script setup></script><template><div><ul><li>学员管理</li><li>图书管理</li><li>请假管理</li><li>考试管理</li><li>班级管理</li><li>教师管理</li></ul></div>
</template><style scoped></style>
Content.vue:
<script setup></script><template><div>这里是展示主要内容</div>
</template><style scoped></style>
APP.vue 入口组件来引入组件:
<script setup>// 引入多个 .vue 组件import Header from './components/Header.vue'import Navigator from './components/Navigator.vue'import Content from './components/Content.vue'
</script><template><div><Header class = "header"></Header><Navigator class = "navigator"></Navigator><Content class="content"></Content></div>
</template><style scoped>.header{height: 80px;border: 1px solid red;}.navigator{width:15%;height: 800px;border:1px solid blue;float:left}.content{width:83%;height: 800px;border: 1px solid goldenrod;float:right}
</style>
启动测试:npm run dev,结果如下:
组件之间传递数据 - 兄弟传参
需求:在上面的页面中,当我们点击 Navigator 处的菜单,Content 处的主要内容区域会改变
Navigator:
<script setup>// 向父组件发送参数// defineEmits 用于定于向父组件提交数据的事件以及正式的提交数据import {defineEmits} from 'vue'// 定义一个向父组件提交数据的事件,事件名称自定义const emits = defineEmits(["sendMenu"])// 提交数据的方法function send(data) {emits("sendMenu",data)}
</script><template><div><ul><li @click="send('学员管理')">学员管理</li><li @click="send('图书管理')">图书管理</li><li @click="send('请假管理')">请假管理</li><li @click="send('考试管理')">考试管理</li><li @click="send('班级管理')">班级管理</li><li @click="send('教师管理')">教师管理</li></ul></div>
</template><style scoped></style>
App.vue
<script setup>// 引入多个 .vue 组件import {ref} from 'vue'import Header from './components/Header.vue'import Navigator from './components/Navigator.vue'import Content from './components/Content.vue'// 1. 定义响应式变量存储接收的数据let menu = ref("")// 2. 定义接收方法,更新 menu 的值function receiver(data) {menu.value = data // data 就是子组件传递过来的数据 }
</script><template><div>{{menu}}<Header class = "header"></Header><!-- 3. 在模板中监听子组件的 sendMenu事件,绑定接收方法 --><Navigator @sendMenu="receiver" class = "navigator"></Navigator><!-- 父传子 通过 :message 向 Content 组件传递数据 --><!-- :message 等价于 v-bind:message 是动态绑定的 Props 属性 --><Content class="content" :message="menu"></Content></div>
</template><style scoped>.header{height: 80px;border: 1px solid red;}.navigator{width:15%;height: 800px;border:1px solid blue;float:left}.content{width:83%;height: 800px;border: 1px solid goldenrod;float:right}
</style>