《循序渐进Vue.js前端开发实战》电子书学习-第7章-vue响应式编程

响应式是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方法进行包装

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值