转载请注明出处,未经同意,不可修改文章内容。
🔥🔥🔥“前端一万小时”两大明星专栏——“从零基础到轻松就业”、“前端面试刷题”,已于本月大改版,合二为一,干货满满,欢迎点击公众号菜单栏各模块了解。
1 需求
实现“列表”与“字母表”之间的联动:
- 点击“字母表”的任意字母,“列表”会自动显示对应字母区域的内容;
- 在“字母表”区域滑动时,“列表”自动显示停止滑动时所对应字母区域的内容。
2 实现“点击”字母,列表显示对应内容的功能
🔗前置知识:
《JavaScript 基础——JS 事件:② 事件对象和事件代理》
《Vue 入门——④ 计算属性、方法与侦听器》
《深入理解 Vue 组件——① 使用组件的细节点》
《深入理解 Vue 组件——② 父子组件间的数据传递》
需求分析:
假设,当点击字母表的“F”时,列表显示对应的以“F”为拼音首字母的城市。这里,就需要在 Alphabet.vue 和 List.vue 这两个“非父子组件”间传值。但,因为他们有共同的父组件 City.vue ,所以,他们之间的通信便属于非常简单的“兄弟组件”间传值。
❓如何实现简单的兄弟组件间传值?
答:在字母表 Alphabet.vue 中,当某个字母被点击时,我们将信息传递给父组件 City.vue ;父组件接收后,再由父组件转发给列表组件 List.vue 。
1️⃣打开 city 下 components 中的 Alphabet.vue :
<template>
<ul class="list">
<li
class="item"
v-for="(item, key) of cities"
:key="key"
@click="handleLetterClick"
> <!-- 1️⃣-①:在 li 标签上绑定 click 事件,给每一个循环项都添加一个点击事件,当元素被点击时
(即字母被点击),执行 handleLetterClick 方法; -->
{{key}}
</li>
</ul>
</template>
<script>
export default {
name: 'CityAlphabet',
props: {
cities: Object
},
methods: { // 1️⃣-②:在 methods 中定义 handleLetterClick 方法;
handleLetterClick (e) { // 1️⃣-③:当字母表中的某个元素被点击时,方法接收一个事件对象 e;
this.$emit('change', e.target.innerText) /*
1️⃣-④:通过 $emit 向外触发一个我们取名
为 change 的事件,并将被点击元素中的文本内容
传递给父组件(即 A、B、C 等字母);
*/
}
}
}
</script>
<style lang="stylus" scoped>
@import '~styles/varibles.styl'
.list
position: absolute
top: 1.58rem
right: 0
bottom: 0
width: .4rem
display: flex
flex-direction: column
justify-content: center
.item
line-height: .4rem
text-align: center
color: $bgColor
</style>
每次点击子组件 Alphabet.vue ,都会触发事件。然后,由父组件 City.vue 监听这个事件,并接收它传递过来的数据。
1️⃣-⑤:打开 city 下的 City.vue ;
<template>
<div>
<city-header></city-header>
<city-search></city-search>
<city-list :hot="hotCities" :cities="cities"></city-list>
<!-- 1️⃣-⑥:监听 change 事件,事件触发时执行 handleLetterChange 方法; -->
<city-alphabet :cities="cities" @change="handleLetterChange"></city-alphabet>
</div>
</template>
<script>
import axios from 'axios'
import CityHeader from './components/Header'
import CitySearch from './components/Search'
import CityList from './components/List'
import CityAlphabet from './components/Alphabet'
export default {
name: 'City',
components: {
CityHeader,
CitySearch,
CityList,
CityAlphabet
},
data () {
return {
hotCities: [],
cities: {}
}
},
methods: {
getCityInfo () {
axios.get('/api/city.json')
.then(this.getCityInfoSucc)
},
getCityInfoSucc (res) {
res = res.data
if (res.ret && res.data) {
const data = res.data
this.hotCities = data.hotCities
this.cities = data.cities
}
},
handleLetterChange (letter) { /*
1️⃣-⑦:handleLetterChange 方法接收 Alphabet.vue
传来的数据 letter;
*/
console.log(letter) // ❗️打印出 letter,查看是否正确接收到数据。
}
},
mounted () {
this.getCityInfo()
}
}
</script>
<style>
</style>
保存后,返回页面查看,当点击字母表时,正确输出了被点击的字母(即父组件已接收到数据):
父组件接收到数据后,需要将数据转发给另一个子组件 List.vue 。
1️⃣-⑧:返回 city 下的 City.vue ;
<template>
<div>
<city-header></city-header>
<city-search></city-search>
<!-- 1️⃣-⑪:通过属性 :letter 将数据 letter 传递给子组件 List.vue; -->
<city-list :hot="hotCities" :cities="cities" :letter="letter"></city-list>
<city-alphabet :cities="cities" @change="handleLetterChange"></city-alphabet>
</div>
</template>
<script>
import axios from 'axios'
import CityHeader from './components/Header'
import CitySearch from './components/Search'
import CityList from './components/List'
import CityAlphabet from './components/Alphabet'
export default {
name: 'City',
components: {
CityHeader,
CitySearch,
CityList,
CityAlphabet
},
data () {
return {
hotCities: [],
cities: {},
letter: '' // 1️⃣-⑨:在 data 中,定义一个变量 letter,默认为空;
}
},
methods: {
getCityInfo () {
axios.get('/api/city.json')
.then(this.getCityInfoSucc)
},
getCityInfoSucc (res) {
res = res.data
if (res.ret && res.data) {
const data = res.data
this.hotCities = data.hotCities
this.cities = data.cities
}
},
handleLetterChange (letter) {
this.letter = letter /*
1️⃣-⑩:当接收到 Alphabet.vue 传来的数据时,将传进来的数据赋值
给 City.vue 中的数据 letter;
*/
}
},
mounted () {
this.getCityInfo()
}
}
</script>
<style>
</style>
❓当子组件 List.vue 收到“被点击字母”的数据后,如何让它显示对应的城市区域呢?
答:我们可以调用 BetterScroll 提供的 scrollToElement() 方法,让 BetterScroll 的滚动区域自动滚动到某一个元素上。
1️⃣-⑫:返回 city 下 components 中的 List.vue ;
<template>
<div class="list" ref="wrapper">
<div>
<div class="area">
<div class="title border-topbottom">当前城市</div>
<div class="button-list">
<div class="button-wrapper">
<div class="button">北京</div>
</div>
</div>
</div>
<div class="area">
<div class="title border-topbottom">热门城市</div>
<div class="button-list">
<div class="button-wrapper" v-for="item of hot" :key="item.id">
<div class="button">{{item.name}}</div>
</div>
</div>
</div>
<div
class="area"
v-for="(item, key) of cities"
:key="key"
:ref="key"
> <!-- 1️⃣-⑯:循环上动态绑定 ref 为 key(即字母); -->
<div class="title border-topbottom">{{key}}</div>
<ul class="item-list">
<li
class="item border-bottom"
v-for="innerItem of item"
:key="innerItem.id"
>
{{innerItem.name}}
</li>
</ul>
</div>
</div>
</div>
</template>
<script>
import BScroll from 'better-scroll'
export default {
name: 'CityList',
props: {
hot: Array,
cities: Object,
letter: String // 1️⃣-⑬:子组件通过 props 接收数据,letter 的值类型为字符串;
},
mounted () {
this.scroll = new BScroll(this.$refs.wrapper)
},
watch: { // 1️⃣-⑭:借助侦听器,监听 letter 的变化;
letter () { // 1️⃣-⑮:定义 letter 方法,一旦监听到数据 letter 发生变化,就执行方法;
const element = this.$refs[this.letter][0] /*
1️⃣-⑰:通过 ref 获取被点击字母对应的 DOM
元素,并赋值给 element;
❗️使用 for 循环输出的 ref,通过
this.$refs[this.letter] 获取到的是
一个数组,所以这里获取 DOM 元素需要在后面
加上下标 0;
*/
this.scroll.scrollToElement(element) /*
1️⃣-⑱:scrollToElement 方法接收一个 DOM 元素
(或一个 DOM 选择器),所以方法中传入 element;
*/
}
}
}
</script>
<style lang="stylus" scoped>
.border-topbottom
&:before
border-color: #ccc
&:after
border-color: #ccc
.border-bottom
&:before
border-color: #ccc
.list
overflow: hidden
position: absolute
top: 1.58rem
left: 0
right: 0
bottom: 0
.title
padding-left: .2rem
line-height: .54rem
background: #eee
color: #666
font-size: .26rem
.button-list
overflow: hidden
padding: .1rem .6rem .1rem .1rem
.button-wrapper
float: left
width: 33.33%
.button
margin: .1rem
padding: .1rem 0
text-align: center
border: .02rem solid #ccc
border-radius: .06rem
.item-list
.item
line-height: .76rem
padding-left: .2rem
</style>
保存后,返回页面查看,当点击字母后,列表正确显示对应的城市:
3 实现在字母表“滑动”,列表显示滑动停止时所在字母对应内容
🔗前置知识:《实现一个简单的左右滑动手势库》
需求分析:
假设我们从字母表的“A”滑动到“F”,那么 F 距离顶部的距离减去 A 距离顶部的距离,就能获得手指滑动的距离。再用滑动的距离除以每个字母的高度,就可以知道手指滑动到第几个字母。然后可以触发一个事件,告诉其他组件滑动到了第几个字母。
2️⃣打开 city 下 components 中的 Alphabet.vue ,按照上面分析的思路编写代码:
<template>
<ul class="list">
<li
class="item"
v-for="item of letters"
:key="item"
:ref="item"
@click="handleLetterClick"
@touchstart="handleTouchStart"
@touchmove="handleTouchMove"
@touchend="handleTouchEnd"
>
<!-- 2️⃣-⑤:li 标签直接对 letters 进行循环,绑定的 key 值和显示的内容也改为循环项 item; -->
<!-- 2️⃣-⑥:添加一个 ref,名字为循环项 item,方便获取 DOM 元素; -->
<!-- 2️⃣-⑦:添加 touchstart 事件,触发时执行 handleTouchStart 方法;
2️⃣-⑧:添加 touchmove 事件,触发时执行 handleTouchMove 方法;
2️⃣-⑨:添加 touchend 事件,触发时执行 handleTouchEnd 方法; -->
{{item}} <!-- ❗️显示的内容改为为 item。 -->
</li>
</ul>
</template>
<script>
export default {
name: 'CityAlphabet',
props: {
cities: Object
},
computed: { // 2️⃣-①:在 computed 中定义一个计算属性 letters;
letters () {
const letters = [] /*
2️⃣-②:定义一个 letters 变量,它为空数组,用来存储字母,
方便我们利用下标查找字母;
*/
for (let i in this.cities) { /*
2️⃣-③:对数据 cities 进行循环,每一项放入 letters
数组中;
*/
letters.push(i)
}
return letters // 2️⃣-④:循环结束,返回 letters;
}
},
data () { // 2️⃣-⑩:定义一个变量 touchStatus 为“滑动”开始和结束的“标识符”,默认为 false;
return {
touchStatus: false
}
},
methods: {
handleLetterClick (e) {
this.$emit('change', e.target.innerText)
},
handleTouchStart () { // 2️⃣-⑪:当滑动开始时,使 touchStatus 变为 true;
this.touchStatus = true
},
handleTouchMove (e) { // 2️⃣-⑬:handleTouchMove 方法接收一个事件 e;
if (this.touchStatus) { /*
2️⃣-⑭:只有当 touchStatus 变为 true 时,
才进行 touchmove 事件的处理;
*/
const startY = this.$refs['A'][0].offsetTop /*
2️⃣-⑮:定义一个 startY 变量,
它的值为字母 A 与顶部之间的距离;
*/
const touchY = e.touches[0].clientY /*
2️⃣-⑯:定义一个 touchY 变量,
它的值为手指与可视区域顶部之间的距离;
*/
console.log(startY) // 2️⃣-⑰:分别输出 startY 和 touchY 查看;
console.log(touchY)
}
},
handleTouchEnd () { // 2️⃣-⑫:当滑动结束时,使 touchStatus 恢复为 false;
this.touchStatus = false
}
}
}
</script>
<style lang="stylus" scoped>
@import '~styles/varibles.styl'
.list
position: absolute
top: 1.58rem
right: 0
bottom: 0
width: .4rem
display: flex
flex-direction: column
justify-content: center
.item
line-height: .4rem
text-align: center
color: $bgColor
</style>
保存后,分别输出 startY 和 touchY ,可以看到 startY 即 A 与其父元素 .list 顶部之间的距离。 touchY 随”手指“滑动而变化,其中最小的值大概在 170 左右,即 A 距离整个可视区域顶部的距离:
2️⃣-⑱:startY 是“A”距离搜索框底部的距离没有问题,但目前的 touchY 是手指距离最顶部的高度。所以我们需要先用得到的值再减去搜索框(高 36)和头部(高 43)两个组件的高度之和(和为 79);
<template>
<ul class="list">
<li
class="item"
v-for="item of letters"
:key="item"
:ref="item"
@click="handleLetterClick"
@touchstart="handleTouchStart"
@touchmove="handleTouchMove"
@touchend="handleTouchEnd"
>
{{item}}
</li>
</ul>
</template>
<script>
export default {
name: 'CityAlphabet',
props: {
cities: Object
},
computed: {
letters () {
const letters = []
for (let i in this.cities) {
letters.push(i)
}
return letters
}
},
data () {
return {
touchStatus: false
}
},
methods: {
handleLetterClick (e) {
this.$emit('change', e.target.innerText)
},
handleTouchStart () {
this.touchStatus = true
},
handleTouchMove (e) {
if (this.touchStatus) {
const startY = this.$refs['A'][0].offsetTop
const touchY = e.touches[0].clientY - 79 /*
❗️touchY 的值为 e.touches[0].clientY
再减去 79。
*/
const index = Math.floor((touchY - startY) / 20) /*
2️⃣-⑲:定义一个 index 变量,
它的值为 touchY 与 startY 的差
再除以每个字母的高度 20 后得到的
整数,表示每个字母对应的下标;
*/
this.$emit('change', this.letters[index]) /*
2️⃣-⑳:最后,依然触发 change 事件,
并传递被点击的字母;
*/
}
},
handleTouchEnd () {
this.touchStatus = false
}
}
}
</script>
<style lang="stylus" scoped>
@import '~styles/varibles.styl'
.list
position: absolute
top: 1.58rem
right: 0
bottom: 0
width: .4rem
display: flex
flex-direction: column
justify-content: center
.item
line-height: .4rem
text-align: center
color: $bgColor
</style>
保存后,返回页面查看,“点击”和“滑动”的功能都能正常使用:
4 性能优化
🔗前置知识:
《JavaScript 基础——浏览器提供的对象:③ 定时器》
《Vue 入门——② Vue 实例的生命周期函数》
至此,“列表”与“字母表”联动的功能貌似我们已经完成了。但,实际上还有几个地方需要优化,我们打印出滑动时每次的 index 查看:
可以“看到”的问题有:
- 事件触发过于频繁(用函数节流解决);
- 滑动时,当滑动的距离超出字母的范围,依然会触发
change事件并传递数据,但此时已经没有对应的字母了,所以控制台报错(对index值进行处理即可解决);
“隐藏”的问题:每次执行 handleTouchMove 都会对字母 A 距离顶部的这个“固定值”进行一次运算(对 startY 进行处理,让它尽量少地进行运算)。
3️⃣返回 city 下 components 中的 Alphabet.vue :
<template>
<ul class="list">
<li
class="item"
v-for="item of letters"
:key="item"
:ref="item"
@click="handleLetterClick"
@touchstart="handleTouchStart"
@touchmove="handleTouchMove"
@touchend="handleTouchEnd"
>
{{item}}
</li>
</ul>
</template>
<script>
export default {
name: 'CityAlphabet',
props: {
cities: Object
},
computed: {
letters () {
const letters = []
for (let i in this.cities) {
letters.push(i)
}
return letters
}
},
data () {
return {
touchStatus: false,
timer: null, // 3️⃣-①:在 data 中定义一个变量 timer,默认值为 null;
startY: 0 // 3️⃣-⑥:在 data 中定义变量 startY,默认值为 0;
}
},
updated () { // 3️⃣-⑦:在 updated 中,获取字母 A 距离顶部的高度并赋值给 startY;
this.startY = this.$refs['A'][0].offsetTop
},
methods: {
handleLetterClick (e) {
this.$emit('change', e.target.innerText)
},
handleTouchStart () {
this.touchStatus = true
},
handleTouchMove (e) {
if (this.touchStatus) { // 3️⃣-②:touchStatus 为 true 时;
if (this.timer) { // 3️⃣-③:如果 timer 已经存在了,就清除掉 timer;
clearTimeout(this.timer)
}
this.timer = setTimeout(() => { /*
3️⃣-④:如果没有 timer,就创建一个 timer,
让它延迟 16 毫秒执行里边的内容;
*/
const touchY = e.touches[0].clientY - 79
const index = Math.floor((touchY - this.startY) / 20) /*
3️⃣-⑧:startY 更改
为 this.startY。
*/
if (index >= 0 && index < this.letters.length) { /*
3️⃣-⑤:当 index 的值大于或
等于 0 且小于 letters 的长
度时,才触发 change 事件并
传递数据;
*/
this.$emit('change', this.letters[index])
}
}, 16)
}
},
handleTouchEnd () {
this.touchStatus = false
}
}
}
</script>
<style lang="stylus" scoped>
@import '~styles/varibles.styl'
.list
position: absolute
top: 1.58rem
right: 0
bottom: 0
width: .4rem
display: flex
flex-direction: column
justify-content: center
.item
line-height: .4rem
text-align: center
color: $bgColor
</style>
保存后,返回页面查看。从肉眼上来看没有什么变化,但从实际输出的内容就能“实在”地看到性能得到了大大的提升:
以上,我们实现了城市选择页的“列表 List.vue ”与“字母表 Alphabet.vue ”两个兄弟组件间的联动,并且优化了代码性能。
🏆本篇总结:
- 兄弟组件间的传值,可以先由子组件传递给父组件,父组件接收到后,再传递给另一个子组件;
- 通过函数节流,限制函数执行的频率来优化代码;
- 在
updated中(当数据发生改变、页面渲染完成后),对元素距离顶部的高度进行运算来优化代码。
祝好,qdywxs ♥ you!