Vue基础-Options API(computed和watch)-Vue基础综合练习: 书籍购物车案例

导读:本篇文章讲解 Vue基础-Options API(computed和watch)-Vue基础综合练习: 书籍购物车案例,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com

计算属性computed

1.认识计算属性

我们知道,在模板中可以直接通过插值语法显示一些data中的数据

但是在某些情况,我们可能需要对数据进行一些转化后再显示,或者需要将多个数据结合起来进行显示

  • 比如我们需要对多个data数据进行运算、三元运算符来决定结果、数据进行某种转化后显示

  • 模板中使用表达式,可以非常方便的实现,但是设计它们的初衷是用于简单的运算

  • 模板中放入太多的逻辑会让模板过重难以维护

  • 并且如果多个地方都使用到,那么会有大量重复的代码;

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

  • 可以,其中一种方式就是将逻辑抽取到一个method中,放到methods的options中;

  • 但是,这种做法有一个直观的弊端,就是所有的data使用过程都会变成了一个方法的调用;

  • 另外一种方式就是使用计算属性computed

那么什么是计算属性呢?

  • 官方并没有给出直接的概念解释;

  • 而是说:对于任何包含响应式数据的复杂逻辑,你都应该使用计算属性

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

计算属性的用法

  • 选项:computed

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

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


2.案例实现

我们来看三个案例

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

案例二:我们有一个分数:score

  • 当score大于60的时候,在界面上显示及格;

  • score小于60的时候,在界面上显示不及格;

案例三:我们有一个变量message,记录一段文字:比如Hello World

  • 某些情况下我们是直接显示这段文字;

  • 某些情况下我们需要对这段文字进行反转;

我们可以有三种实现思路:

思路一:在模板语法中直接使用表达式;

思路二:使用method对逻辑进行抽取;

思路三:使用计算属性computed

实现思路一:模板语法

思路一的实现:模板语法

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

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

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

<div id="app">
  <!-- 1.案例一: 名字的拼接 -->
  <h2>{{ firstNmae}} {{ lastName }}</h2>

  <!-- 2.案例二: 根据分数显式文本 -->
  <h2>{{ score >= 60? "及格": "不及格" }}</h2>

  <!-- 3.案例三: 对文本的单词反转 -->
  <h2>{{ message.split(" ").reverse().join(" ") }}</h2>
</div>

<!-- 从本地引入Vue -->
<script src="../js/vue.js"></script>
<script>
  const app = Vue.createApp({
    data() {
      return {
        // 案例一的数据
        firstNmae: "chen",
        lastName: "yq",

        // 案例二的数据
        score: 50,

        // 案例三的数据
        message: "my name is chenyq",
      };
    },
  });

  app.mount("#app");
</script>

实现思路二:method

思路二的实现:method实现

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

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

<div id="app">
  <!-- 1.案例一: 名字的拼接 -->
  <h2>{{ getFullName() }}</h2>

  <!-- 2.案例二: 根据分数显式文本 -->
  <h2>{{ getScoreLevel() }}</h2>

  <!-- 3.案例三: 对文本的单词反转 -->
  <h2>{{ reverseMessage() }}</h2>
</div>

<!-- 从本地引入Vue -->
<script src="../js/vue.js"></script>
<script>
  const app = Vue.createApp({
    data() {
      return {
        // 案例一的数据
        firstNmae: "chen",
        lastName: "yq",

        // 案例二的数据
        score: 50,

        // 案例三的数据
        message: "my name is chenyq",
      };
    },
    methods: {
      // 案例一的方法
      getFullName() {
        return this.firstNmae + " " + this.lastName;
      },

      // 案例二的方法
      getScoreLevel() {
        return this.score >= 60 ? "及格" : "不及格";
      },

      // 案例三的方法
      reverseMessage() {
        return this.message.split(" ").reverse().join(" ");
      },
    },
  });

  app.mount("#app");
</script>

实现思路三:computed

思路三的实现:computed实现

  • 注意:计算属性看起来像是一个函数,但是我们在使用的时候不需要加(),这个后面讲setter和getter时会讲到;

  • 我们会发现无论是直观上,还是效果上计算属性都是更好的选择

  • 并且计算属性是有缓存的

