响应式是vue框架主要的特点,在开发的过程里,vue响应式特性的使用是非常频繁的,常见的是通过数据绑定的方式将变量的值渲染在页面里,当变量的值发生变化,页面对应的元素也更新
响应式编程的原理及在vue中的应用
响应式的本质是对变量实现监听,当监听到变量发生变化,我们可以做一些预定义的逻辑。例如数据绑定技术,需要在变量改动对页面元素进行刷新
响应式的原理在生活里随处可见,开关和点灯的关系,通过开关来改变电灯的状态,复杂的例如使用excel,对数据统计时可以使用公式,当公式的变量发生变化,结果也会变化
手动追踪变量的变化
<script>
let a=1
let b=2
let sum=a+b
console.log(sum);
a=3
b=3
console.log(sum);
</script>
上面的代码无法正确武城响应式,我们需要能够监听会影响最终sum变量值的自变量的变化,也就是监听a和b的变化。在js里,可以使用proxy对原对象进行包装,实现对对象属性设置和获取的监听
<script>
let a={
value:1
};
let b={
value:2
}
handleA={
get(target,prop){
console.log(`获取A:${prop}的值`);
return target[prop]
},
set(target,key,value){
console.log(`设置A:${key}的值${value}`);
}
}
handleB={
get(target,prop){
console.log(`获取A:${prop}的值`);
return target[prop]
},
set(target,key,value){
console.log(`设置B:${key}的值${value}`);
}
}
let pa=new Proxy(a,handleA)
let pb=new Proxy(b,handleB)
let sum=pa.value+pb.value
pa.value=3
pa.value=4
</script>
proxy对象在初始化的时候需要传入有个要包装的对象和对于的处理器,处理器里可以定义get和set方法,创建的新代理对象的用法和原对象完全一致,只是在对其内部属性进行获取和设置的时候,都会被处理器定义的get或set方法所拦截。结果如下
现在,尝试让sum变量具有响应式
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="https://unpkg.com/vue@next"></script>
</head>
<body>
<script>
let a={
value:1
};
let b={
value:2
}
let trigger=null
handleA={
get(target,prop){
console.log(`获取A:${prop}的值`);
return target[prop]
},
set(target,key,value){
console.log(`设置A:${key}的值${value}`);
target[key]=value
if(trigger){
trigger()
}
}
}
handleB={
get(target,prop){
console.log(`获取A:${prop}的值`);
return target[prop]
},
set(target,key,value){
console.log(`设置B:${key}的值${value}`);
target[key]=value
if(trigger){
trigger()
}
}
}
let pa=new Proxy(a,handleA)
let pb=new Proxy(b,handleB)
let sum=0
trigger=()=>{
sum=pa.value+pb.value
}
pa.value=3
pb.value=4
console.log(sum);
</script>
</body>
</html>
vue里的响应式对象
js对象的proxy对象是可以实现对象的响应式的。在vue里,大多数情况下都不需要关心数据的响应式问题,因为按照vue组件模板编写组件的元素时,data方法里返回的数据默认都是有响应式的。但是在某些特殊情况,我们还是需要对某些数据进行特殊的响应式的处理
在v3引入了组合式api的新特性,这种新特性可以让我们在setup方法里定义组件所需的数据和函数。setup方法可以在组件被创建前定义组件所需要的数据和函数
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="https://unpkg.com/vue@next"></script>
</head>
<body>
<div id="Application">
</div>
<script>
const App=Vue.createApp({
setup(){
let myData={
value:0
}
function click(){
myData.value++
console.log(myData.value);
}
return{
myData,
click
}
},
template:`
<h1>测试数据:{{myData.value}}</h1>
<button @click="click">点击</button>
`
})
App.mount("#Application")
</script>
</body>
</html>
虽然mydata的value属性通过打印可以看到是一直在变化的,但是页面并没有刷新,还是显示的0.这是因为mydata对象是我们自己定义的普通js对象,本身是没有响应式的,对其修改也不会同步刷新到页面上,这和我们常规使用组件的data方法返回的数据不同,data方法返回的数据会被默认保存为proxy对象,从而获得响应式
为了解决这种问题,v3提供了reactive方法,使用此方法对js对象进行封装,即可以方便的为其添加响应式,修改一下代码
setup(){
let myData=Vue.reactive({
value:0
})
function click(){
myData.value++
console.log(myData.value);
}
return{
myData,
click
}
},
这个时候页面就能正常渲染
独立的响应式ref的应用
在实际的开发里,很多时候只是需要的一个独立的原始值,对于上一小节的代码,我们需要的只是一个数值,对于这种场景,不需要手动将其包装为对象的属性,可以直接使用vue提供的ref方法来定义响应式独立值,ref会帮我们完成对象的包装
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="https://unpkg.com/vue@next"></script>
</head>
<body>
<div id="Application">
</div>
<script>
const App=Vue.createApp({
setup(){
let myObject=Vue.ref(0)
function click(){
myObject.value++
console.log(myObject.value);
}
return{
myObject,
click
}
},
template:`
<h1>测试数据:{{myObject}}</h1>
<button @click="click">点击</button>
`
})
App.mount("#Application")
</script>
</body>
</html>
运行效果是一样的,但需要注意的是,使用ref方法创建响应式对象后,在setup方法里想修改护具,需要对myobject里面的value属性来进行修改,value属性值是vue内部生成的,但是对于setup方法导出的数据来说,我们在模板使用的myobject数据已经是最终的独立值,可以直接进行使用,模板中使用setup返回的使用ref定义的数据时,数据对象会被自动展开
vue里还提供了一个名为torefs的方法来支持响应式对象的解构赋值,可以直接将js对象里的属性进行解构,从而直接赋值给变量使用
script>
const App=Vue.createApp({
setup(){
let myObject=Vue.reactive({
value:0
})
let {value}=myObject
function click(){
value++
console.log(value);
}
return{
value,
click
}
},
template:`
<h1>测试数据:{{value}}</h1>
<button @click="click">点击</button>
`
})
App.mount("#Application")
</script>
但是又出现了一开始的问题,也就是代码失去1响应式,对于这种情况,我们可以使用vue里提供的torefs来进行对偶性的解构,其会自动将解构出的变量转换为ref变量,从而获得响应式
script>
const App=Vue.createApp({
setup(){
let myObject=Vue.reactive({
value:0
})
let {value}=Vue.toRefs(myObject)
function click(){
value.value++
console.log(value.value);
}
return{
value,
click
}
},
template:`
<h1>测试数据:{{value}}</h1>
<button @click="click">点击</button>
`
})
App.mount("#Application")
</script>
响应式的计算和监听
以本博客开头的那串ab和sum代码为例,本身和页面元素是没有绑定关系的,变量a和变量b的值仅仅会影响sum的值,对于这种场景,sum更像是一种计算变量,在vue里提供了computed方法来定义计算变量
关于计算变量
有时候定义的变量的值依赖于其他变量的状态。在组件里可以使用computed选项来定义计算属性,vue也提供了一个同名的方法来直接使用其创建计算变量
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="https://unpkg.com/vue@next"></script>
</head>
<body>
<div id="Application"></div>
<script>
const App=Vue.createApp({
setup(){
let a=1
let b=2
let sum=a+b
function click(){
a++
b++
console.log(a);
console.log(b);
}
return {
sum,
click
}
},
template:`
<h1>测试数据:{{sum}}</h1>
<button @click="click">点击</button>
`
})
App.mount("#Application")
</script>
</body>
</html>
单击按钮,页面上渲染的值不会发生改变
let a=Vue.ref(1)
let b=Vue.ref(2)
let sum=Vue.computed(()=>{
return a.value+b.value
})
function click(){
a.value++
b.value++
}
改一下,这样a和b的值发生变化,就会同步改变sum变量的值了,也可以响应式的进行页面元素的更新,与计算属性类似,计算变量也可以支持被赋值
setup(){
let a=Vue.ref(1)
let b=Vue.ref(2)
let sum=Vue.computed({
set(value){
a.value=value
b.value=value
},
get(){
return a.value+b.value
}
})
function click(){
a.value+=1
b.value+=2
if(sum.value>10){
sum.value=0
}
}
return {
sum,
click
}
},
监听响应式变量
vue中的ref,reactive,computed可以来创建拥有响应式特性的变量,有时候,需要当响应式变量发生变化的时候监听变化其行为。在v3里面,watcheffect可以自动实现对其内部用到的响应式变量进行,其原理是在组件初始化的时候对所有依赖进行收集。因此在使用的时候我们无需手动指定要监听的变量
setup(){
let a=Vue.ref(1)
Vue.watchEffect(()=>console.log("a变化了"+a.value))
a.value=2
let b=Vue.ref(2)
let sum=Vue.computed({
set(value){
a.value=value
b.value=value
},
get(){
return a.value+b.value
}
})
function click(){
a.value+=1
b.value+=2
if(sum.value>10){
sum.value=0
}
}
在调用watcheffect方法的时候,其会立即执行传入的函数参数,并追踪其内部的响应式变量,在其变更的时候再次调用此函数参数
watcheffect在setup方法被调用后,其会和当前组件的生命周期绑定在一起,组件卸载的时候会自动停止监听,如果需要手动停止监听,如下
setup(){
let a=Vue.ref(1)
let stop= Vue.watchEffect(()=>console.log("a变化了"+a.value))
stop()
a.value=2
let b=Vue.ref(2)
let sum=Vue.computed({
set(value){
a.value=value
b.value=value
},
get(){
return a.value+b.value
}
})
function click(){
a.value+=1
b.value+=2
if(sum.value>10){
sum.value=0
}
}
watch是一个和watcheffect类似的方法,和watcheffect方法相比,watch能够更加精准的监听指定的响应式数据的变化
<script>
const App=Vue.createApp({
setup(){
let a=Vue.reactive({
data:0
})
let b=Vue.ref(0)
Vue.watch(()=>{
return a.data
},(value,old)=>{
console.log(value,old);
})
a.data=1
Vue.watch(b,(value,old)=>{
console.log(value,old);
})
b.value=3
},
})
App.mount("#Application")
</script>
watch方法比watcheffect方法强大的地方在于能获取到变化前后的值,十分方便于某些和值的对比相关的业务逻辑。watch也可以同时监听多个数据源
const App=Vue.createApp({
setup(){
let a=Vue.reactive({
data:0
})
let b=Vue.ref(0)
Vue.watch([()=>{
return a.data
},b],([valueA,valueB],[oldA,oldB])=>{
console.log(valueA,oldA);
console.log(valueB,oldB);
})
a.data=1
b.value=3
},
})
组合式API的应用
组合式ap能够帮我们更好的梳理复杂组件的逻辑分布,能够从代码层面将分离的相关逻辑点进行聚合,更适合复杂模块组件的开发
关于setup方法
setup是v3新增的方法,属于v3的新特性,也是组合式api的核心方法
setup方法是组合式api功能的入口方法,如何使用组合式api模式进行组件开发,那么逻辑代码都需要编写在setup方法里面。setup方法会在组件创建之前被执行,即对应组件的生命周期方法beforecreate方法调用之前被执行。由于setup方法特殊的执行时机,除了可以访问组件的传参外部属性props外,在其内部我们不可以使用this来引用组件的其他属性,在setup方法最后,我们可以将定义的组件所需要的数据、函数等内容暴露给组件的其他选项(如生命周期、业务方法、计算属性),深入了解一下setup方法
setup方法可以接收两个参数:props和context。props是组件使用的时候设置的外部参数,是响应式的,context是一个js对象,其中可以使用的属性有attrs、slots和emit
<div id="Application">
<com name="组件名"></com>
</div>
<script>
const App=Vue.createApp({})
App.component("com",{
setup(props,context){
console.log(props.name);
console.log(context.attrs);
console.log(context.slots);
console.log(context.emit);
},
props:{
name:String
}
})
App.mount("#Application")
</script>
在setup方法的最后可以返回一个js对象,此对象包装的数据可以在组件的其他选项中使用,也可以直接用于HTML模板里
App.component("com",{
setup(props,context){
let data="setup的数据"
return data
},
props:{
name:String
},
template:`
<div>{{data}}</div>
`
})
如果不在组件中定义template模板,也可以直接使用setup方法来返回一个渲染函数,当组件将要被展示的时候,会使用此渲染函数进行渲染,
App.component("com",{
setup(props,context){
let data="setup的数据"
return ()=>Vue.h('div',[data])
},
props:{
name:String
},
})
setup方法中不要使用this关键字,其中的this和当前组件实例不是同一个对象
在setup方法中定义生命周期行为
setup方法本身也是可以定义组件的生命周期方法,方便将相关的逻辑组合在一起,setup方法常用的生命周期定义方法比组件的生命周期少了beforecreate和created这俩,因为从逻辑和是哪个,setup方法的执行时机和这两个生命周期方法的执行时机基本一致,在setup方法里直接编写逻辑代码就可以
App.component("com",{
setup(props,context){
let data="setup的数据"
Vue.onMounted(() => {
console.log("setup定义的mounted");
})
return ()=>Vue.h('div',[data])
},
props:{
name:String
},
})
如果组件和setup方法都定义了同样的生命周期方法,他们之间不会冲突,会先调用setup里定义的,再调用组件内部定义的
实现支持搜索和筛选的用户列表
场景模拟:有一个用户列表页面,页面的列表支持性别筛选和搜索,可以假想用户数据是通过网络请求到前端页面的,在实际编写代码的时候,可以使用延时函数来模拟这种情况
常规风格的示例工程开发
初始化模板
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="https://unpkg.com/vue@next"></script>
<style>
.container{
margin: 50px;
}
.content{
margin: 20px;
}
</style>
</head>
第一步:设计页面的根组件的数据框架,分析页面的功能需求,主要是有三个的:能够渲染用户列表、能够根据用户性别筛选数据、能够根据输入的关键字进行搜索,因此,我们需要三个响应式的数据:用户列表数据、性别筛选字段和关键词字段。定义组件的data选项:
data() {
return {
sexFilter:-1,
showDatas:[],
searchKey:""
}
},
性别字段的取值可以是-1,0,1,代表全部、男、女
第二步,思考页面需要支持的行为,首先从网络上请求用户数据,将其渲染在页面上(延时函数),要支持性别筛选功能,需要定义一个筛选函数来完成,关键词搜索功能,也需要一个检索函数来完成,定义组件的methods
methods: {
queryAllData(){
this.showDatas=mock
},
FilterData(){
this.searchKey=""
if(this.sexFilter==-1){
this.showDatas=mock
}else{
this.showDatas=mock.filter((data)=>{
return data.sex==this.sexFilter
})
}
},
searchData(){
this.sexFilter=-1
if (this.searchKey.length==0){
this.showDatas=mock
}else{
this.showDatas=mock.filter((data)=>{
return data.name.search(this.searchKey)!=-1
})
}
}
},
mock变量是本地定义的模拟数据,方便测试效果
let mock=[
{
name:"小王",
sex:0
},
{
name:"小红",
sex:1
},
{
name:"小李",
sex:1
},
{
name:"小张",
sex:0
},
]
定义好功能函数,需要进行调用,queryalldata方法可以在组件挂载的时候调用来获取数据
mounted() {
setTimeout(this.queryAllData,3000)
},
页面挂载后,会延时三秒来获得测试的模拟数据,对于性别筛选和关键词检索功能,可以监听对应的属性,当这些属性发生变化时,进行筛选或检索功能,定义组件的watch
watch:{
sexFilter(oldValue,newValue){
this.FilterData()
},
searchKey(oldValue,newValue){
this.searchData()
}
}
逻辑代码编写完成后,我们需要渲染页面所需的HTML框架搭建
<div id="Application">
<div class="container">
<div class="content">
<input type="radio" :value="-1" v-model="sexFilter">全部
<input type="radio" :value="0" v-model="sexFilter">男
<input type="radio" :value="-1" v-model="sexFilter">女
</div>
<div class="content">搜索:<input type="text" v-model="searchKey"></div>
<div class="content">
<table border="1" width="300px">
<tr>
<th>姓名</th>
<th>性别</th>
</tr>
<tr v-for="(data,index) in showDatas">
<td>{{data.name}}</td>
<td>{{data.sex=='0'?'男':'女'}}</td>
</tr>
</table>
</div>
</div>
</div>
效果:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="https://unpkg.com/vue@next"></script>
<style>
.container{
margin: 50px;
}
.content{
margin: 20px;
}
</style>
</head>
<body>
<div id="Application">
<div class="container">
<div class="content">
<input type="radio" :value="-1" v-model="sexFilter">全部
<input type="radio" :value="0" v-model="sexFilter">男
<input type="radio" :value="-1" v-model="sexFilter">女
</div>
<div class="content">搜索:<input type="text" v-model="searchKey"></div>
<div class="content">
<table border="1" width="300px">
<tr>
<th>姓名</th>
<th>性别</th>
</tr>
<tr v-for="(data,index) in showDatas">
<td>{{data.name}}</td>
<td>{{data.sex=='0'?'男':'女'}}</td>
</tr>
</table>
</div>
</div>
</div>
<script>
let mock=[
{
name:"小王",
sex:0
},
{
name:"小红",
sex:1
},
{
name:"小李",
sex:1
},
{
name:"小张",
sex:0
},
]
const App={
data() {
return {
sexFilter:-1,
showDatas:[],
searchKey:""
}
},
methods: {
queryAllData(){
this.showDatas=mock
},
FilterData(){
this.searchKey=""
if(this.sexFilter==-1){
this.showDatas=mock
}else{
this.showDatas=mock.filter((data)=>{
return data.sex==this.sexFilter
})
}
},
searchData(){
this.sexFilter=-1
if (this.searchKey.length==0){
this.showDatas=mock
}else{
this.showDatas=mock.filter((data)=>{
return data.name.search(this.searchKey)!=-1
})
}
}
},
mounted() {
setTimeout(this.queryAllData,3000)
},
watch:{
sexFilter(oldValue,newValue){
this.FilterData()
},
searchKey(oldValue,newValue){
this.searchData()
}
}
}
Vue.createApp(App).mount("#Application")
</script>
</body>
</html>
使用组件式api重构用户列表页面
在上一小节,实现了完整的用户列表页面,深入分析,其实上面的代码的逻辑点是十分分散的,例如用户的性别筛选是一个独立的功能,要实现这个功能,需要在data选项里面定义属性,如何在methods选项里面定义功能方法,最后在watch选项里面监听属性,实现筛选的功能,逻辑点的分离会让代码的可读性非常差,随着迭代项目,页面的功能会越来越复杂,对于后续此组件的维护者来说,扩展也会变得更加困难
v3里面提供的组合式API的开发风格可以很好地解决这个问题,我们可以将逻辑都梳理在setup方法里,相同的逻辑点聚合性更强,更易阅读和扩展
使用组合式api重写的代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="https://unpkg.com/vue@next"></script>
<style>
.container{
margin: 50px;
}
.content{
margin: 20px;
}
</style>
</head>
<body>
<div id="Application"></div>
<script>
let mock=[
{
name:"小王",
sex:0
},
{
name:"小红",
sex:1
},
{
name:"小李",
sex:1
},
{
name:"小张",
sex:0
},
]
const App=Vue.createApp({
setup(){
const showDatas=Vue.ref({})
const queryAllData=()=>{
setTimeout(()=>{
showDatas.value=mock
},3000);
}
Vue.onMounted(queryAllData)
let sexFilter=Vue.ref(-1)
let searchKey=Vue.ref("")
let FilterData=()=>{
searchKey.value=""
if(sexFilter.value==-1){
showDatas.value=mock
}else{
showDatas.value=mock.filter((data)=>{
return data.sex==sexFilter.value
})
}
}
searchData=()=>{
sexFilter.value=-1
if (searchKey.value.length==0){
showDatas.value=mock
}else{
showDatas.value=mock.filter((data)=>{
return data.name.search(searchKey.value)!=-1
})
}
}
Vue.watch(sexFilter,FilterData)
Vue.watch(searchKey,searchData)
return{
showDatas,
searchKey,
sexFilter
}
},
template:`
<div class="container">
<div class="content">
<input type="radio" :value="-1" v-model="sexFilter">全部
<input type="radio" :value="0" v-model="sexFilter">男
<input type="radio" :value="-1" v-model="sexFilter">女
</div>
<div class="content">搜索:<input type="text" v-model="searchKey"></div>
<div class="content">
<table border="1" width="300px">
<tr>
<th>姓名</th>
<th>性别</th>
</tr>
<tr v-for="(data,index) in showDatas">
<td>{{data.name}}</td>
<td>{{data.sex=='0' ? '男':'女'}}</td>
</tr>
</table>
</div>
</div>
`
})
App.mount("#Application")
</script>
</body>
</html>
在使用组合式api编写代码的时候,要注意,需要使用的响应式的数据,有使用ref或者reactive方法进行包装