本文转载自我的个人博客。
React中一个重要理念就是组件化,通过组件化从而实现组件的复用。我认为用回到顶部按钮来讲解react的组件化思想是最合适不过的了,因为回到顶部按钮其本身的实现逻辑并不复杂,但是将其组件化却用到了很多基础但是很重要的react知识点,如JSX中回调函数的this绑定、条件渲染、生命周期函数等等。
先看一下我们要实现的回到顶部组件长什么样子吧:

注意这个实现有两个细节:
- 组件一开始是隐藏,只有当下滑到一定程度时才会出现。
- 回到顶部这个动作不是瞬间的,是有动画的。
完整的代码如下,注释里有一些简短的解释,然后我们把重要的知识点单拎出来讲解。
import React, { Component } from 'react';
//引入由styled-components创建的样式组件
import {ScrollToTopWrapper} from './style';
class ScrollToTop extends Component {
constructor(props) {
super(props)
//show为true时回到顶部按钮显示,false时隐藏
this.state = ({
show: false
})
//将函数里的this指向绑定到当前组件,也就是组件ScrollToTop
this.changeScrollTopShow = this.changeScrollTopShow.bind(this);
this.scrollToTop = this.scrollToTop.bind(this);
}
//挂载事件监听
componentDidMount() {
window.addEventListener('scroll', this.changeScrollTopShow)
}
//卸载事件监听
componentWillUnmount() {
window.removeEventListener('scroll', this.changeScrollTopShow)
}
render() {
const { show } = this.state;
return(
//ScrollToTopWrapper是一个由styled-components定义的样式组件,其本质
//是一个div标签
<ScrollToTopWrapper>
{
// 逻辑与符号左边的show为true时返回右边的html标签
show &&
<div
className = "scrollTop"
onClick = {this.scrollToTop}
>
<span className ="iconfont icon-dingbu"></span>
</div>
}
</ScrollToTopWrapper>
)
}
//控制show的状态从而控制回到顶部按钮的显示和隐藏
changeScrollTopShow() {
if (window.pageYOffset < 400) {
this.setState({
show: false
})
}else {
this.setState({
show: true
})
}
}
//添加动画效果
scrollToTop() {
const scrollToTop = window.setInterval(() => {
let pos = window.pageYOffset;
if ( pos > 0 ) {
window.scrollTo( 0, pos - 20 );
} else {
window.clearInterval( scrollToTop );
}
}, 1);
}
}
//导出组件
export default ScrollToTop;
知识点一:回调函数中this的指向问题
我们可以注意到,上段代码中我们对两个函数changeScrollTopShow和scrollToTop进行了this绑定。我们先分别看一下如果这两个函数不绑定this会是什么样子。
对于changeScrollTopShow函数而言,如果我们不对其绑定this,那么changeScrollTopShow函数内部的this指向的则是调用addEventListener的对象,即windows。这个时候就会出现问题了,因为在changeScrollTopShow函数里我们调用了this.setState方法,而显然windows对象里并没有这个方法,所以程序就会报错。所以我们需要对changeScrollTopShow函数的this进行绑定,绑定为ScrollToTop组件本身,因为setState方法在这个组件对象里面。
再来看看scrollToTop函数。scrollToTop函数是JSX中的回调函数,如果我们不对它的this进行绑定,则scrollToTop函数里的this指向undifined。可能有心的同学已经注意到了,就我们这个例子来说,scrollToTop函数其实是没有必要进行this绑定的,因为在scrollToTop函数内部我们并没有用到this。但是我建议大家在定义的时候就绑定一下,因为在函数没有写完的情况下你往往是不知道函数内部会不会用到this的。
关于如何绑定this,react的官方文档给了三种方法,一种是利用上例中bind方法,还有两种方法是使用箭头函数或使用属性初始化语法器。
以changeScrollTopShow函数为例,使用箭头函数处理this:
componentDidMount() {
window.addEventListener('scroll', (e) => this.changeScrollTopShow(e) )
}
箭头函数之所以能起到绑定this的作用是因为箭头函数体内的this对象就是定义时所在 的对象,而不是使用时所在的对象,而且箭头函数中的this指向是固定的,不会变的。在我们这个例子中,箭头函数是定义在ScrollToTop这个对象中的(注意组件的本质是对象)所以箭头函数体内的this永远都会指向ScrollToTop这个组件。
以changeScrollTopShow函数为例,使用属性初始化语法器处理this:
changeScrollTopShow = () => {
if (window.pageYOffset < 400) {
this.setState({
show: false
})
}else {
this.setState({
show: true
})
}
}
属性初始化语法器是在定义函数时做文章,而调用的时的方法是不变的。
知识点二:生命周期函数
在这个例子中我们用到了两个生命周期函数:componentDidMount() 和componentWillUnmount():
//挂载事件监听
componentDidMount() {
window.addEventListener('scroll', this.changeScrollTopShow)
}
//卸载事件监听
componentWillUnmount() {
window.removeEventListener('scroll', this.changeScrollTopShow)
}
componentDidMount()好理解,当我们的ScrollToTop组件被渲染到DOM后会执行componentDidMount()钩子,所以我们把时间监听函数放在这个钩子上,表示当组件被渲染时开始监听事件。合情合理,这一步是必须的。
但是有的同学可能会有疑问,那在componentWillUnmount()里卸载事件监听函数也是必须的吗?
答案是,是必须的,是必须的,是必须的。
你可以自己实践一下,如果把卸载监听事件那一段注释掉,当ScrollToTop组件被unmount时(通常我们有两种方式会导致一个组件unmount,一是通过条件渲染,二是通过某些路由手段路由到了别的组件。)你就会得到一个警告(Warning):
- Warning: Can only update a mounted or mounting component. This usually means you called setState, replaceState, or forceUpdate on an unmounted component. This is a no-op.
这个Warning的意思是你在一个已经被卸载的组件是上使用了setState, replaceState, or forceUpdate方法。 我们真的使用了吗,回查一下代码我们发现,在changeScrollTopShow这个方法里我们果然使用了setState。因为监听事件被挂载上去以后,我们并没有卸载它,导致ScrollToTop组件被卸载(unmount)时我们的changeScrollTopShow依然在运行,而changeScrollTopShow里又正好调用了setState方法,但是此时组件已经被卸载了,被卸载的组件的state是不能再更新的,所以这个时候会就出现警告。
关于这个警告我有一篇解释的更详细的文章,感兴趣的话请移步。
知识点三:条件渲染
在ScrollToTop这个组件中我们通过show这个变量的状态,即true和false来控制组件的显示和隐藏。实现方法是:
{
// 逻辑与符号左边的show为true时返回右边的html标签
show &&
<div
className = "scrollTop"
onClick = {this.scrollToTop}
>
<span className ="iconfont icon-dingbu"></span>
</div>
}
这里是想提醒一下大家两点:
- 在
JSX中可以使用js,但是记得把js用花括号括起来。 - 逻辑与符号
&&符号的一些扩展用法:逻辑与操作可以用于任何对象,不仅仅是布尔值。在有一个操作数不是布尔值的情况下,逻辑与返回的不一定是布尔值。比如在我们这个例子中,第一个操作数为布尔值,第二个操作数为对象,那么在第一个操作数为true的情况下返回第二个操作数。这样我们就避免了if语法,代码看起来更加整洁。
小结
由于ScrollToTop这个组件不需要用到别的组件的参数,所以复用的时候也不涉及传参的问题,可以直接把自己提供给任何别的组件,不论嵌套有多深,都只需要import一下就好了。真正做到了哪里要用粘哪里。