认识Vue
是一套用于构建用户界面的渐进式JavaScript框架 渐进式框架:表示我们可以在项目中一点点来引入和使用vue,而不一定需要全部使用vue来开发整个项目
如何使用Vue?
Vue的本质,就是一个JavaScript的库:
我们可以把它理解成一个已经帮我们封装好的库 在项目中可以引入并且使用它 使用方式:
在页面通过CDN方式引入 下载Vue的JS文件,并且自己手动引入 通过npm包管理工具安装使用
<! DOCTYPE html >
< html lang = " en" >
< head>
< meta charset = " UTF-8" >
< meta http-equiv = " X-UA-Compatible" content = " IE=edge" >
< meta name = " viewport" content = " width=device-width, initial-scale=1.0" >
< title> Document</ title>
</ head>
< body>
< script src = " https://cdn.jsdelivr.net/npm/vue@3.3.11/dist/vue.global.min.js" > </ script>
< div id = " app" > </ div>
< script>
const app = Vue. createApp ( {
template : "<h2>hello world</h2>"
} )
app. mount ( "#app" )
</ script>
</ body>
</ html>
案例一:动态数据
const app = Vue.createApp({
// 插值语法:{{}}
template:"<h2>{{title}}</h2>",
data:function(){
return {
title:'hello world',
message:"你好,Vue3"
}
}
})
app.mount("#app")
案例二:列表数据
const app = Vue. createApp ( {
template : `
<h2>电影列表</h2>
<ul>
<li v-for="item in movies">{{item}}</li>
</ul>
` ,
data : function ( ) {
return {
title : 'hello world' ,
movies : [ '大话西游' , "星际穿越" , "盗梦空间" , "少年派" ]
}
}
} )
app. mount ( "#app" )
案例三:计数器
const app = Vue. createApp ( {
template : `
<h2>当前计数:{{counter}}</h2>
<button @click="a">+1</button>
<button @click="b">-1</button>
` ,
data : function ( ) {
return {
counter : 0
}
} ,
methods : {
a : function ( ) {
this . counter++ ;
} ,
b : function ( ) {
this . counter-- ;
}
}
} )
app. mount ( "#app" )
声明式编程
Vue和React一样都是声明式编程 命令式编程关注的是“how to do”,自己完成整个how的过程 声明式编程关注的是“what to do”,由框架通过低层帮助我们完成"how"的过程
MVVM模型
MVC和MVVM都是一种软件的体系结构
MVC是model-view-controller的简称 MVVM是model-view-viewmodel的简称 通常情况下,我们也称Vue是一个MVVM模型
但是Vue官方说,Vue并没有完全遵守MVVM的模型,但是整个设计是受到它的启发的
options选项
data属性
data属性时传入一个函数,并且该函数需要返回一个对象:
在Vue2的时候,也可以传入一个对象 在Vue3的时候,必须传入一个函数 data中返回的对象会被Vue的响应式系统劫持,之后对该对象的修改或者访问都会在劫持中被处理
所以我们在template或者app中通过{{counter}}访问counter,可以从对象中获取到数据 所以我们修改counter的值时,app中的{{counter}}也会发生变化
< script src = " vue.js" > </ script>
< div id = " app" >
< h2> {{message}}</ h2>
< button @click = " change" > 改变message</ button>
</ div>
< script>
const app = Vue. createApp ( {
data : function ( ) {
return {
message : "hello world"
}
} ,
methods : {
change : function ( ) {
this . message = "helo data"
}
}
} )
app. mount ( "#app" )
</ script>
methods属性
methods属性是一个对象,通常我们会在这个对象中定义很多的方法:
这些方法可以被绑定在模版中 在该方法中,我们可以使用this关键字来直接访问到data中返回的对象中的属性
问题一:不能使用箭头函数?
官方解释:
箭头函数绑定了父级作用域的上下文,所以this将不会按照期望指向组件实例,this.counter将会是undefined 使用箭头函数,this会指向window
问题二:不使用箭头函数的情况下,this到底指向的是什么?
事实上Vue的源码当中就是对methods中的所有函数进行了遍历,并且通过bind绑定了this
模版语法
React的开发模式:
React使用的jsx,所以对应的代码都是编写的类似于js的一种语法 之后通过Babel将jsx编译成React.createElement函数调用 Vue也支持jsx的开发模式
但是大多数情况下,使用基于HML的模版语法 在模版中,允许开发者以声明式的方式将DOM和底层组件实例的数据绑定在一起 在底层的实现中,Vue将模版编译成虚拟DOM渲染函数
插值语法
插值语法也就是双大括号语法将数据显示到模版中 插值语法不仅仅可以是data中的属性,也可以是一个JS的表达式 或者methos中的函数
v-once指令
v-once用于指定元素或者组件只渲染一次
当数据发生变化时,元素或者组件以及所有的子元素将被视为静态元素并且跳过 该指令用于性能优化 子元素也会被渲染一次
v-text指令
用于更新元素的textContent 类似插值语法的作用
v-html
默认情况下,如果我们展示的内容本身是html的,那么vue并不会对齐进行特殊的解析
如果我们希望这个内容可以被Vue解析出来,那么可以使用v-html来展示
< script src = " vue.js" > </ script>
< div id = " app" >
< h2> {{message}}</ h2>
< h2 v-html = " message" > </ h2>
</ div>
< script>
const app = Vue. createApp ( {
data : function ( ) {
return {
message : ` <span>helo v-html</span> `
}
} ,
} )
app. mount ( "#app" )
</ script>
v-pre
v-pre用于跳过元素和它的子元素的编译过程,显示原始的插值语法标签
v-cloak
这个指令保持在元素上直到关联组件实例结束编译
和CSS一起使用时,可以隐藏未编译的插值语法标签直到组件实例准备完毕
<! DOCTYPE html >
< html lang = " en" >
< head>
< meta charset = " UTF-8" >
< meta http-equiv = " X-UA-Compatible" content = " IE=edge" >
< meta name = " viewport" content = " width=device-width, initial-scale=1.0" >
< title> Document</ title>
</ head>
< style>
[v-cloak] {
display : none;
}
</ style>
< body>
< script src = " vue.js" > </ script>
< div id = " app" >
< h2 v-cloak > {{message}}</ h2>
</ div>
< script>
setTimeout ( ( ) => {
const app = Vue. createApp ( {
data : function ( ) {
return {
message : ` helo world `
}
} ,
} )
app. mount ( "#app" )
} , 500 )
</ script>
</ body>
</ html>
v-memo
< div v-memo = " [name]" >
< h2> {{name}}</ h2>
< h2> {{age}}</ h2>
< h2> {{height}}</ h2>
</ div>
v-bind绑定属性
某些属性我们也希望动态来绑定
比如动态绑定a元素的href伤心 比如动态绑定img元素的src属性
绑定基本属性
v-bind用于绑定一个或多个属性值,或者向另一个组件传递prop值 语法糖写法:
<! DOCTYPE html >
< html lang = " en" >
< head>
< meta charset = " UTF-8" >
< meta http-equiv = " X-UA-Compatible" content = " IE=edge" >
< meta name = " viewport" content = " width=device-width, initial-scale=1.0" >
< title> Document</ title>
</ head>
< body>
< script src = " vue.js" > </ script>
< div id = " app" >
< img v-bind: src= " imgURL" alt = " " >
< a v-bind: href= " href" > 百度</ a>
</ div>
< script>
const app = Vue. createApp ( {
data : function ( ) {
return {
message : ` helo world ` ,
imgURL : "https://img1.baidu.com/it/u=1546227440,2897989905&fm=253&fmt=auto&app=138&f=JPEG?w=889&h=500" ,
href : "http://www.baidu.com"
}
} ,
} )
app. mount ( "#app" )
</ script>
</ body>
</ html>
绑定class
在开发中,有时候我们的元素class也是动态的,比如:
当数据为某个状态时,字体为红色 当数据为另一个状态时,字体显示黑色 动态绑定的class可以和普通class同时使用 对象语法:我们可以传给:class一个对象,以动态的切换class 数组语法:我们可以把一个数组传给:class,以应用一个class列表
< style>
.active {
color : red;
}
</ style>
< body>
< script src = " vue.js" > </ script>
< div id = " app" >
< h2 :class = " classes" > hello world</ h2>
< button :class = " isActive?'active':''" @click = " btn" > 按钮</ button>
< button :class = " {active:isActive}" @click = " btn" > 按钮</ button>
< div :class = " ['a',{active:isActive}]" > 哈哈哈</ div>
</ div>
< script>
const app = Vue. createApp ( {
data : function ( ) {
return {
message : ` helo world ` ,
classes : "abc cba" ,
isActive : false
}
} ,
methods : {
btn : function ( ) {
this . isActive = ! this . isActive
}
}
} )
app. mount ( "#app" )
</ script>
</ body>
绑定style属性
我们可以使用v-bind:style来绑定一些CSS内联样式:
CSS属性名使用驼峰式或哦者短横线分隔(记得用引号括起来)来命名
< h2 :style = " {color:fontColor,fontSize:'30px'}" > hhh</ h2>
动态绑定属性
某些情况下,我们需要动态绑定属性名
如果属性名是不固定的,我们可以使用:[属性名]="值"的格式来定义
< div :[name] = " value" > {{message}}</ div>
v-on绑定事件
前端开发中,需要经常和用户进行各种各样的交互
需要监听用户发生的事件,比如点击、拖拽、键盘事件等 在Vue中使用v-on指令监听事件
< div class = " box" v-on: click= " btn" > </ div>
< div class = " box" @click = " btn" > </ div>
< div v-on = " {click:btn, mousemove:divMousemove}" > </ div>
v-on 传递参数
< button @click = " btn" > </ button>
< button @click = " btn(" why")" > </ button>
< button @click = " btn(" why",$event)" > </ button>
v-on修饰符
v-on支持修饰符,修饰符相当于对事件进行了一些特殊的处理
.stop 调用event.stopPropagation() .prevent 调用event.preventDefault() .capture 添加事件侦听器使用capture模式 .self 只当事件是从侦听器绑定的元素本身触发时才触发回调 .{keyAlias} 仅当事件是从特定键触发时才触发回调 .once 只触发一次回调 .left 只当点击鼠标左键触发 .right 只当点击鼠标右键时触发 .middle 只当点击鼠标中键时触发 .passive {passive:true}模式添加侦听器
< button @click.stop = " btn" > button</ button>
条件渲染
Vue提供了下面的指令进行条件判断
v-if v-else v-else-if v-show
< body>
< script src = " vue.js" > </ script>
< div id = " app" >
< div v-if = " Object.keys(info).length" >
< h2> 个人信息</ h2>
< ul>
< li> 姓名:{{info.name}}</ li>
< li> 年龄:{{info.age}}</ li>
</ ul>
</ div>
< div v-else >
< h2> 没有信息</ h2>
</ div>
< h2 v-if = " score > 90" > 1</ h2>
< h2 v-else-if = " score > 80" > 2</ h2>
< h2 v-else > 3</ h2>
</ div>
< script>
const app = Vue. createApp ( {
data : function ( ) {
return {
message : ` helo world ` ,
info : {
name : "gpl" ,
age : 18
} ,
score : 0
}
}
} )
app. mount ( "#app" )
</ script>
</ body>
template元素
因为v-if是一个指令,所以必须将其添加到一个元素上:
此时我们渲染div,但是我们并不希望div这种元素被渲染 这个时候,我们可以选择template template元素可以被当作不可见的包裹元素
v-show
类似v-if,也是根据一个条件决定是否显示元素或者组件
< template id = " my-app>
<h2 v-show=" isShow" > hhhh</ h2>
</ template>
和v-if的区别
不支持template,因为template没有display样式来切换 不可以和v-else一起使用 本质区别
v-show元素无论是否显示到浏览器上,它的DOM实际都是存在的,只是同多CSS的display来切换 v-if当条件为false时,其对应的原生根本不会被渲染到DOM中 开发中选择
如果我们的元素需要在显示和隐藏之间频繁切换,使用v-show 否则使用v-if
v-for 列表渲染
遍历数组
遍历对象
格式:v-for=“(value,key,index) in obj” 遍历字符串
v-for = “item in message” 遍历数字
最外层可以使用template元素
数组更新检测
Vue将被侦听的数组的变更方法进行了包裹,所以它们也将会触发视图更新 这些被包裹的方法包括: push() pop() shift() unshift() splice() sort() reverse() 不修改原数组的方法不被侦听
v-for中的key是什么作用?
在使用v-for进行列表渲染时,我们通常会给元素或者组件绑定一个key属性
< li v-for = " item in letters" :key = " item" > {{item}}</ li>
这个key属性有什么作用呢?
官方解释: key属性主要用在Vue的虚拟DOM算法,在新旧nodes对比时辨识VNodes 不使用key,Vue会使用一种最大限度减少动态元素并且尽可能尝试就地修改/复用相同类型元素的算法 而使用key采用另一种算法,会基于key的变化重新排列元素顺序,并且会移除/销毁key不存在的元素
认识VNode
VNode全称是Virtual Node,也就是虚拟节点 无论是组件还是元素,在Vue中表现出来的都是一个个VNode VNode本质是一个JS对象
< div class = " title" style = " font-size : 30px; color : red; " > helo world</ div>
const vnode = {
type : "div" ,
props : {
class : "title" ,
style : {
class : "title" ,
style : {
"font-size" : "30px" ,
color : "red"
}
}
} ,
children : "hahaha"
}
虚拟DOM
多个VNode,会形成一个VNode Tree 虚拟DOM的作用
使用diff算法 实现跨平台
key在列表元素中间插入新元素的作用
在Vue中,对于相同父元素的子元素节点并不会重新渲染整个列表 因为对于列表中a、b、c它们都是没有变化的 在操作真实DOM时,只需要插入一个f的li即可 有key时,vue调用patchKeyedChildren方法 没有key时,使用patchUnkeyedChildren方法
Vue的compouted
复杂data的处理方式
我们可以通过插值语法显示一些data中的数据
在模版中放入太多的逻辑会让模版过重和难以维护 并且如果多个地方都使用到,会有大量重复的代码 我们有什么办法可以将逻辑抽离出去呢?
将逻辑抽离到method中 使用计算属性computed
认识计算属性computed
什么是计算属性?
对于任何包含响应式数据的复杂逻辑,都应该使用计算属性 计算属性将被混入到组件实例中
所有getter和setter的this上下文自动地绑定为组件实例(即可以使用this访问数据)
< div id = " app" >
< h2> {{fullname}}</ h2>
</ div>
< script>
const app = Vue. createApp ( {
data : function ( ) {
return {
a : "a" ,
b : "b" ,
}
} ,
computed : {
fullname ( ) {
return this . a + this . b
}
}
} )
app. mount ( "#app" )
</ script>
computed vs methods
计算属性是有缓存的
测试:如果在函数中添加console.log,会发现methods会多次执行,而计算属性只执行一次,这是vue做的性能优化 计算属性会基于它们的依赖关系进行缓存 在数据不发生变化时,计算属性时不需要重新计算的 但是如果依赖的数据发生变化,在使用时,计算属性依然会重新进行计算
计算属性的set和get写法
<! DOCTYPE html >
< html lang = " en" >
< head>
< meta charset = " UTF-8" >
< meta http-equiv = " X-UA-Compatible" content = " IE=edge" >
< meta name = " viewport" content = " width=device-width, initial-scale=1.0" >
< title> Document</ title>
</ head>
< style>
</ style>
< body>
< script src = " vue.js" > </ script>
< div id = " app" >
< h2> {{fullname}}</ h2>
< button @click = " setFullname" > button</ button>
</ div>
< script>
const app = Vue. createApp ( {
data : function ( ) {
return {
a : "a" ,
b : "b" ,
}
} ,
computed : {
fullname : {
get : function ( ) {
return this . a + this . b
} ,
set : function ( value ) {
const names = value. split ( " " )
this . a = names[ 0 ]
this . b = names[ 1 ]
}
}
} ,
methods : {
setFullname ( ) {
this . fullname = "helo world"
}
} ,
} )
app. mount ( "#app" )
</ script>
</ body>
</ html>
认识侦听器watch
data返回的对象中定义了数据,这个数据通过插值语法绑定到template中 当数据变化时,template会自动进行更新 某些情况下,我们希望在代码逻辑中监听某个数据的变化,就需要watch来实现
< div id = " app" >
< h2> {{message}}</ h2>
< button @click = " change" > button</ button>
</ div>
< script>
const app = Vue. createApp ( {
data : function ( ) {
return {
message : "hello world"
}
} ,
methods : {
change ( ) {
this . message = "hello watch"
}
} ,
watch : {
message ( ) {
console. log ( 'message发生了变化' ) ;
}
}
} )
app. mount ( "#app" )
</ script>
v-model
表单提交是开发中非常常见的功能 要求我们可以在代码逻辑中获取到用户提交的数据,通常使用v-model指令来完成
v-model指令可以在表单input、textarea以及select中创建双向数据绑定 会根据控件类型自动选取正确的方法来更新元素 本质上不是语法糖,负责监听用户的输入事件来更新数据,并在某种极端场景下进行一些特殊操作
< div id = " app" >
< h2> {{message}}</ h2>
< input type = " text" v-model = " message" >
</ div>
v-model的原理
官方解释:v-model的原理其实就是背后两个操作
v-bind绑定value属性的值 v-on绑定input事件监听到函数,函数会获取最新的值赋值到绑定的属性中
< input v-model = " searchText" />
< input :value = " searchText" @input = " searchText = $event.target.value" />
v-model修饰符-lazy
加上lazy修饰符,那么会将绑定的事件切换成change事件,只有在提交时(回车)才会触发
v-model修饰符-number
v-model绑定后的值总是string类型 我们希望转换成数字类型,那么可以使用.number修饰符
v-model修饰符-trim
Vue组件化开发基础
Vue组件化开发思想
一个页面拆分成一个个小的功能块,每个功能块完成自己的独立的功能,那么之后整个页面的管理和维护就变得非常容易 我们需要通过组件化的思想来思考整个应用程序
一个完整的页面拆分成多个组件 每个组件实现一个功能块 每个组件又进行细分 组件本身可以复用
注册Vue的全局组件
全局组件:在任何其他的组件中都可以使用的组件 注册方式:
需要使用我们全局创建的app来注册组件 通过component方法传入组件名称、组件对象即可注册一个全局组件了 之后,我们可以在APP组件的template中直接使用这个全局组件 使用app.component注册第一个组件时,第一个参数是组件的名称,定义组件名的方式有两种
< script src = " vue.js" > </ script>
< div id = " app" >
< h2> {{message}}</ h2>
< input type = " text" v-model = " message" >
< product-item> </ product-item>
</ div>
< script>
const App = {
data : function ( ) {
return {
message : "hello world"
}
}
}
const productItem = {
template : ` <div class="product">
<h2>我是商品</h2>
</div> `
}
const app = Vue. createApp ( App)
app. component ( "product-item" , productItem)
app. mount ( "#app" )
</ script>
注册Vue的局部组件
局部组件:只有在注册的组件中才能使用的组件 全局组件往往在应用程序一开始就会全局组件完成,那么就意味如果某些组件我们没有用到,也会一起被打包
比如我们注册了三个全局组件:ComponentA ComponentB ConponentC 在开发中我们只使用A和B,没有用到C但是我们依然在全局进行了注册,那么webpack也会对其进行打包 会增加用户下载对应的JS是的包大小 所以开发中通常注册的是局部组件
局部注册由我们需要使用到的组件中,通过components属性选项来进行注册
< script src = " vue.js" > </ script>
< div id = " app" >
< h2> {{message}}</ h2>
< Home-Nav> </ Home-Nav>
< product-item>
</ product-item>
</ div>
< script>
const App = {
components : {
productItem : {
template : ` <div class="product">
<h2>我是商品</h2>
</div> `
} ,
HomeNav : {
template : ` <div class="product">
<h2>HomeNav</h2>
</div> `
}
} ,
data : function ( ) {
return {
message : "hello world"
}
}
}
const app = Vue. createApp ( App)
app. mount ( "#app" )
</ script>
</ body>
Vue的开发模式解析
目前的开发模式存在很多问题 真实开发中,我们可以通过后缀名为.vue的单文件组件来解决问题,并且通过打包工具对其进行处理
单文件的特点
ES6、CommonJS的模块化能力 组件作用域的CSS 可以使用预处理器来构建更加丰富的组件,比如TS、Babel、Less等
< template>
< h2> </ h2>
</ template>
< script>
module. exports = {
data : function ( ) {
return {
}
}
</ script>
< style scoped >
p {
font-size : 2em;
}
</ style>
Vue项目脚手架生成
方式一:安装Vue CLI 使用vuecli 生成项目(基于webpack)
vue create app
方式二: 安装并执行 create-vue,它是 Vue 官方的项目脚手架工具(基于vite)
npm create vue@latest
其中jsconfig.json的作用:
给VSCode来进行读取,VSCode在读取到其中的内容时,给我们的代码更加友好的提示
< ! -- < script>
export default {
data ( ) {
return {
title : "hello world"
} ;
}
} ;
< / script> -- >
< ! -- 使用< script setup> 时,不再需要像传统的Vue组件那样显式声明data属性。< script setup> 是一种简化的语法,Vue. js 3.0 中引入的特性,它会自动将模板中使用的变量注入到组件中。 -- >
< script setup>
const title = "hello world"
< / script>
< template>
< h2> { { title } } < / h2>
< / template>
< style scoped>
< / style>
import './assets/main.css'
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
const app = createApp ( App)
app. use ( router)
app. mount ( '#app' )
组件间通信
父子组件之间通信的方式
父子组件之间如何进行通信呢?
父传子:通过props属性 子传父:通过$emit触发事件
父传子
什么是props
是你可以在组件上注册的一些自定义的attribute 子组件通过attribute的名称获取到对应的值 props有两种常见的用法:
方式一:字符串数,数组中的字符串就是attribute的名称 方式二:对象类型,我们可以指定attribute名称的同时,指定它需要传递的类型、是否是必须的、默认值等等
< script>
import Showinfo from './components/Showinfo.vue' ;
export default {
components : {
Showinfo
} ,
data ( ) {
return {
title : "hello world"
} ;
}
} ;
< / script>
< template>
< showinfo name= "a" age= "18" height= "1.88" > < / showinfo>
< showinfo name= "b" age= "28" height= "1.90" > < / showinfo>
< / template>
< style scoped>
< / style>
< template>
< div class = "infos" >
< h2> 姓名: { { name } } < / h2>
< h2> 年龄: { { age } } < / h2>
< h2> 身高: { { height } } < / h2>
< / div>
< / template>
< script>
export default {
props : {
name : {
type : String,
required : true ,
default : "default"
} ,
age : {
type : Number,
default : 0
} ,
height : {
type : Number,
default : 2
}
}
}
< / script>
< style scoped>
< / style>
props对象类型的细节
props : {
friend : {
type : Object,
default : ( ) => { { name : "james" } }
} ,
friend2 : {
type : Array,
default : ( ) => { [ "a" , "b" ] }
}
}
子传父
什么情况下子组件需要传递内容给父组件?
当子组件有一些事件发生时,父组件需要切换内容 子组件有一些内容想要传递给父组件的时候 如何传递
现在子组件中定义好在某些情况下触发的事件名称 在父组件中以v-on的方式传入要监听的事件名称,并且绑定到对应的方法中 在子组件发生某个事件的时候,根据事件名称触发对应的事件
< template>
< div class = "add" >
< button @click= "btnClick(1)" > + 1 < / button>
< button @click= "btnClick(5)" > + 5 < / button>
< button @click= "btnClick(10)" > + 10 < / button>
< / div>
< / template>
< script>
export default {
methods : {
btnClick ( count ) {
this . $emit ( "add" , count)
}
}
}
< / script>
< template>
< div class = "sub" >
< button @click= "btnClick(-1)" > - 1 < / button>
< button @click= "btnClick(-5)" > - 5 < / button>
< button @click= "btnClick(-10)" > - 10 < / button>
< / div>
< / template>
< script>
export default {
methods : {
btnClick ( count ) {
this . $emit ( "sub" , count)
}
}
}
< / script>
< script>
import AddCounter from './components/AddCounter.vue' ;
import SubCounter from './components/SubCounter.vue' ;
export default {
components : {
AddCounter,
SubCounter
} ,
data ( ) {
return {
counter : 0
} ;
} ,
methods : {
addBtnClick ( count ) {
this . counter += count
} ,
subBtnClick ( count ) {
console. log ( count) ;
this . counter += count
}
}
} ;
< / script>
< template>
< div class = "app" >
< h2> 当前计数:{ { counter } } < / h2>
< add- counter v- on: add= "addBtnClick" > < / add- counter>
< sub- counter @sub= "subBtnClick" > < / sub- counter>
< / div>
< / template>
< style scoped>
< / style>
非父子组件通信
Provide/Inject
Provide/Inject用于非父子组件之间共享数据:
比如有一些深度嵌套的组件,子组件想要获取父组件的部分内容 在这种情况下,如果我们仍然将props沿着组件链逐级传递下去,就会非常麻烦 父组件有一个provide选项来提供数据 子组件有一个inject选项来开始使用这些数据
< template>
< div>
< h2> Home< / h2>
< home- banner> < / home- banner>
< / div>
< / template>
< script>
import HomeBanner from './HomeBanner.vue' ;
export default {
components : {
HomeBanner
}
}
< / script>
< style scoped> < / style>
< template>
< h2>
HomeBanner : { { name } } { { age } }
< / h2>
< / template>
< script>
export default {
inject : [ "name" , "age" ]
}
< / script>
< style scoped> < / style>
< script>
import Home from './components/Home.vue' ;
import { computed } from 'vue' ;
export default {
components : {
Home
} ,
data ( ) {
return {
massage : "helo app"
}
} ,
provide ( ) {
return {
name : "why" ,
age : 18 ,
massage : this . message,
message2 : computed ( ( ) => this . message)
}
}
} ;
< / script>
< template>
< div class = "app" >
< home> < / home>
< / div>
< / template>
< style scoped>
< / style>
全局事件总线
Vue3中移除了
o
n
、
on、
o n 、 off和$once方法,所以我们如果希望继续使用全局事件总线,要通过第三方的库:
Vue3官方有推荐一些库,例如mitt和tiny-emitter 这里使用hy-event-store
npm install hy-event-store utiles文件夹的event-bus.js
import { HYEventBus} from "hy-event-store"
const eventBus = new HYEventBus ( )
export default eventBus
components文件夹中的HomeBanner.vue
< template>
< h2> HomeBanner< / h2>
< button @click= "bannerBtnClick" > banner< / button>
< / template>
< script>
import eventBus from '../utils/event-bus'
export default {
methods : {
bannerBtnClick ( ) {
eventBus. emit ( "why" , 18 )
}
}
}
< / script>
< style scoped>
< / style>
App.vue
< script>
import Home from './components/Home.vue' ;
import eventBus from './utils/event-bus'
export default {
components : {
Home
} ,
data ( ) {
return {
massage : "helo app"
}
} ,
methods : {
whyEvent ( args ) {
console. log ( "app中监听" , args)
}
} ,
created ( ) {
eventBus. on ( "why" , this . whyEvent)
eventBus. off ( "why" , this . whyEvent)
}
} ;
< / script>
< template>
< div class = "app" >
< home> < / home>
< / div>
< / template>
< style scoped>
< / style>
插槽Slot
插槽Slot的作用
在开发中,我们经常封装一个个可复用的组件:
为了让这个组件更加具有通用性,我们不能将组件中的内容限制为固定的div、span等元素 比如某些情况下我们使用组件,希望组件显示是一个按钮或者一张图片 我们希望让使用者来决定某一块区域到底存放什么内容和元素 这个时候我们就可以来定义插槽Slot
插槽的使用过程其实是抽取共性、预留不同 我们会将共同的元素、内容依然在组件内进行封装 同时会将不同的元素作为占位,让外部决定到底显示什么样的元素 如何使用插槽slot?
Vue中将元素作为承载分发内容的出口 在封装组件中,使用特殊的元素就可以作为封装组件开启一个插槽 该插槽插入什么内容取决于父组件如何使用
插槽Slot的基本使用
< template>
< h1> { { title } } < / h1>
< div class = "content" >
< slot>
< p> 插槽默认内容< / p>
< / slot>
< / div>
< / template>
< script>
export default {
props : {
title : {
type : String,
default : " "
} ,
content : {
type : String,
default : " "
}
}
}
< / script>
< style scoped>
< / style>
< script>
import ShowMessage from './components/ShowMessage.vue' ;
export default {
components : {
ShowMessage
} ,
data ( ) {
return {
massage : "helo app"
}
} ,
} ;
< / script>
< template>
< div class = "app" >
< show- message title= "slot" content= "插槽" >
< button> 我是按钮元素< / button>
< / show- message>
< show- message title= "slot2" content= "插槽2" >
< a href= "http://www.baiud.com" > 百度一下< / a>
< / show- message>
< show- message title= "slot3" content= "插槽3" >
< / show- message>
< / div>
< / template>
< style scoped>
< / style>
具名插槽Slot使用
当有多个插槽时,我们希望插槽有对应的显式,这个时候我们就可以使用具名插槽
给插槽起一个名字,元素有一个特殊的attribute:name 一个不带name的slot,会带有隐含的名字default 动态插槽名
我们可以通过v-slot:[name]方式动态绑定一个名称
< template>
< div class = "nav-bar" >
< div class = "left" >
< slot name= "left" > left< / slot>
< / div>
< div class = "center" >
< slot name= "center" > center< / slot>
< / div>
< div class = "right" >
< slot name= "right" > right< / slot>
< / div>
< / div>
< / template>
< template>
< div class = "app" >
< show- message title= "slot" content= "插槽" >
< ! -- 插入具名插槽 -- >
< template v- slot: left>
< button> 返回< / button>
< / template>
< template v- slot: center>
< span> 内容< / span>
< / template>
< template v- slot: right>
< a href= "#" > 登录< / a>
< / template>
< / show- message>
< / div>
< / template>
作用域插槽Slot使用
渲染作用域:
无论是父组件自己的tamplate或者是子组件插槽的tamplate,只要是在父级模版中,那么都是在父级作用域中编译的 子模版中的所有内容都是在子作用域中编译的 作用域slot:
即在父组件自定义插槽时,可以使用子组件插槽标签中传递过来的数据
< div>
< slot : item= "item" >
< span> { { item } } < / span>
< / slot>
< / div>
< show- message title= "slot" content= "插槽" >
< template #default = "props" >
< button> { { props. item } } < / button>
< / template>
< / show- message>
如果我们的插槽是默认插槽default
那么在使用的时候v-slot:default="slotProps"可以简写成v-slot=“slotProps” 组件的标签可以被当作插槽的模版来使用,即可以省略template,直接将v-slot直接用在组件上
组件化的额外知识补充
组件的生命周期
什么是生命周期?
每个组件都可能经历从创建、挂载、更新、卸载等一系列过程 在这个过程中的某一个阶段,我们可能会想要添加一些属于自己的代码逻辑(比如组件创建完就请求一些服务器数据) 生命周期函数:
生命周期函数是一些钩子函数(回调函数),在某个时间被Vue源码内部进行回调 通过对生命周期的回调,我们可以知道目前组件正在经历什么阶段 也可以在该生命周期函数中编写属于自己的逻辑代码
组件中的ref引用($refs)
某些情况下,我们在组件中想要直接获取到元素对象或者子组件实例
在Vue开发中是不推荐进行原生DOM操作的 这个时候,我们可以给元素或者组件绑定一个ref的attribute属性 组件实例也有一个$refs属性:
它一个对象Object,持有注册过ref attribute的所有DOM元素和组件实例
< template>
< div class = "app" >
< h2 ref= "title" > hello world< / h2>
< button ref= "btn" @click= "change" > 修改title< / button>
< banner ref= "banner" > < / banner>
< / div>
< / template>
< script>
import Banner from './components/Banner.vue' ;
export default {
methods : {
change ( ) {
console. log ( this . $refs. title, this . $refs. btn)
console. log ( this . $refs. banner)
console. log ( this . $refs. banner. $el)
}
} ,
components : {
Banner
}
}
< / script>
< style scoped> < / style>
动态组件的使用
比如我们想实现一个功能:
有两种不同的实现思路来实现
方式一:通过v-if来判断,显示不同的组件 方式二:动态组件的方式
动态组件使用component组件,通过一个特殊的attribute is来实现
< div class = "app" >
< ! -- 方式一: -- >
< template v- if = "index == 0" >
< banner> 1 < / banner>
< / template>
< template v- else = "index==2" >
< banner> 2 < / banner>
< / template>
< ! -- 方式二 -- >
< ! -- is中的组件需要来自全局注册的组件或者局部注册的组件 -- >
< ! -- tabs是包含需要显示的组件的数组 -- >
< component : is= "tabs[index]" > < / component>
< / div>
keep-alive的使用
当我们切换组件时,为了避免组件频繁的创建和卸载,可以使用keep-alive 对于缓存的组件来说,再次进入时,我们呢不会执行created或者mounted等生命周期的
但是我们希望监听到何时重新进入到了组件,何时离开了组件 这个时候可以使用activated和deactivated这两个生命周期钩子函数来监听 keep-alive有一些属性:
include: 只有名称匹配的组件会被缓存 exclude:任何名称匹配的组件都不会被缓存 max:最多可以缓存多少组件实例,一旦到达这个数字,那么缓存组件中没有被访问的实例会被销毁
< keep-alive include = " home,about" >
< component :is = " tabs[index]" > </ component>
</ keep-alive>
异步组件的使用
默认的webpack打包过程:
默认情况下,在构建整个组件树的过程中,因为组件和组件之间是通过模块化直接依赖的,那么webpack在打包时就会将组件模块打包到一起(比如一个app.js文件中) 这个时候随着项目不断庞大,app.js文件的内容过大,会造成首屏的渲染速度变慢 所以需要在打包时对代码进行分包
对于一些不需要立即使用的组件,单独对他们进行拆分,拆分成一些小的代码块chunk.js 这些chunk.js会在需要时从服务器加载下来,并且运行代码,显示对应的内容
import ( "./utils/math" ) . then ( res => {
res. sum ( 20 , 30 )
} )
import { defineAsyncComponent } from 'vue' ;
const AsyncBanner = defineAsyncComponent ( ( ) => import ( "./components/Banner.vue" ) )
组件的v-model
在input中可以使用v-model来完成双向绑定
v-model默认帮助我们完成了两件事:v-bind:value的数据绑定和@input的数据监听 组件上使用v-model
< my-input v-model = " message" />
< my-input :model-value = " message" @update: model-value= " messag = $event" > </ my-input>
组件的混入Mixin
目前组件化开发中,组件和组件有时候存在相同的代码逻辑,我们希望对相同的代码逻辑进行抽取 Vue2和Vue3中都支持的一种方式就是使用Mixin来完成
Mixin提供了一种非常灵活的方式,来分发Vue组件中的可复用功能 一个Mixin对象可以包含任何组件选项 当组件使用Mixin对象时,所有Mixin对象的选项将被混入进入组件本身的选项中 Mixin的合并规则 情况一:data函数的返回对象
返回值对象默认情况下会进行合并 如果data返回值对象的属性发生了冲突,那么会保留组件自身的数据 情况二:生命周期钩子函数
情况三:值为对象的选项,例如methos、components和directives,将被合并为同一个对象
比如都有methods选项,并且都定义了方法,那么它们都会生效 但是如果对象的key相同,那么会取组件对象的键值对
import './assets/main.css'
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp ( App)
app. mixin ( {
created ( ) {
console. log ( "mixin created" ) ;
}
} )
app. mount ( '#app' )
export default {
data ( ) {
return {
message : "hello world"
}
} ,
created ( ) {
console. log ( "message" , this . message)
}
}
< template>
< h2> Home</ h2>
</ template>
< script>
import messageMixin from "../mixins/message-mixin"
export default {
mixins : [ messageMixin]
}
</ script>
< style scoped >
</ style>
Vue3 Composition API
认识CompositionAPI
在Vue2中,编写组件的方式是Options API
特点是在对应的属性中编写对应的功能模块 比如data定义数据、methods定义方法、computed定义计算属性、watch中监听属性变化 Options API的弊端
当我们实现某一个功能时,这个功能对应的代码逻辑会被拆分到各个属性中 当我们组件变得更大、更复杂时,同一个功能的逻辑会被拆分的很分散 Composition API可以帮助我们把同一个逻辑关注点相关的代码收集在一起 在组件中,编写Composition API的位置是setup函数
Setup函数的基本使用
setup函数的参数
props就是父组件传递过来的属性会被放到props对象中,我们在setup中如果需要使用,那么就可以直接通过props参数获取:
对于定义props的类型,我们还是和之前的规则是一样的,在props选项中定义 并且在template中正常去使用props中的属性 如果我们在setup函数中想要获取props,不可以通过this去获取 因为props有直接作为参数传递到setup函数中,我们可以直接通过参数来使用即可 context,包含三个属性
attrs:所有非prop的attribute slots:父组件传递过来的插槽 emit:当我们组件内部需要发出事件时会用到emit(因为不能访问this,所以不能通过this.$emit发出事件)
< template>
< div class = "app" >
< h2> 当前计数: { { counter } } < / h2>
< button @click= "addClick" > + 1 < / button>
< button @click= "subClick" > - 1 < / button>
< / div>
< / template>
< script>
import { ref } from "vue"
export default {
setup ( ) {
let counter = ref ( 100 )
const addClick = ( ) => {
counter. value++
}
const subClick = ( ) => {
counter. value--
}
return {
counter,
addClick,
subClick
}
}
}
< / script>
< style scoped> < / style>
Setup中数据的响应式
如果想在setup中定义的数据提供响应式的特性,可以使用reactive的函数
reactive函数处理我们的数据之后,数据再次被使用就会进行依赖收集 当数据发生改变时,所有收集到的依赖都是进行对应的响应式操作(比如更新界面) 我们之前编写的data选项,也是在内部交给reactive函数将其变成响应式对象的
< template>
< div class = "app" >
< h2> message: { { message } } < / h2>
< h2> 账号:{ { account. username } } < / h2>
< h2> 密码:{ { account. password } } < / h2>
< button @click= "changeAccount" > 修改账号< / button>
< / div>
< / template>
< script>
import { ref, reactive } from "vue"
export default {
setup ( ) {
const message = "hello world"
const account = reactive ( {
username : "codewhy" ,
password : "123456"
} )
function changeAccount ( ) {
account. username = "big"
}
return {
message,
account,
changeAccount
}
}
}
< / script>
< style scoped> < / style>
Reactive API对传入的类型是有限制的,它要求我们必须传入的是一个对象或者数组类型
这个时候我们可以使用ref API
ref会返回一个可变的响应式对象,该对象作为一个响应式的引用维护者它内部的值,这就是ref名称的来源 它内部的值是在ref的value属性中被维护的 其中在模版中使用ref的值时,Vue会自动帮助我们进行解包操作(自动提取value值) 但是在setup函数内部,依然是一个ref引用,需要使用ref.value的方式
Vue中单向数据流的规范和做法
单向数据流(规范)
子组件拿到数据后只能 使用,不能修改 如果确实要修改,那么应该将事件传递出去,由父组件统一进行修改 readonly
Vue3为我们提供了readonly方法 readonly会返回原生对象的只读代理
setup中禁用this
官方关于this的描述:
表达的含义是this并没有指向当前组件实例 并且在setup被调用之前,data、computed、methos等都没有被解析 所以无法在setup中获取this
setup中computed函数使用
< template>
< h2> { { fullname } } < / h2>
< / template>
< script>
import { reactive, computed } from 'vue'
export default {
setup ( ) {
const names = reactive ( {
firstname : "kobe" ,
lastname : "bryant"
} )
const fullname = computed ( ( ) => {
return names. firstname + " " + names. lastname
} )
return {
names,
fullname
}
}
}
< / script>
setup中生命周期函数
script setup语法糖
< script setup>
console. log ( 'hello world")
< / script>
Vue-Router
认识前端路由
路由其实是网络工程中的一个术语
在架构一个网络时,非常重要的两个设备就是路由器 和** 交换机** 事实上,路由器主要维护的是一个映射表 映射表 会决定数据的流向多个电脑通过路由器 连接网络,路由器给每台设备分配一个ip地址 (私网ip),每一个ip地址对应电脑独一无二的mac地址 路由的概念在软件工程中出现,最早是在后端路由中实现的,原因是web的发展主要经历了这样一些阶段:
后端路由阶段:后端根据url渲染出完整网页,再根据服务器返回到前端显示 前后端分离阶段:属于后端路由,但是一些数据根据axios网络请求动态变化 单页面富应用(SPA):==url地址改变,渲染不同的组件,url映射组件而不是整个页面 ==
URL的hash
前端路由是如何做到URL和内容进行映射的?监听URL的改变 URL的hash
URL的hash也就是描点(#),本质上是改变window.location的href属性 我们可以通过直接赋值location.hash来改变href,但是页面不发生刷新 本质就是对location.hash的监听事件
< div id = " app" >
< a href = " #/home" > home</ a>
< a href = " #/about" > about</ a>
< div class = " router-view" > </ div>
</ div>
const routerViewEl = document. querySelector ( ".router-view" )
window. addEventListener ( "hashchange" , ( ) => {
switch ( location. hash) {
case "#/home" :
routerViewEl. innerHTML = "home" ;
break ;
case "#/about" :
routerViewEl. innerHTML = "about"
break ;
default :
routerViewEl. innerHTML = "default"
}
} )
HTML5的History
history接口是HTML5新增的,它有六种模式改变URL而不刷新页面:
replaceState:替换原来的路径 pushState:使用新的路径 popState:路径的回退 go:向前或向后改变路径 forward:向前改变路径 back:向后改变路径
const routerViewEl = document. querySelector ( ".router-view" )
const aEls = document. getElementsByTagName ( "a" ) ;
for ( let aEl of aEls) {
aEl. addEventListener ( "click" , ( e ) => {
e. preventDefault ( ) ;
const href = aEl. getAttribute ( "href" ) ;
history. pushState ( { } , "" , href) ;
historyChange ( )
} )
}
window. addEventListener ( "popstate" , historyChange) ;
window. addEventListener ( "go" , historyChange) ;
function historyChange ( ) {
switch ( location. pathname) {
case "/home" :
routerViewEl. innerHTML = "home" ;
break ;
case "/about" ;
routerViewEl. innerHTML = "about"
break ;
default :
routerViewEl. innerHTML = "default"
}
}
Vue-Router基本使用
npm install vue- router
src目录下新建router文件夹,其中创建index.js文件,创建路由对象
import { createRouter, createWebHashHistory } from 'vue-router'
import Home from '../Views/Home.vue'
import About from '../Views/About.vue'
const router = createRouter ( {
history : createWebHashHistory ( ) ,
routes : [
{
path : '/home' ,
component : Home
} ,
{
path : '/about' ,
component : About
}
]
} )
export default router
import './assets/main.css'
import router from './router'
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp ( App)
app. use ( router)
app. mount ( '#app' )
router-view的占位,router-link进行路由切换
< script setup> < / script>
< template>
< div class = "app" >
< h2> App Content< / h2>
< div class = "nav" >
< router- link to= "/home" > 首页< / router- link>
< router- link to= "/about" > 关于< / router- link>
< / div>
< router- view> < / router- view>
< / div>
< / template>
< style scoped> < / style>
路由的默认路径
routes : [
{
path : '/' ,
redirect : '/home'
} ,
{
path : '/home' ,
component : Home
} ,
{
path : '/about' ,
component : About
}
]
router-link
to属性
replace属性:
设置replace属性的话,当点击时,会调用router.replace(),而不是router.push() active-class属性:
设置激活a元素后应用的class,默认是router-link-active exact-active-class属性:
链接精准激活时,应用于渲染的的class,默认是router-link-exact-active
路由懒加载分包处理
当打包构建应用时,JS包会变得非常大,影响页面加载
如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就会更高效 也可以提高首屏的渲染效率
const Home = ( ) => import ( '../Views/Home.vue' )
const About = ( ) => import ( '../Views/About.vue' )
路由的其他属性
name属性:路由记录独一无二的名称 meta属性:自定义的数据
{
name : 'home' ,
path : '/home' ,
component : Home,
meta : {
name : 'name' ,
age : 18
}
} ,
动态路由和路由嵌套
动态路由基本匹配
很多时候我们需要将给定匹配模式的路由映射到同一个组件
例如我们有一个User组件,应该对所有用户进行渲染,但是用户的ID是不同的 在Vue Router中,我们可以在路径中使用一个动态字段来实现,我们称之为 路径参数
{
path : '/user/:id' ,
component : ( ) => import ( '../Views/User.vue' )
}
< router- link to= "/user/123" > 用户123 < / router- link>
< router- link to= "/user/456" > 用户456 < / router- link>
<script setup>
import { onBeforeRouteUpdate, useRoute } from 'vue-router'
// 第一次获取
const route = useRoute()
console.log(route.params.id)
onBeforeRouteUpdate((to, from) => {
console.log('from', from.params.id)
console.log('to', to.params.id)
})
</script>
<template>
<!-- 在模版中获取到id -->
<div>User:{{ $route.params.id }}</div>
</template>
<style scoped></style>
NotFound
对于那些没有匹配到的路由,我们通常会匹配到固定的某个页面
比如NotFound的错误页面中,这个时候我们可以编写一个动态路由用于匹配所有的页面
{
path : '/:pathMatch(.*)' ,
component : ( ) => import ( '../Views/NotFound.vue' )
}
< div> {{ $route.params.pathMatch }}-----NotFound</ div>
路由的嵌套
<script setup></script>
<template>
<div>home</div>
<router-link to="/home/recommend">推荐</router-link>
<router-link to="/home/ranking">排行</router-link>
<router-view></router-view>
</template>
<style scoped></style>
{
name : 'home' ,
path : '/home' ,
component : ( ) => import ( '../Views/Home.vue' ) ,
meta : {
name : 'name' ,
age : 18
} ,
children : [
{
path : 'recommend' ,
component : ( ) => import ( '../Views/HomeRecommend.vue' )
} ,
{
path : 'ranking' ,
component : ( ) => import ( '../Views/HomeRanking.vue' )
}
]
} ,
路由的编程式导航
homeClick ( ) {
this . $router. push ( '/home' )
}
homeClick ( ) {
this . $router. push ( {
path : '/home'
} )
}
import { useRouter } from 'vue-router'
const router = useRouter ( )
function homeClick ( ) {
router. push ( {
path : '/home' ,
query : {
name : 'name'
}
} )
}
function aboutClick ( ) {
router. push ( '/about' )
}
动态添加路由
某些情况下我们需要动态的来添加路由:
比如根据用户不同的权限,注册不同的路由 这个时候我们可以使用一个方法addRoute
let isAdmin = true
if ( isAdmin) {
router. addRoute ( {
path : '/admin' ,
component : ( ) => import ( '../Views/Admin.vue' )
} )
router. addRoute ( 'home' , {
path : 'vip' ,
component : ( ) => import ( '../Views/HomeVip.vue' )
} )
}
删除路由有以下三种方式:
方式一:添加一个name相同的路由 方式二:通过removeRoute方法,传入路由的名称 方式三:通过addRoute方法的返回值回调
路由导航守卫
vue-router提供的导航守卫主要用来通过跳转或取消的方式守卫导航 全局的前置守卫beforeEach是在导航触发时会被回调的 它有两个参数:
to:即将进去的路由Route对象 from:即将离开的路由Route对象 它有返回值:
false:取消当前导航 不返回或者undefined:进行默认导航 返回一个路由地址:
可以是一个string类型的路径 可以是一个对象,对象包含path、query、params等信息 可选的第三个参数:next(不推荐)
在vue2中我们是通过next函数来决定如何进行跳转的 但是在Vue3中我们是通过返回值来控制的,不再推荐使用next函数,这是因为开发中很容易调用多次next
< script setup >
import { useRouter } from 'vue-router'
const router = useRouter ( )
function loginClick ( ) {
console. log ( '点击了登录' )
localStorage. setItem ( 'token' , 'token' )
router. push ( '/order' )
}
</ script>
< template>
< div>
< h2> 登录页面</ h2>
< button @click = " loginClick" > 登录</ button>
</ div>
</ template>
< style scoped > </ style>
router. beforeEach ( ( to, from ) => {
const token = localStorage. getItem ( 'token' )
if ( ! token && to. path === '/order' ) {
return '/login'
}
} )
Vuex状态管理
认识应用状态管理
开发中,应用程序需要处理各种各样的数据,这些数据需要保存在我们应用程序的某一个位置,对于这些数据的管理我们就称之为状态管理 之前是如何管理自己的状态的?
在Vue开发中,我们使用组件化 的开发方式 而在组件中我们定义data或者setup中返回使用的数据 ,这些数据我们称之为state 在模块template 中我们可以使用这些数据,模块最终会被渲染成DOM,我们称之为View 在模块中我们会产生一些行为事件 ,处理这些行为事件时,有可能修改state,这些行为事件我们称之为actions
需要管理的数据越来越复杂
JS需要管理的状态越来越多,越来越复杂 这些状态包括服务器返回的状态、缓存数据、用户操作产生的数据等等 也包括一些UI状态,比如某些元素是否被选中,是否显示加载动效,当前分页 当我们的应用遇到多个组件共享状态时,单向数据流的简洁性很容易被破坏
多个视图依赖于同一状态 来自不同视图的行为需要变更同一状态