<div id="app">
  <!-- 1.案例一: 名字的拼接 -->
  <h2>{{ fullName }}</h2>

  <!-- 2.案例二: 根据分数显式文本 -->
  <h2>{{ scoreLevel }}</h2>

  <!-- 3.案例三: 对文本的单词反转 -->
  <h2>{{ reverseMessage }}</h2>
</div>

<!-- 从本地引入Vue -->
<script src="../js/vue.js"></script>
<script>
  const app = Vue.createApp({
    data() {
      return {
        // 案例一的数据
        firstNmae: "chen",
        lastName: "yq",

        // 案例二的数据
        score: 50,

        // 案例三的数据
        message: "my name is chenyq",
      };
    },
    computed: {
      // 案例一的计算属性
      fullName() {
        return this.firstNmae + " " + this.lastName;
      },

      // 案例二的计算属性
      scoreLevel() {
        return this.score >= 60 ? "及格" : "不及格";
      },

      // 案例三的计算属性
      reverseMessage() {
        return this.message.split(" ").reverse().join(" ");
      },
    },
  });

  app.mount("#app");
</script>

3.计算属性 VS methods

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

  • 接下来我们来看一下同一个计算多次使用,计算属性和methods的差异:
const app = Vue.createApp({
  data() {
    return {
      firstNmae: "chen",
      lastName: "yq",
    };
  },
  // 1.定义methods方法
  methods: {
    getFullName() {
      console.log("调用了methods的getFullName");
      return this.firstNmae + " " + this.lastName;
    },
  },
  
  // 2.定义计算属性
  computed: {
    fullName() {
      console.log("调用了computed的fullName");
      return this.firstNmae + " " + this.lastName;
    },
  },
});
<!-- 1.使用methods -->
<h2>{{ getFullName() }}</h2>
<h2>{{ getFullName() }}</h2>
<h2>{{ getFullName() }}</h2>
<h2>{{ getFullName() }}</h2>

<!-- 2.使用computed -->
<h2>{{ fullName }}</h2>
<h2>{{ fullName }}</h2>
<h2>{{ fullName }}</h2>
<h2>{{ fullName }}</h2>

打印的结果如下 ( 我们会发现methods调用了四次, 而computed只会调用一次 ):

在这里插入图片描述

这是什么原因呢?

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

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

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

例如我们对上面代码在添加一个按钮, 要求点击按钮, 内容发生改变

<!-- 1.使用methods -->
<h2>{{ getFullName() }}</h2>
<h2>{{ getFullName() }}</h2>
<h2>{{ getFullName() }}</h2>
<h2>{{ getFullName() }}</h2>

<!-- 2.使用computed -->
<h2>{{ fullName }}</h2>
<h2>{{ fullName }}</h2>
<h2>{{ fullName }}</h2>
<h2>{{ fullName }}</h2>

<!-- 3.添加按钮点击改变信息 -->
<button @click="changeMessage">改变信息</button>

那么我们来看看点击按钮前后的打印效果:

  • 点击按钮改变前:

在这里插入图片描述

  • 点击按钮改变信息后:

在这里插入图片描述

  • 改变信息后计算属性会重新执行一次, 而methods又重新执行了四次
  • 由此可见, computed的性能是更高的

4.计算属性的set和get

其实上面的代码中, 我们使用的都是计算属性的简写方式

  • 计算属性有完整的写法, 只不过开发中很少会这样写
  • 下面给大家介绍一下计算属性的完整写法

完整写法中, 我们设置一个计算属性, 这个计算属性其实是一个对象

  • 对象中有两个方法, 一个是set, 一个是get

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

  • 计算属性的完整写法
computed: {
  fullName: {
    get() {
      return this.firstName + " " + this.lastName;
    },
  },
}
  • 当我们只需要getter方法的时候, 我们可以直接写出一个函数, 也就是我们之前的写法
computed: {
  fullName() {
    return this.firstName + " " + this.lastName;
  },
}

但是,如果我们确实想设置或修改计算属性的值(了解), 基本不会使用set

  • 这个时候我们也可以给计算属性设置一个setter的方法
<div id="app">
  <h2>{{ fullName }}</h2>
  <button @click="setFullName">改变fullName</button>
</div>

