一、缘起
事情的起因是这样的,有位朋友(无中生友)遇到了如下需求:
上面是一个商品列表,每个商品对应一个价格、优惠、数量,并且数量可以动态改变,最后动态计算出一个总价。当然,这只是一个简单地乘法计算,往往在实际项目开发中,遇到的需求要复杂的多,计算也更复杂。
大部分人第一时间想到是利用函数传参返回计算结果,显示在页面上。实现方式如下:
function getSumMoney(row) {
return row.price * row.num * row.discount;
}
结果如下:
当改变数量时,总价也跟着变化,看似没有任何问题,达到了我们的预期结果。其实不然,我们看下面的gif演示就会发现问题。
观察上面的gif动图,细心地你已经发现问题了。观察浏览器的打印日志发现,每次改变一个商品的数量时,其它商品的数量虽然没有变化,但是也会在计算一次。还有,当浏览器视口宽度高度变化时,我们并没有改变商品的数量,同样的也会每次触发计算,这个方法会被不断反复的调用。
这是为什么呢?
这是因为当监听到表格的数据data变化时,会重新渲染列表;当浏览器视口宽度高度变化时,会引发重排,列表也会重新渲染。
当计算量大且复杂时,这种情况是相当耗性能的,并且体验也不好。我们期望的结果是哪一行变化时重新计算哪一行,数据不变的行不需要重新计算。
二、computed如何传参?
针对以上情况,大家第一时间可能想到是利用计算属性computed传入一个参数。计算属性computed是基于它们的响应式依赖进行缓存的,只在相关响应式依赖发生改变时它们才会重新求值。
那么问题来了,Vue中计算属性computed如何传参?
答:计算属性是不能直接传参的。在vue3中的组合式API中,computed是无法传入参数的。在vue2的选项式API中,computed可以通过闭包函数(也叫匿名函数)间接传参来实现。
代码如下:
<el-table-column prop="sum" label="总价">
<template #default="scope">
{{ computedPrice(scope.row) }}
</template>
</el-table-column>
computed: {
computedPrice() {
return function(row) {
console.log(row)
return row.price * row.num * row.discount;
}
}
}
很明显以上方法也没有实现我们想要的结果!那么到底怎么才能实现我们的预期结果呢?
三、分析与实现
我们仔细分析一下,其实这个需求就是希望为每一个数据参数对应一个计算属性computed,当每一行数据不变时,就返回这个没有改变的计算属性。反之某一行数据变化了,那么它对应的计算属性也发生了变化。也就是说,一个参数对应一个计算属性,每一行数据里面都有一个计算属性方法。
例如:
const tableData = ref([
{
product: '华为 Mate 60Pro',
price: 8000,
num: 1,
discount: 0.9,
totalPrice: computed(() => {
// do something
})
},
{
product: '华为 Pura 70Pro',
price: 7000,
num: 1,
discount: 0.95,
totalPrice: computed(() => {
// do something
})
},
{
product: '华为 Mate X5',
price: 11500,
num: 1,
discount: 0.88,
totalPrice: computed(() => {
// do something
})
}
])
如果要这么实现非常复杂笨拙,而且当列表有非常多数据时现实起来也不切实际。那么我们可以封装一个函数useComputed,把我们真正要计算的函数传入useComputed并返回一个新的函数computedPrice,只需要用computedPrice去进行总价的计算即可。
完整代码如下:
<template>
<div class="container">
<el-table :data="tableData" border style="width: 100%">
<el-table-column prop="product" label="产品" />
<el-table-column prop="price" label="价格" />
<el-table-column prop="discount" label="优惠" />
<el-table-column prop="num" label="数量">
<template #default="scope">
<el-input-number v-model="scope.row.num" :min="1" :max="10" :key="scope.row.price" />
</template>
</el-table-column>
<el-table-column prop="sum" label="总价">
<template #default="scope">
{{ computedPrice(scope.row) }}
</template>
</el-table-column>
</el-table>
</div>
</template>
<script setup>
import { ref, computed } from 'vue';
const tableData = ref([
{
product: '华为 Mate 60Pro',
price: 8000,
num: 1,
discount: 0.9,
},
{
product: '华为 Pura 70Pro',
price: 7000,
num: 1,
discount: 0.95,
},
{
product: '华为 Mate X5',
price: 11500,
num: 1,
discount: 0.88,
},
])
function useComputed(fn) {
const map = new Map();
return function(...args) {
const key = JSON.stringify(args);
if(map.has(key)) {
return map.get(key)
}
const result = computed(() => {
return fn(...args)
})
map.set(key, result);
return result;
}
}
function totalPrice(row) {
console.log(row)
return row.price * row.num * row.discount;
}
const computedPrice = useComputed(totalPrice)
</script>
效果如下:
上图可以看到,哪一行数据变化了就重新计算哪一行,数据不变的行不进行计算,完美的达到了我们想要的结果。
下面就对重要代码进行分析:
function useComputed(fn) {
// 创建Map缓存结果
const map = new Map();
// 返回一个函数
return function(...args) {
// args是一个数组,把参数转成字符串,当做Map的key
const key = JSON.stringify(args);
console.log(key);
// [{"product":"华为 Mate 60Pro","price":8000,"num":1,"discount":0.9}]把这一长串当做key
// 判断是否有对应的计算属性
if(map.has(key)) {
// 有就返回计算属性
return map.get(key)
}
// 没有就创建一个计算属性
const result = computed(() => {
return fn(...args)
})
// 把创建的计算属性返回的结果result和参数关联
map.set(key, result);
return result;
}
}