兄弟组件间联动(重要) 实现功能:点击城市列表页面右侧的字母,列表选项会滚动到对应的字母区域。
兄弟组件的传值,可以通过 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 ) { 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 相同的首字母的列表项要显示出来,怎么做呢?
这个时候就要借助一个侦听器,监听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 ( ) { if (this .letter) { const element = this .$refs[this .letter][0 ]; this .scroll.scrollToElement(element); } } }
这个时候就可以通过字母获取到对应的区域,然后把 element 传入 scrollToElement 里,注意,上边代码最后加了一个 [0],这是因为如果不加,通过 ref 或的内容就是一个数组,这个数组里的第一个元素才是真正的 DOM 元素,这个时候,点击右侧字母表,就可以跳到对应的字母下的城市列表了。
点击跳转的功能实现了
接下来再实现一下滑动右侧字母表,左侧城市列表切换的效果。
知识补充
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 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 ); if (index >= 0 && index < this .letters.length) { this .$emit("change" , this .letters[index]); } }, 16 ); } },
这里设置的周期是16ms,16ms这个代码只会执行一次,大大优化了性能
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; },