Vue3的Options-API

续上文:

vue3学习之旅–邂逅vue3-了解认识Vue3

vue3学习之旅–邂逅vue3-了解认识Vue3(二)

Vue3学习之旅–爱上Vue3–Vue3基础语法(一)–以及vscode基本使用和快速生成代码片段

Vue3学习之旅–爱上Vue3–Vue3基础语法(二)–模板语法–条件渲染–列表渲染–为什么Vue的v-for需要绑定key属性

复杂data的处理方式

我们知道,在模板中可以直接通过插值语法显示一些data中的数据。但是在某些情况,我们可能需要对数据进行一些转化后再显示,或者需要将多个数据结合起来进行显示;

  1. 比如我们需要对多个data数据进行运算、三元运算符来决定结果、数据进行某种转化后显示
  2. 在模板中使用表达式,可以非常方便的实现,但是设计它们的初衷是用于简单的运算
  3. 在模板中放入太多的逻辑会让模板过重和难以维护
  4. 并且如果多个地方都使用到,那么会有大量重复的代码

我们有没有什么方法可以将逻辑抽离出去呢?

可以,其中一种方式就是将逻辑抽取到一个method中,放到methods的options中,但是,这种做法有一个直观的弊端,就是所有的data使用过程都会变成了一个方法的调用;另外一种方式就是使用计算属性computed

认识计算属性computed

什么是计算属性呢?

官方并没有给出直接的概念解释;而是说:对于任何包含响应式数据的复杂逻辑,你都应该使用计算属性

计算属性将被混入到组件实例中。所有 getter 和 setter 的 this 上下文自动地绑定为组件实例

计算属性的用法

选项:computed

类型:{ [key: string]: Function | { get: Function, set: Function } }

那接下来我们通过案例来理解一下这个计算属性

案例实现思路

我们来看三个案例:

案例一:我们有两个变量:firstName和lastName,希望它们拼接之后在界面上显示;

案例二:我们有一个分数:score,当score大于60的时候,在界面上显示及格;当score小于60的时候,在界面上显示不及格。

案例三:我们有一个变量message,记录一段文字:比如Hello World。某些情况下我们是直接显示这段文字;某些情况下我们需要对这段文字进行反转;

我们可以有三种实现思路

  1. 思路一:在模板语法中直接使用表达式
  2. 思路二:使用method对逻辑进行抽取;
  3. 思路三:使用计算属性computed;
实现思路一:模板语法

思路一的实现:模板语法

<template id="my-app">
    <!-- 使用插值语法完成字符串的拼接等案例 -->
    <h2>{{firstName + lastName}}</h2>
    <h2>{{score >= 60 ? '及格' : '不及格'}}</h2>
    <h3>{{message.split('').reverse().join('')}}</h3>
  </template>

缺点一:模板中存在大量的复杂逻辑,不便于维护(模板中表达式的初衷是用于简单的计算);

缺点二:当有多次一样的逻辑时,存在重复的代码;

缺点三:多次使用的时候,很多运算也需要多次执行,没有缓存;

实现思路二:method实现

思路二的实现:method实现

<div id="app"></div>
  <template id="my-app">
    <h2>{{getFullName()}}</h2>
    <h2>{{getResult()}}</h2>
    <h2>{{getReverseMessage()}}</h2>
  </template>
  <script src="../vue3/vue3.js"></script>
  <script>
    Vue.createApp({
      template: '#my-app',
      data() {
        return {
          firstName: '毛',
          lastName: '俊',
          score: 80,
          message: 'hello Vue3!'
        }
      },
      methods: {
        getFullName(){
          return this.firstName + this.lastName
        },
        getResult(){
          return this.score>=60? '及格' : '不及格'
        },
        getReverseMessage(){
          return this.message.split('').reverse().join('')
        }
      },
    }).mount('#app')
  </script>

缺点一:我们事实上先显示的是一个结果,但是都变成了一种方法的调用;

缺点二:多次使用方法的时候,没有缓存,也需要多次计算

思路三的实现:computed实现

思路三的实现:computed实现

<div id="app"></div>
  <template id="my-app">
    <h2>{{fullName}}</h2>
    <h2>{{result}}</h2>
    <h2>{{reverseMessage}}</h2>
  </template>
  <script src="../vue3/vue3.js"></script>
  <script>
    Vue.createApp({
      template: '#my-app',
      data() {
        return {
          firstName: '毛',
          lastName: '俊',
          score: 80,
          message: 'hello Vue3!'
        }
      },
      // 计算属性  计算属性在多次使用时,如果数据没有发生改变,是会重复使用上次的数据的
      computed: {
        // 定义一个计算属性叫做 fullName
        // 虽然计算属性看起来和函数很相似,但是本质上计算属性其实就是一个属性
        fullName() {
          return this.firstName + this.lastName
        },
        result() {
          return this.score >= 60 ? '及格' : '不及格'
        },
        reverseMessage() {
          return this.message.split('').reverse().join('')
        }
      }
    }).mount('#app')
  </script>

