一、缘起
事情的起因是这样的,有位朋友(无中生友)遇到了如下需求:
上面是一个商品列表,每个商品对应一个价格、优惠、数量,并且数量可以动态改变,最后动态计算出一个总价。当然,这只是一个简单地乘法计算,往往在实际项目开发中,遇到的需求要复杂的多,计算也更复杂。
大部分人第一时间想到是利用函数传参返回计算结果,显示在页面上。实现方式如下:
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的keyconst 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;}
}