<!-- 从本地引入Vue -->
<script src="../js/vue.js"></script>
<script>
  const app = Vue.createApp({
    data() {
      return {
        firstName: "chen",
        lastName: "yq",
      };
    },
    computed: {
      fullName: {
        get() {
          return this.firstName + " " + this.lastName;
        },
        set(value) {
          const names = value.split(" ");
          this.firstName = names[0];
          this.lastName = names[1];
        },
      },
    },
    methods: {
      setFullName() {
        this.fullName = "hu tutu";
      },
    },
  });

  app.mount("#app");
</script>

监听器watch

1.认识监听器watch

什么是侦听器呢?

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

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

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

侦听器的用法如下

  • 选项:watch

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

演示代码, 例如监听message发生了改变

// watch监听器
watch: {
  // 监听message的改变
  message() {
    console.log("message发生了改变");
  },
}
  • 如果我们想要拿到message改变之前的信息呢?
  • 其实默认是有两个参数的, 一个oldValue改变之前的数据, 一个是newValue改变之后的数据
watch: {
  // 默认有两个参数
  message(newValue, oldValue) {
    console.log("message改变前是:", oldValue);
    console.log("message改变后是:", newValue);
  },
}
  • 如果是对象类型, 参数获取到的是Proxy代理对象
// 监听info对象
info(newValue, oldValue) {
  // 如果是对象类型, 那么拿到的是代理对象
  console.log("info数据发生了变化:", newValue, oldValue);
  // 代理对象同样可以使用对象的属性
  console.log(newValue.name, oldValue.name);
},
  • 我们也可以获取Proxy代理对象的原始对象
// 监听info对象
info(newValue, oldValue) {
  // 通过toRaw方法获取原始对象
  console.log(Vue.toRaw(newValue), Vue.toRaw(oldValue));
},

2.watch的配置选项

我们先来看一个例子

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

  • 这个时候我们使用watch来侦听info,可以侦听到info.name的变化吗?答案是不可以

<div id="app">
  <h2>{{ info.name }}</h2>
  <button @click="changeInfo">按钮</button>
</div>

<!-- 从本地引入Vue -->
<script src="../js/vue.js"></script>
<script>
  const app = Vue.createApp({
    data() {
      return {
        info: { name: "chenyq", age: 18 },
      };
    },
    methods: {
      changeInfo() {
        this.info.name = "kaisa";
      },
    },
    watch: {
      // 监听info
      info(newValue, oldValue) {
        // info.name发生改变, 监听不到
       console.log("info内部数据发生了变化");
      },
    },
  });

  app.mount("#app");
</script>
  • 我们会发现上面案例中, 页面内容有发生改变, 而监听info对象, 无法监听到info.name的变化

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

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

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

watch: {
  // 进行更深层次的监听
  info: {
    // 回调函数放在handler中
    handler(newValue, oldValue) {
      console.log("info内部数据发生了变化");
    },
    // 添加deep选项, 开启深度监听
    deep: true,
  },
},

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

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

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

watch: {
  // 进行更深层次的监听
  info: {
    // 回调函数放在handler中
    handler(newValue, oldValue) {
      console.log("info内部数据发生了变化");
    },
    // 添加deep选项, 开启深度监听
    deep: true,
    // 添加immediate选项, 启动默认监听
    immediate: true,
  },
},

3.watch的其他方式(了解)

另外一个是Vue3文档中没有提到的,但是Vue2文档中有提到的是侦听对象的属性

  • 例如刚刚监听info.name属性, 可以有如下写法:
watch: {
	"info.name": function (newValue, oldValue) {
		console.log("info.name发生了改变", newValue, oldValue);
	},
},

还有另外一种方式就是使用 $watch 的API:我们可以在created的生命周期(暂时了解, 后续会讲到)中,使用 this.$watchs 来侦听

  • 第一个参数是要侦听的源;
  • 第二个参数是侦听的回调函数callback;
  • 第三个参数是额外的其他选项,比如deep、immediate;
created() {
  this.$watch("info", (newValue, oldValue) => {
    console.log("info内部数据发生了变化");
  }, { deep: true,  immediate: true})
}

综合案例

我们已经学习了很多Vue的语法, 现在我们来做一个相对综合一点的练习:书籍购物车

在这里插入图片描述