注意:计算属性看起来像是一个函数,但是我们在使用的时候不需要加(),这个后面讲setter和getter时会讲到;我们会发现无论是直观上,还是效果上计算属性都是更好的选择;并且计算属性是有缓存的

计算属性 vs methods

在上面的实现思路中,我们会发现计算属性和methods的实现看起来是差别是不大的,而且我们多次提到计算属 性有缓存的

接下来我们来看一下同一个计算多次使用,计算属性和methods的差异:

<div id="app"></div>
  <template id="my-app">
    <button @click="firstName = '毛毛'">修改firstName</button>
    <!-- 表面上 计算属性和方法所达到的效果的确是一样的 -->
    <!-- 但是当我们多次调用计算属性的时候,发现计算属性只被调用一次,因为默认情况下是有缓存的 -->
    <!-- 计算属性会随着数据(也就是data里面我们计算属性用到的那部分数据)的改变,而发生改变 -->
    <h2>{{fullName}}</h2>
    <h2>{{fullName}}</h2>
    <h2>{{fullName}}</h2>
    <h2>{{getFullName()}}</h2>
    <h2>{{getFullName()}}</h2>
    <h2>{{getFullName()}}</h2>
  </template>
  <script src="../vue3/vue3.js"></script>
  <script>
    Vue.createApp({
      template: '#my-app',
      data() {
        return {
          firstName: '毛',
          lastName: '俊'
        }
      },
      // 计算属性  计算属性在多次使用时,如果数据没有发生改变,是会重复使用上次的数据的
      computed: {
        // 定义一个计算属性叫做 fullName
        // 虽然计算属性看起来和函数很相似,但是本质上计算属性其实就是一个属性
        fullName() {
          console.log(1);
          return this.firstName + this.lastName
        }
      },
      methods: {
        getFullName() {
          console.log(2);
          return this.firstName + this.lastName
        }
      },
    }).mount('#app')
  </script>

image-20210722153008321

image-20210722153036987

计算属性的缓存

这是什么原因呢?

  1. 这是因为计算属性会基于它们的依赖关系进行缓存;

  2. 在数据不发生变化时,计算属性是不需要重新计算的;

  3. 但是如果依赖的数据发生变化,在使用时,计算属性依然会重新进行计算;

image-20210722153259949

计算属性的setter和getter

计算属性在大多数情况下,只需要一个getter方法即可,所以我们会将计算属性直接写成一个函数(语法糖)。

但是,如果我们确实想设置计算属性的值呢?

这个时候我们也可以给计算属性设置一个setter的方法;

computed: {
        // 直接书写一个计算属性,实际上是简写,调用的是计算属性的getter方法
        // fullName() {
        //   return this.firstName + this.lastName
        // }
        // 计算属性的完整写法
        fullName:{
          get:function(){
            return this.firstName + this.lastName
          },
          set:function(newVal){
            console.log(newVal);
            this.firstName = newVal.split('')[0];
            this.lastName = newVal.split('')[1];
          }
        }
      }
源码如何对setter和getter处理呢?

你可能觉得很奇怪,Vue内部是如何对我们传入的是一个getter,还是说是一个包含setter和getter的对象进行处 理的呢?

事实上非常的简单,Vue源码内部只是做了一个逻辑判断而已;

image-20210722153542978

认识侦听器watch

什么是侦听器呢?

开发中我们在data返回的对象中定义了数据,这个数据通过插值语法等方式绑定到template中;

当数据变化时,template会自动进行更新来显示最新的数据;

但是在某些情况下,我们希望在代码逻辑中监听某个数据的变化,这个时候就需要用侦听器watch来完成了;

侦听器的用法如下:

选项:watch

类型:{ [key: string]: string | Function | Object | Array}

侦听器案例

举个栗子(例子):

比如现在我们希望用户在input中输入一个问题;

每当用户输入了最新的内容,我们就获取到最新的内容,并且使用该问题去服务器查询答案;

那么,我们就需要实时的去获取最新的数据变化;

image-20210722153938057

image-20210722153949432

image-20210722154322714

