Vue去哪儿项目&兄弟组件间联动&节流


兄弟组件间联动(重要)

实现功能:点击城市列表页面右侧的字母,列表选项会滚动到对应的字母区域。

兄弟组件的传值,可以通过 bus 总线的形式来传值。但是因为我们现在这个非父子组件比较简单,可以让 Alphabet.vue 组件将值传递给父组件 City.vue 组件,然后 City.vue 组件再将值转发给 List.vue 组件,这样就实现了兄弟组件的传值。【子组件给父组件,父组件再转给另一个子组件】。这样,在 Alphabet.vue 中点击右侧字母,会获取到对应的字母。

src\pages\city\components\Alphabet.vue

在循环的元素上加一个点击事件,例如 handleLetterClick,然后在 methods 中写这个事件方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<template>
<ul class="list">
<li
class="item"
v-for="item of letters"
:key="item"
:ref="item"
@click="handleLetterClick"
>
{{item}}
</li>
</ul>
</template>

<script>
methods: {
handleLetterClick(e) {
this.$emit("change", e.target.innerHTML);
}
}
</script>

首先在父组件 City.vue 里的 data 中定义一个 letter,默认值是空,在 handleLetterClick 方法中,当接受到外部传来的 letter 的时候,让 this.letter = letter。

City.vue部分代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<city-list :cities="cities" :hot="hotCities" :letter="letter"></city-list>
<city-alphabet :cities="cities" @change="handLetterChange"></city-alphabet>

export default {
name: "City",
components: {
CityHeader,
CitySearch,
CityList,
CityAlphabet
},
methods: {
handLetterChange(letter) {
// console.log(letter);
this.letter = letter;
}
},
// 存放数据
data() {
return {
cities: {},
hotCities: [],
letter: ""
};
},

};
</script>

接下来,将父组件接收到的这个数据转发给子组件 List.vue,父组件是通过属性props向子组件传值的。

最后只需要把 letter 传递给子组件 List.vue 就可以了,在 City.vue 组件的模板 city-list 中通过 :letter=”letter” 向子组件 List 传值,在 props 中接收这个 letter,并且验证类型为 String 类型。

List.vue

1
2
3
4
5
props: {
hot: Array,
cities: Object,
letter: String
}

这样就实现了兄弟组件的传值。

【项目难点】

接下来要做的是,当 List.vue 发现 letter 有改变的时候,就需要让组件显示的列表项跟 letter 相同的首字母的列表项要显示出来,怎么做呢?

  1. 这个时候就要借助一个侦听器,监听letter的变化;

better-scroll 给提供了这样一个接口,scroll.scorllToElement,如果 letter 不为空的时候,就调用 this.scroll.scrollToElement() 这个方法,可以让滚动区自动滚到某一个元素上,那么怎么传这个元素呢?在循环城市这一块中,给循环项加一个 ref 引用来获取当前 Dom 元素,等于 key,然后回到侦听器的 letter 中,定义一个 element,它就等于通过 ref 获取到的元素:

src\pages\city\components\List.vue

ref:如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例,可以通过实例直接调用组件的方法或访问数据

1
2
3
4
5
6
7
8
9
10
11
<div class="area" v-for="(item, key) of cities" :key="key" :ref="key">

watch: {
letter() {
// console.log(this.letter);
if (this.letter) {
const element = this.$refs[this.letter][0];
this.scroll.scrollToElement(element);
}
}
}

这个时候就可以通过字母获取到对应的区域,然后把 element 传入 scrollToElement 里,注意,上边代码最后加了一个 [0],这是因为如果不加,通过 ref 或的内容就是一个数组,这个数组里的第一个元素才是真正的 DOM 元素,这个时候,点击右侧字母表,就可以跳到对应的字母下的城市列表了。

点击跳转的功能实现了

  1. 接下来再实现一下滑动右侧字母表,左侧城市列表切换的效果。

知识补充

1
2
3
4
5
6
7
touchstart事件:当手指触摸屏幕时候触发,即使已经有一个手指放在屏幕上也会触发。

touchmove事件:当手指在屏幕上滑动的时候连续地触发。在这个事件发生期间,调用preventDefault()事件可以阻止滚动。

touchend事件:当手指从屏幕上离开的时候触发。

