6.4 双向绑定
单项绑定和双向绑定
- 单向绑定: 响应式数据的变化会更新dom树,但是dom树上用户的操作造成的数据改变不会同步更新到响应式数据
- 双向绑定: 响应式数据的变化会更新dom树,但是dom树上用户的操作造成的数据改变会同步更新到响应式数据
- 用户通过表单标签才能够输入数据,所以双向绑定都是应用到表单标签上的,其他标签不行
- v-model专门用于双向绑定表单标签的value属性,语法为
v-model:value=''
,可以简写为v-model=''
- v-model还可以用于各种不同类型的输入,
<textarea>
、<select>
元素。
<script type="module" setup> //引入模块 import { reactive,ref} from 'vue' let hbs = ref([]); //装爱好的值 let user = reactive({username:null,password:null,introduce:null,pro:null}) function login(){ alert(hbs.value); alert(JSON.stringify(user)); } function clearx(){ //user = {};// 这中写法会将数据变成非响应的,应该是user.username="" user.username='' user.password='' user.introduce='' user.pro='' hbs.value.splice(0,hbs.value.length);; } </script> <template> <div> 账号: <input type="text" placeholder="请输入账号!" v-model="user.username"> <br> 密码: <input type="text" placeholder="请输入账号!" v-model="user.password"> <br> 爱好: 吃 <input type="checkbox" name="hbs" v-model="hbs" value="吃"> 喝 <input type="checkbox" name="hbs" v-model="hbs" value="喝"> 玩 <input type="checkbox" name="hbs" v-model="hbs" value="玩"> 乐 <input type="checkbox" name="hbs" v-model="hbs" value="乐"> <br> 简介:<textarea v-model="user.introduce"></textarea> <br> 籍贯: <select v-model="user.pro"> <option value="1">黑</option> <option value="2">吉</option> <option value="3">辽</option> <option value="4">京</option> <option value="5">津</option> <option value="6">冀</option> </select> <br> <button @click="login()">登录</button> <button @click="clearx()">重置</button> <hr> 显示爱好:{{ hbs }} <hr> 显示用户信息:{{ user }} </div> </template> <style scoped> </style>
6.5 属性计算
模板中的表达式虽然方便,但也只能用来做简单的操作。如果在模板中写太多逻辑,会让模板变得臃肿,难以维护。比如说,我们有这样一个包含嵌套数组的对象:
<script type="module" setup> //引入模块 import { reactive,computed} from 'vue' const author = reactive({ name: 'John Doe', books: [ 'Vue 2 - Advanced Guide', 'Vue 3 - Basic Guide', 'Vue 4 - The Mystery' ] }) </script> <template> <div> <p>{{author.name}} Has published books?:</p> <span>{{ author.books.length > 0 ? 'Yes' : 'No' }}</span> </div> </template> <style scoped> </style>
- 这里的模板看起来有些复杂。我们必须认真看好一会儿才能明白它的计算依赖于
author.books
。更重要的是,如果在模板中需要不止一次这样的计算,我们可不想将这样的代码在模板里重复好多遍。
因此我们推荐使用计算属性来描述依赖响应式状态的复杂逻辑。这是重构后的示例:
<script type="module" setup> //引入模块 import { reactive,computed} from 'vue' const author = reactive({ name: 'John Doe', books: [ 'Vue 2 - Advanced Guide', 'Vue 3 - Basic Guide', 'Vue 4 - The Mystery' ] }) // 一个计算属性 ref const publishedBooksMessage = computed(() => { console.log("publishedBooksMessage") return author.books.length > 0 ? 'Yes' : 'No' }) // 一个函数 let hasBooks = ()=>{ console.log("hasBooks") return author.books.length > 0?'Yes':'no' } </script> <template> <div> <p>{{author.name}} Has published books?:</p> <span>{{ author.books.length > 0 ? 'Yes' : 'No' }}</span> <span>{{ hasBooks() }}</span><!-- 调用方法,每个标签都会调用一次 --> <span>{{ hasBooks() }}</span> <p>{{author.name}} Has published books?:</p> <span>{{ publishedBooksMessage }}</span><!-- 属性计算,属性值不变时,多个个标签只会调用一次 --> <span>{{ publishedBooksMessage }}</span> </div> </template> <style scoped> </style>
我们在这里定义了一个计算属性 publishedBooksMessage。computed() 方法期望接收一个 getter 函数,返回值为一个计算属性 ref。和其他一般的 ref 类似,你可以通过 publishedBooksMessage.value 访问计算结果。计算属性 ref 也会在模板中自动解包,因此在模板表达式中引用时无需添加 .value。
Vue 的计算属性会自动追踪响应式依赖。它会检测到 publishedBooksMessage 依赖于 author.books,所以当 author.books 改变时,任何依赖于 publishedBooksMessage 的绑定都会同时更新。
计算属性缓存 vs 方法
若我们将同样的函数定义为一个方法而不是计算属性,两种方式在结果上确实是完全相同的,然而,不同之处在于计算属性值会基于其响应式依赖被缓存。一个计算属性仅会在其响应式依赖更新时才重新计算。这意味着只要 author.books 不改变,无论多少次访问 publishedBooksMessage 都会立即返回先前的计算结果!
6.6 数据监听器
计算属性允许我们声明性地计算衍生值。然而在有些情况下,我们需要在状态变化时执行一些“副作用”:例如更改 DOM,或是根据异步操作的结果去修改另一处的状态。我们可以使用 watch 函数在每次响应式状态发生变化时触发回调函数:
- watch主要用于以下场景:
- 当数据发生变化时需要执行相应的操作
- 监听数据变化,当满足一定条件时触发相应操作
- 在异步操作前或操作后需要执行相应的操作
监控响应式数据(watch):
<script type="module" 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(newValue) console.log(oldValue) 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):
- watchEffect默认监听所有的响应式数据
<script type="module" 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
vs. watchEffect
watch
和watchEffect
都能响应式地执行有副作用的回调。它们之间的主要区别是追踪响应式依赖的方式:watch
只追踪明确侦听的数据源。它不会追踪任何在回调中访问到的东西。另外,仅在数据源确实改变时才会触发回调。watch
会避免在发生副作用时追踪依赖,因此,我们能更加精确地控制回调函数的触发时机。
watchEffect
,则会在副作用发生期间追踪依赖。它会在同步执行过程中,自动追踪所有能访问到的响应式属性。这更方便,而且代码往往更简洁,但有时其响应性依赖关系会不那么明确。
6.7. Vue生命周期
6.7.1 生命周期简介
每个 Vue 组件实例在创建时都需要经历一系列的初始化步骤,比如设置好数据侦听,编译模板,挂载实例到 DOM,以及在数据改变时更新 DOM。在此过程中,它也会运行被称为生命周期钩子的函数,让开发者有机会在特定阶段运行自己的代码!
- 周期图解:
常见钩子函数
- onMounted() 注册一个回调函数,在组件挂载完成后执行。
- onUpdated() 注册一个回调函数,在组件因为响应式状态变更而更新其 DOM 树之后调用。
- onUnmounted() 注册一个回调函数,在组件实例被卸载之后调用。
- onBeforeMount() 注册一个钩子,在组件被挂载之前被调用。
- onBeforeUpdate() 注册一个钩子,在组件即将因为响应式状态变更而更新其 DOM 树之前调用。
- onBeforeUnmount() 注册一个钩子,在组件实例被卸载之前调用。
6.7.2 生命周期案例
<script setup> import {ref,onUpdated,onMounted,onBeforeUpdate} from 'vue' let message =ref('hello') // 挂载完毕生命周期 onMounted(()=>{ console.log('-----------onMounted---------') let span1 =document.getElementById("span1") console.log(span1.innerText) }) // 更新前生命周期 onBeforeUpdate(()=>{ console.log('-----------onBeforeUpdate---------') console.log(message.value) let span1 =document.getElementById("span1") console.log(span1.innerText) }) // 更新完成生命周期 onUpdated(()=>{ console.log('-----------onUpdated---------') let span1 =document.getElementById("span1") console.log(span1.innerText) }) </script> <template> <div> <span id="span1" v-text="message"></span> <br> <input type="text" v-model="message"> </div> </template> <style scoped> </style>
6.8 Vue组件
6.8.1 组件基础
组件允许我们将 UI 划分为独立的、可重用的部分,并且可以对每个部分进行单独的思考。组件就是实现应用中局部功能代码和资源的集合!在实际应用中,组件常常被组织成层层嵌套的树状结构:
- 这和我们嵌套 HTML 元素的方式类似,Vue 实现了自己的组件模型,使我们可以在每个组件内封装自定义内容与逻辑。
传统方式编写应用:
组件方式编写应用:
- 组件化:对js/css/html统一封装,这是VUE中的概念
- 模块化:对js的统一封装,这是ES6中的概念
- 组件化中,对js部分代码的处理使用ES6中的模块化
6.8.2 组件化入门案例
案例需求: 创建一个页面,包含头部和菜单以及内容显示区域,每个区域使用独立组建!
1 准备vue项目
npm create vite cd vite项目 npm install
2 安装相关依赖
npm install sass npm install bootstrap
3 创建子组件 在src/components文件下 vscode需要安装Vetur插件,这样vue文件有快捷提示
- Header.vue
<script setup type="module"> </script> <template> <div> 欢迎: xx <a href="#">退出登录</a> </div> </template> <style> </style>
- Navigator.vue
<script setup type="module"> </script> <template> <!-- 推荐写一个根标签--> <div> <ul> <li>学员管理</li> <li>图书管理</li> <li>请假管理</li> <li>考试管理</li> <li>讲师管理</li> </ul> </div> </template> <style> </style>
- Content.vue
<script setup type="module"> </script> <template> <div> 展示的主要内容! </div> </template> <style> </style>
- App.vue 入口组件App引入组件
<script setup> 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; display: inline-block; border: 1px blue solid; float: left; } .content{ width: 83%; height: 800px; display: inline-block; border: 1px goldenrod solid; float: right; } </style>
4 启动测试
npm run dev
6.8.3 组件之间传递数据
6.8.3.1 父传子
Vue3 中父组件向子组件传值可以通过 props 进行,具体操作如下:
- 首先,在父组件中定义需要传递给子组件的值,接着,在父组件的模板中引入子组件,同时在引入子组件的标签中添加 props 属性并为其设置需要传递的值。
- 在 Vue3 中,父组件通过 props 传递给子组件的值是响应式的。也就是说,如果在父组件中的传递的值发生了改变,子组件中的值也会相应地更新。
- 父组件代码:App.vue
<script setup> import Son from './components/Son.vue' import {ref,reactive,toRefs} from 'vue' let message = ref('parent data!') let title = ref(42) function changeMessage(){ message.value = '修改数据!' title.value++ } </script> <template> <div> <h2>{{ message }}</h2> <hr> <!-- 使用子组件,并且传递数据! --> <Son :message="message" :title="title"></Son> <hr> <button @click="changeMessage">点击更新</button> </div> </template> <style scoped> </style>
- 子组件代码:Son.vue
<script setup type="module"> import {ref,isRef,defineProps} from 'vue' //声明父组件传递属性值 defineProps({ message:String , title:Number }) </script> <template> <div> <div>{{ message }}</div> <div>{{ title }}</div> </div> </template> <style> </style>
6.8.3.2 子传父
- 父组件: App.vue
<script setup> import Son from './components/Son.vue' import {ref} from 'vue' let pdata = ref('') const padd = (data) => { console.log('2222'); pdata.value =data; } //自定义接收,子组件传递数据方法! 参数为数据! const psub = (data) => { console.log('11111'); pdata.value = data; } </script> <template> <div> <!-- 声明@事件名应该等于子模块对应事件名!调用方法可以是当前自定义!--> <Son @add="padd" @sub="psub"></Son> <hr> {{ pdata }} </div> </template> <style> </style>
- 子组件:Son.vue
<script setup> import {ref,defineEmits} from 'vue' //1.定义要发送给父组件的方法,可以1或者多个 let emites = defineEmits(['add','sub']); let data = ref(1); function sendMsgToParent(){ console.log('-------son--------'); //2.出发父组件对应的方法,调用defineEmites对应的属性 emites('add','add data!'+data.value) emites('sub','sub data!'+data.value) data.value ++; } </script> <template> <div> <button @click="sendMsgToParent">发送消息给父组件</button> </div> </template>
6.8.3.3 兄弟传参
- Navigator.vue: 发送数据到App.vue
<script setup type="module"> 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> </ul> </div> </template> <style> </style>
- App.vue: 发送数据到Content.vue
<script setup> import Header from './components/Header.vue' import Navigator from './components/Navigator.vue' import Content from './components/Content.vue' import {ref} from "vue" //定义接受navigator传递参数 var navigator_menu = ref('ceshi'); const receiver = (data) =>{ navigator_menu.value = data; } </script> <template> <div> <hr> {{ navigator_menu }} <hr> <Header class="header"></Header> <Navigator @sendMenu="receiver" class="navigator"></Navigator> <!-- 向子组件传递数据--> <Content class="content" :message="navigator_menu"></Content> </div> </template> <style scoped> .header{ height: 80px; border: 1px solid red; } .navigator{ width: 15%; height: 800px; display: inline-block; border: 1px blue solid; float: left; } .content{ width: 83%; height: 800px; display: inline-block; border: 1px goldenrod solid; float: right; } </style>
- Content.vue
<script setup type="module"> defineProps({ message:String }) </script> <template> <div> 展示的主要内容! <hr> {{ message }} </div> </template> <style> </style>