<div id="app"></div>
  <template id="my-app">
    <label for="question">
      请输入问题:
      <input id="question" type="text" v-model="question">
    </label>

  </template>
  <script src="../vue3/vue3.js"></script>
  <script>
    Vue.createApp({
      template: '#my-app',
      data() {
        return {
          question: 'hello Vue3!'
        }
      },
      // 侦听器  侦听某个属性(数据)的变化,并进行一些逻辑的处理
      // 在实际工作时:我们侦听器一般不会监听计算属性,一般来说都是侦听元数据(data)
      watch: {
        // 第一个参数的变化后的新值
        // 第二个参数是变化前的旧值
        question(newVal, oldVal) {
          console.log(newVal,oldVal);
          this.getAnswer(newVal);
        }
      },
      methods: {
        getAnswer(question) {
          console.log(`${question}问题答案是:哈哈哈哈哈`)
        }
      },
    }).mount('#app')
  </script>
侦听器watch的配置选项

我们先来看一个例子:

当我们点击按钮的时候会修改info.name的值;

这个时候我们使用watch来侦听info,可以侦听到吗?答案是不可以。

这是因为默认情况下,watch只是在侦听info的引用变化,对于内部属性的变化是不会做出响应的:

这个时候我们可以使用一个选项deep进行更深层的侦听;

注意前面我们说过watch里面侦听的属性对应的也可以是一个Object;

还有另外一个属性,是希望一开始的就会立即执行一次:

这个时候我们使用immediate选项;

这个时候无论后面数据是否有变化,侦听的函数都会有限执行一次;

image-20210722154612049

<div id="app"></div>
  <template id="my-app">
    <button @click="info.gender = ''">增加info的gender属性</button>
    <button @click="info.name = '哈哈哈'">改变info的name属性</button>
    <h2 v-for="item of info">{{item}}</h2>

  </template>
  <script src="../vue3/vue3.js"></script>
  <script>
    Vue.createApp({
      template: '#my-app',
      data() {
        return {
          info: {
            name: '毛毛',
            age: 22
          }
        }
      },
      watch: {
        // 直接监听这个info对象,发现属性发生改变了也不会触发
        // 特点: 默认情况下只能监听整个对象是否发生改变(也就是是不是换成一个新对象了)
        // 1. 如果直接将对象info直接重新赋值为一个新的对象时,可以直接侦听到
        // 2. 无法直接侦听到对象内部属性的改变
        // info(newVal, oldVal) {
        //   console.log(newVal, oldVal);
        // }
        // 深度侦听:
        // info: {
        //   // 开启深度侦听,可以侦听到对象内部属性的变化了
        //   deep: true, // 即使对象的属性还是一个对象,仍然还是可以侦听到
        //   handler(newVal, oldVal) {
        //     console.log(newVal, oldVal)
        //   },
        //   // 侦听器的立即执行
        //   immediate: true // 在页面刚加载的时候,数据很明显是没有发生变化的(旧的值相当于不存在,也就是undefined),但是我们仍然想执行一遍侦听器的代码逻辑,就可以让 immediate属性为true
        // }

        // 字符串方法名 侦听器的方法也可以放在methods里面,然后通过字符串的方式赋值,
        // info: "changeInfo" // 实际上用的不多

        // 也可以使用回调数组 会被逐一调用
        info: [
          "changeInfo", // 字符串方法名
          function handler2(newVal, oldVal) {
            console.log(newVal, oldVal, 'handle2');
          },
          {
            handler: function handle3(newVal, oldVal) {
              console.log(newVal, oldVal, 'handle3');
            }
          }
        ]
      },
      methods: {
        changeInfo() {

        }
      },
    }).mount('#app')
  </script>
侦听器watch的其他方式
  1. 字符串方法名

    info: "changeInfo" // 实际上用的不多
    
  2. 回调数组的方式

    // 也可以使用回调数组 会被逐一调用
            info: [
              "changeInfo", // 字符串方法名
              function handler2(newVal, oldVal) {
                console.log(newVal, oldVal, 'handle2');
              },
              {
                handler: function handle3(newVal, oldVal) {
                  console.log(newVal, oldVal, 'handle3');
                }
              }
            ]
    
  3. 另外一个是Vue3文档中没有提到的,但是Vue2文档中有提到的是侦听对象的属性:

    image-20210722154814869

    watch: {
            // 直接侦听对象内的某个属性
            // 发现只有对象的这个顺序发生改变时,才会被侦听到
            // 当然,如果整个对象都直接被替换了,也一样是会被侦听的
            "info.name": function (newVal, oldVal) {
              console.log(newVal, oldVal);
            }
          }
    
    1. 还有另外一种方式就是使用 $watch 的API:

      我们可以在created的生命周期(后续会讲到)中,使用 this.$watchs 来侦听;

      1. 第一个参数是要侦听的源
      2. 第二个参数是侦听的回调函数callback;
      3. 第三个参数是额外的其他选项,比如deep、immediate;
      // 生命周期函数 created 在组件被创建的时候执行(后面会讲)
            created() {
              // 使用当前组件的$watch方法来创建侦听器
              // 参数一:侦听的对象 
              // 参数二:回调函数
              // 参数三:侦听器的配置选项(对象形式) 
              // $watch() 的返回值 unwatch 是一个函数
              // 调用这个返回值函数,可以取消侦听器,而直接定义在watch里面的侦听器一般是没办法取消的
              const unwatch = this.$watch('info', (newVal, oldVal) => {
                console.log(newVal, oldVal);
              }, {
                deep: true,
                immediate: true
              });
              // unwatch() // 调用后 侦听器失效
            }
      