案例需求

  • 在界面上以表格的形式,显示一些书籍的数据;
  • 点击+或者-可以增加或减少书籍数量(如果为1,那么不能继续-);
  • 点击移除按钮,可以将书籍移除(当所有的书籍移除完毕时,显示:您当前没有选择任何商品~~~);
  • 在底部显示书籍的总价格;
  • 点击某一行时, 某一行高亮

思路分析:

  • 购买数量不能为负数, 等于零的的时候将按钮禁用: :disabled="item.count === 0"
  • 购买数量的增加和减少, 在v-for里面需要确定点击的是哪一个书籍的增加/减少, 需要在点击事件里面去传递一个index作为参数, 用于确定增加目标
  • 移除同理: 需要确定具体的移除目标, 点击事件里传入index, 使用数组的方法splice(index, 1)进行移除
  • 总价: 使用计算属性计算总价格, 遍历数组拿到每一个对象, 让数量*价格, 最后然后总价格
  • 购物车为空时, 显式一行字, 用v-if 和 v-else, 判断books.lenght是否等于0
  • 点击某一处高亮, 把样式写在一个类中(例如active), 动态添加类 :class = "{active: index === currentIndex}"

示例源代码:

  • css源代码
<style>
  * {
    margin: 0;
    padding: 0;
  }

  #app {
    width: 657px;
    margin: 0 auto;
  }

  table {
    border-collapse: collapse;
  }

  thead {
    background-color: #eee;
  }

  th,
  td {
    border: 1px solid #000;
    padding: 10px 20px;
  }

  thead th {
    color: #536270;
    font-weight: 700;
  }

  .count {
    position: relative;
    text-align: center;
  }

  .count button {
    position: absolute;
    padding: 0 5px;
    height: 25px;
  }

  .count .sub {
    left: 20px;
  }

  .count .add {
    right: 20px;
  }

  tbody .remove {
    padding: 0 5px;
  }

  .active {
    background-color: skyblue;
  }
</style>
  • html和js代码
<body>
  <div id="app">
    <template v-if="books.length !== 0">
      <table>
        <thead>
          <tr>
              <th></th>
              <th>书籍名称</th>
              <th>出版日期</th>
              <th>价格</th>
              <th>购买数量</th>
              <th>操作</th>
            </tr>
        </thead>
        <tbody>
          <tr 
            v-for="(item, index) in books" 
            @click="changeColor(index)" 
            :class="{ active: index === currentIndex }">
            <td>{{ index + 1 }}</td>
            <td>{{ item.name }}</td>
            <td>{{ item.date }}</td>
            <td>{{ "¥" + item.price }}</td>
            <td class="count">
              <button 
								class="sub"
                @click="sub(index)"
                :disabled="item.count === 0">-</button>
              {{ item.count }}
              <button class="add" @click="add(index)">+</button>
            </td>
            <td>
              <button class="remove" @click="remove(index)">移除</button>
            </td>
          </tr>
        </tbody>
      </table>
      <h2>总价格: {{ totalPrice }}</h2>
    </template>
    <h2 v-else>您当前没有选择任何商品~~~</h2>
  </div>

  <!-- 从本地引入Vue -->
  <script src="../js/vue.js"></script>
  <script>
    const app = Vue.createApp({
      data() {
        return {
          books: [
            { name: "《算法导论》", date: "2006-9", price: "85", count: "1" },
            { name: "《数据结构》", date: "2006-2", price: "59", count: "1" },
            {
              name: "《代码大全》",
              date: "2008-10",
              price: "39",
              count: "1",
            },
            {
              name: "《你不知道的JavaScript》",
              date: "2006-3",
              price: "128",
              count: "1",
            },
          ],
          currentIndex: null
        };
      },
      computed: {
        totalPrice() {
          let price = 0;
          for (let item of this.books) {
            price += item.price * item.count;
          }
          return price;
        }
      },
      methods: {
        sub(index) {
          this.books[index].count--
        },
        add(index) {
          this.books[index].count++
        },
        remove(index) {
          this.books.splice(index, 1)
        },
        changeColor(index) {
          this.currentIndex = index
        }
      }
    });

    app.mount("#app");
  </script>
</body>

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/120090.html

(0)
seven_的头像seven_bm

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!