GoodsList组件用来展示商品列表,而单个商品对应的组件是GoodsListItem,前者是后者的容器。之前我们在实现类似结构时,会在item组件中定义几个插槽,然后在容器组件中对插槽进行填充。但这次由于item中的结构较为复杂,所以老师采用了另一种组件通信方式,即props。首先爷爷组件Home将请求到的30个商品的数据发送给父组件GoodsList,然后GoodsList在通过v-for动态生成GoodsListItem的同时,将每个商品的数据发送给各个GoodsListItem。在GoodsListItem中,以恰当的方式来显示每个商品的价格、图片、描述等信息。
先来看看GoodsListItem组件的结构:
<template> <div class="goods-item" @click="itemClick"> <img :src="showImg" alt="" @load="imageLoad"> <div class="goods-info"> <p>{{goodsItem.title}}</p> <span class="price">{{goodsItem.price}}</span> <span class="collect">{{goodsItem.cfav}}</span> </div> </div> </template>
当点击该item后,会触发itemClick方法,触发结果就是跳转到该商品的详情页面,通过修改当前路由来实现。具体我们在详情组件中再介绍。
itemClick(){ this.$router.push('/detail/' + this.goodsItem.iid) }
图片的src属性为一个计算属性showImg,这是因为在服务器返回的数据的存储结构中,图片的位置有两种可能。
showImg(){ return this.goodsItem.image || this.goodsItem.show.img }
图片加载完成后会触发imageLoad方法,众所周知,图片的加载是最慢的,很可能在scroll组件计算滚动区域高度时,图片还没加载完。当图片全部加载完后,页面实际高度为6000,而可滚动区域的高度只有5000,这就导致页面可能滚到一半就滚不下去了。所以我们有必要在所有图片加载完之后,让scroll组件刷新可滚动区域的高度。
但是注意到当前的GoodsListItem组件是Home的孙子组件,它们之间通信需要发送两次$emit,我们采用另一种方法:事件总线。
在main.js文件中,我们在Vue的原型上添加一个$bus属性,属性值为,,,一个vue实例:
Vue.prototype.$bus = new Vue()
之后我们在任意位置访问this.$bus都相当于在访问一个vue实例,而vue实例上可以通过$emit和$on来发送和监听自定义事件。如下:
// GoodsListItem组件中
imageLoad(){ this.$bus.$emit('homeItemImageLoad') // 事件总线发送事件 }
// Home组件中 const refresh = debounce(this.$refs.scroll.refresh, 200); this.$bus.$on("homeItemImageLoad", () => { refresh(); });
由于我们给每个图片都绑定了imageLoad方法,所以很可能在短时间内,该方法会被触发30次(因为每次加载30张图片)。而我们只希望最后一张图片加载完成时刷新,因此这里使用了防抖函数debounce来提升性能,当refresh方法被调用时,必须在之后的200ms之内没有出现同样的调用,这时refresh才会真的被执行。
debounce方法的代码如下,这是面试时的必考题(还有节流函数throttle),必须烂熟于心。
function debounce(func, delay){ let timer = null return function(...args){ if(timer) clearTimeout(timer) timer = setTimeout(() => { func.apply(this, args) }, delay) } }
另外还要注意这里 和 轮播图加载完成的事件响应函数 的区别,轮播图由于多个图片和一张图片占用的高度相同,所以第一张图片加载完就发送了事件,之后的图片加载完成后,压根没有再发送事件。而这里父组件监听到事件后进入回调函数,防抖函数只是减少了该回调的调用次数,而子组件发送事件的次数,仍然为30次没有变。
scroll组件中的refresh方法其实是scroll对象自带的方法,这里进行了一层封装,使得在调用时可以少写一个.scroll。另外,因为scroll对象是在scroll组件的mounted()中初始化的,当在父组件中调用scroll的方法时,很可能scroll对象还未创建,方法压根不存在,便会报错。通过将方法名和方法调用进行逻辑与,可以保证只有当方法存在时才会被调用,避免报错。
refresh() { this.scroll && this.scroll.refresh(); },
还要注意的是,对事件总线上事件的监听,被写在了生命周期钩子mounted()里面。这是因为只有当组件都挂载完成以后,才能确保this.$refs可以正常使用;如果在created()中使用this.refs.xxx,很可能返回undefined。