综合案例

现在我们来做一个相对综合一点的练习:书籍购物车

image-20210722155110498

案例说明:

  1. 在界面上以表格的形式,显示一些书籍的数据;
  2. 在底部显示书籍的总价格;
  3. 点击+或者-可以增加或减少书籍数量(如果为1,那么不能继续-);
  4. 点击移除按钮,可以将书籍移除(当所有的书籍移除完毕时,显示:购物车为空~)
<!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>
  <style>
    table {
      border: 1px solid #e9e9e9;
      border-collapse: collapse;
      border-spacing: 0;
    }

    th,
    td {
      padding: 8px 16px;
      border: 1px solid #e9e9e9;
      text-align: left;
    }

    th {
      background-color: #f7f7f7;
      color: #5c6b77;
      font-weight: 600;
    }

    .counter {
      margin: 0 5px;
    }
  </style>
</head>

<body>
  <div id="app"></div>
  <template id="my-app">
    <template v-if="books.length>0">
      <table>
        <thead>
          <th>序号</th>
          <th>书籍名称</th>
          <th>出版日期</th>
          <th>价格</th>
          <th>购买数量</th>
          <th>操作</th>
        </thead>
        <tbody>
          <tr v-for="(book, index) in books">
            <td>{{index + 1}}</td>
            <td>{{book.name}}</td>
            <td>{{book.date}}</td>
            <td>{{formatPrice(book.price)}}</td>
            <td>
              <button :disabled="book.count <= 1" @click="decrement(index)">-</button>
              <span class="counter">{{book.count}}</span>
              <button @click="increment(index)">+</button>
            </td>
            <td>
              <button @click="removeBook(index)">移除</button>
            </td>
          </tr>
        </tbody>
      </table>
      <h2>总价格: {{formatPrice(totalPrice)}}</h2>
    </template>
    <template v-else>
      <h2>购物车为空!</h2>
    </template>
  </template>
  <script src="../vue3/vue3.js"></script>
  <script>
    Vue.createApp({
      template: '#my-app',
      data() {
        return {
          books: [
            {
              id: 1,
              name: '《算法导论》',
              date: '2006-9',
              price: 85.00,
              count: 1
            },
            {
              id: 2,
              name: '《UNIX编程艺术》',
              date: '2006-2',
              price: 59.00,
              count: 1
            },
            {
              id: 3,
              name: '《编程珠玑》',
              date: '2008-10',
              price: 39.00,
              count: 1
            },
            {
              id: 4,
              name: '《代码大全》',
              date: '2006-3',
              price: 128.00,
              count: 1
            },
          ]
        }
      },
      computed: {
        // vue2 filter/map/reduce
        totalPrice() {
          let finalPrice = 0;
          for (let book of this.books) {
            finalPrice += book.count * book.price;
          }
          return finalPrice;
        },
        // Vue3不支持过滤器了, 推荐两种做法: 使用计算属性/使用全局的方法
        filterBooks() {
          const newBook = this.books.map(item => {
            // 生成新的对象
            const newItem = Object.assign({}, item);
            newItem.price = "¥" + item.price;
            return newItem;
          })
          return newBook
        }
      },
      methods: {
        increment(index) {
          // 通过索引值获取到对象
          this.books[index].count++
        },
        decrement(index) {
          this.books[index].count--
        },
        removeBook(index) {
          this.books.splice(index, 1);
        },
        formatPrice(price) {
          return "¥" + price;
        }
      }
    }).mount('#app')
  </script>
</body>

</html>
Logo

CSDN联合极客时间,共同打造面向开发者的精品内容学习社区,助力成长!

更多推荐