touchcancel事件:当系统停止跟踪触摸的时候触发。关于这个事件的确切出发时间,文档中并没有具体说明,咱们只能去猜测了。

src\pages\city\components\Alphabet.vue部分代码

思路:绑定三个新的事件, @touchstart @touchmove @touchend,然后定义一个标识位, touchStatus: false,开始@touchstart=true,结束@touchend=false

首先我们要知道我们滑动的是第几个字母–思路:先获得A字母到顶部的高度,然后滑动后获得当前位置距离顶部的高度,做一个差值就能算出当前位置距离A的高度了,除以每个字母的高度就能得到当前是第几个字母了,然后去取对应的字母触发一个change事件给外部。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
<template>
<ul class="list">
<li
class="item"
v-for="item of letters"
:key="item"
:ref="item"
@touchstart="handleTouchStart"
@touchmove="handleTouchMove"
@touchend="handleTouchEnd"
@click="handleLetterClick"
>
{{ item }}
</li>
</ul>
</template>
<script>
export default {
name: "CityAlphabet",
props: {
cities: Object
},
//计算属性得到一个字母数组['A','B',......]
computed: {
letters() {
const letters = [];
for (let i in this.cities) {
letters.push(i);
}
return letters;
}
},
data() {
return {
touchStatus: false,
};
},
methods: {
handleLetterClick(e) {
// console.log(e.target.innerText);
this.$emit("change", e.target.innerText);
},
handleTouchStart() {
this.touchStatus = true;
},
handleTouchMove(e) {
if (this.touchStatus) {
//startY 计算的是A的顶部距离上沿的距离
//HTMLElement.offsetTop 为只读属性,它返回当前元素相对于其 offsetParent 元素的顶部内边距的距离。
const startY = this.$refs["A"][0].offsetTop;//74
//clientY 事件属性clientY 事件属性返回当事件被触发时鼠标指针向对于浏览器页面(客户区)的垂直坐标。 客户区指的是当前窗口。
const touchY = e.touches[0].clientY - 79;
const index = Math.floor((touchY - startY) / 20); //20为每个字母的高度,index计算出来为每个字母的下标0,1,2,3,4...
if (index >= 0 && index < this.letters.length) {
this.$emit("change", this.letters[index]);
}
}

},
handleTouchEnd() {
this.touchStatus = false;
}
}
};
</script>

列表切换性能优化

节流

手指在城市字母表中滑动时,会触发无数次handleTouchMove这个函数,这就对性能影响很大。

函数节流:通过设定一个时间周期,只要在这个周期内函数就不执行。

实现方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

// if (this.touchStatus) {
// const startY = this.$refs["A"][0].offsetTop;
// const touchY = e.touches[0].clientY - 79;
// const index = Math.floor((touchY - startY) / 20); //20为每个字母的高度
// if (index >= 0 && index < this.letters.length) {
// this.$emit("change", this.letters[index]);
// }
// }
//节流
handleTouchMove(e) {
if (this.touchStatus) {
if (this.timer) {
clearTimeout(this.timer);
}
this.timer = setTimeout(() => {
const touchY = e.touches[0].clientY - 79;
const index = Math.floor((touchY - this.startY) / 20); //20为每个字母的高度
if (index >= 0 && index < this.letters.length) {
this.$emit("change", this.letters[index]);
}
}, 16);
}
},

这里设置的周期是16ms,16ms这个代码只会执行一次,大大优化了性能

  1. offsetTop的值是固定的,我们每一次去执行这个方法就会去运算一次,性能很低

解决: data()中 先定义startY: 0,分析:页面刚加载的时候,Alphabet.vue中什么都不会显示出来,当City.vue中ajax获取数据后,Citys的值才发生变化,Alphabet才被渲染出来。当往Alphabet中传入的数据发生变化时,Alphabet才会被重新渲染,当Alphabet重新渲染之后,updated() 这个生命周期就会被执行,这个时候页面已经展示了城市字母列表的所有内容,这个时候我们去计算offsetTop就可以了。

1
2
3
4
5
6
7
8
9
10
data() {
return {
touchStatus: false,
startY: 0,
timer: null
};
},
updated() {
this.startY = this.$refs["A"][0].offsetTop;
},