目标: 实现兄弟组件之间的联动
(一) 点击城市页面的Alphabet.vue中的字母能够跳转到对应的List.vue的位置
-
给Alphabet.vue中的每一个循环的li绑定一个点击事件
<li class="item" v-for="(item,key) of cities" :key="key" @click="handleLetterClick">{{key}}</li>
,并将这个事件定义到methods对象中,当点击某一个字母的时候这个事件会接收到一个事件对象e,console.log(e.target.innerText)
会发现,在页面上点击字母的时候在终端就会显示相应的字母。 希望的是: 我们把这个字母传递给List.vue组件, 让其对应的区块显示出来。 -
兄弟组件的传值: 兄弟组件是非父子组件,可以使用bus总线的方式,而这里的非父子组件就是一个兄弟组件,我们可以将Alphabet.vue中点击字母获取到的数据传递给City.vue组件,然后再由这个组件转发给List.vue组件
Alphabet.vue组件中触发点击事件获取到数据,使用this.$emit('change', e.target.innerText)
的方式向外触发事件,事件的名字是change
,携带的数据是e.target.innerText
。
然后, 由City.vue组件来监听这个事件,即<city-alphabet :cities="cities" @chang="handleLetterChange"></city-alphabet>
。在methods中定义这个事件,并且它会接收到一个letter数据,这个数据就是由子组件Alphabet.vue传递过来的那个点击的字母。
现在,将letter转发给List.vue。这里就是父组件向子组件传递数据了,是通过属性的形式传递的, 首先在data函数中定义一个数据letter,默认为一个空字符串,当接收到外部传来的letter数据的时候,即:
handleLetterChange (letter) {
this.letter = letter
}
最后就只需要将这个letter传递给city-list就可以了:<city-list :cities="cities" :hotCities="hotCities" :letter="letter"></city-list>
。
父组件传递了一个letter,子组件List.vue就要接收这个letter:
props: {
cities: Object,
hotCities: Array,
letter: String
}
这个时候,子组件就监听到由父组件传递过来的这个letter的内容了。我们想要做的是: 当我监听到这个letter发生变化的时候,我要把本组件中与letter值相同的那个城市列表项显示出来。
实现:借助vue自带的侦听器watch,在watch中侦听letter的变化。better-scroll提供了一个接口scrollToElement()
,它可以让better-scroll的滚动区自动的滚到某一个元素上。具体的,先给循环区域添加一个ref::ref="key"
,就可以通过this.$refs
获取到这个字母this.letter
对应的class="area"
的这个区域:
watch: {
letter () {
if (this.letter) {
const element = this.$refs[this.letter]
this.scroll.scrollToElement(element)
}
}
}
上述代码会报错,原因是ref是通过循环输出的,this.$refs[this.letter]
或得到的内容是一个数组而不是一个标准的DOM元素,this.scroll.scrollToElement(element)
的参数element需要是一个DOM元素或者是一个DOM的选择器,所以需要修改为:
watch: {
letter () {
if (this.letter) {
const element = this.$refs[this.letter][0]
this.scroll.scrollToElement(element)
}
}
}
(二) 希望在右侧的字母表上做上下拖拽的时候,也会导致左侧的列表项的相应变动
首先,给Alphabet组件中的li绑定三个touch事件,即@touchstart="handleTouchStart" @touchmove="handleTouchMove" @touchend="handleTouchEnd"
,并定义相应的事件函数。然后,在data中设置一个标识位:touchStatus: false
,当手指触摸的时候,将这个值设置为true,当手指停止触摸的时候设置为false。只有当touch状态为true的时候才去做move的处理,此时代码是这样的:
handleTouchStart () {
this.touchStatus = true
},
handleTouchMove () {
if (this.touchStatus) {
}
},
handleTouchEnd () {
this.touchStatus = false
}
其次,在右侧的字母表处上下滑动的时候,想要直接获取当前手指所在的是哪一个字母是比较困难的,我们的思路是: 首先获得A这个字母距离顶部的高度,然后获得当前滑动的时候距离顶部的高度,做一个差值,就能够获取当前字母与A字母之间的高度,再除以每个字母的高度,就可以知道当前是哪一个字母了。这样的话,去取对应的字母,触发一个change事件给外部就可以了。
如果想要根据下标找到这个下标对应的字母的话,就需要有个数组来存储这个字母的列表, 我们之前使用的cities是一个对象而不是一个数组,所以需要定义一个数组类型的数据,在这里设置一个计算属性:
computed: {
letters () {
const letters = []
for (let i in this.cities) {
letters.push(i)
}
return letters
}
}
其返回的结果是一个数组,形式是[‘A’,‘B’,‘C’,…]。有了这个数组,之前的循环就可以做以下修改:
<ul>
<li class="item"
v-for="item of letters"
:key="item"
@click="handleLetterClick"
@touchstart="handleTouchStart"
@touchmove="handleTouchMove"
@touchend="handleTouchEnd"
>
{{item}}</li>
</ul>
当我们上下拖动的时候,即this.touchStatus = true
的时候,需要计算一下A字母距离顶部的高度,方法是:设置一个ref属性来获取A元素距离顶部的高度:
<ul>
<li class="item"
v-for="item of letters"
:key="item"
:ref="item"
@click="handleLetterClick"
@touchstart="handleTouchStart"
@touchmove="handleTouchMove"
@touchend="handleTouchEnd"
>
{{item}}</li>
</ul>
接下来,就可以按照之前的思路进行位置的计算:
handleTouchMove (e) {
if (this.touchStatus) {
const startY = this.$refs['A'][0].offsetTop
const touchY = e.touches[0].clientY - 79
const index = Math.floor((touchY - startY) / 20)
if (index >= 0 && index < this.letters.length) {
this.$emit('change', this.letters[index])
}
}
}
其中,const startY = this.$refs['A'][0].offsetTop
计算的是字母A距离头部蓝色区域的下边沿的高度。handleTouchMove的时候会接收到一个事件参数对象e,在这个事件对象中会有一个touches的数组,第0项表示的就是我手指触摸屏幕的一些信息,于是可以获取到手指的clientY的值,而这个值表示的是当前的位置到屏幕最顶部的距离,要想计算当前位置到蓝色区域下边沿的高度,还要减去头部蓝色区域的高度,即e.touches[0].clientY - 79
。index就是当前字母的下标了,公式中的20表示的是每个字母的高度。当索引值在0到this.letters.length
之间的时候就可以向外触发change事件了,传递的数据就是this.letters[index]
。
(三) 列表切换性能优化
- 在
handleTouchMove
方法中,当我们的手指在右侧的字母表上进行上下滑动的时候,这个方法就会被执行,其中A字母的offsetTop一直都是固定的,而每次执行这个方法的时候,这里都会计算一次,所以性能就会比较低。解决方法: 在data中定义一个变量startY,初始值设置为0,然后再写一个updated的生命周期钩子,当页面的数据被更新的时候,并且页面完成了自己的渲染之后,updated这个钩子就会执行,即:
updated () {
this.startY = this.$refs['A'][0].offsetTop
}
分析: 当初次渲染Alphabet.vue的时候, cities的数值化值是一个空对象,页面刚加载的时候,Alphabet.vue里面什么东西都不会显示出来,当City.vue通过ajax获取到数据之后,cities的值才发生变化,Alphabet.vue才会被渲染出来,当向Alphabet.vue中传递的数据发生变化的时候,它就会重新渲染,当Alphabet.vue被重新渲染之后,这个updated生命周期钩子就会被执行,这时,页面上已经展示出了字母列表的所有内容,这时去获取字母A的offsetTop就不会有问题,拖动效果依然能够实现。
- 函数节流:
当我们的鼠标在字母表上来回的移动的时候,touchmove执行的频率是非常高的,我们可以通过节流来限制一下函数执行的频率。
在数据项中定义一个变量timer,默认值为null,当在执行handleTouchMove的时候,首先在判断是否已经存在了,如果存在就清除掉这个timer,否则就创建一个timer:
handleTouchMove (e) {
if (this.touchStatus) {
if (this.timer) {
clearInterval(this.timer)
}
this.timer = setTimeout(() => {
const touchY = e.touches[0].clientY - 79
const index = Math.floor((touchY - this.startY) / 20)
if (index >= 0 && index < this.letters.length) {
this.$emit('change', this.letters[index])
}
}, 16);
}
}
优化完成。