本文详解在React组件化开发中,如何通过函数节流优化onScroll滚动事件的性能,并结合Redux将滚动状态同步至子组件。重点解析了节流函数的时间戳控制逻辑,以及在componentWillReceiveProps中正确接收props变化以更新高亮样式的关键步骤,避免无效渲染。
问题背景与解决方案概述
问题描述
常见的网站布局通常包含顶部导航栏,假设页面分为A、B、C、D四个栏目。当点击导航栏的A按钮时,页面锚点跳转至A栏目,同时导航栏上的A按钮显示高亮状态;同理,点击B按钮则跳转至B栏目并高亮B按钮。此外,当用户在Main组件区域内滚动页面至B模块时,导航栏上的B按钮也应自动高亮。这是前端开发中常见的交互模型,过去使用jQuery实现时较为熟悉。
解决方案
本文主要探讨在React组件化开发背景下,针对滚动事件的性能优化方法。
页面结构与事件绑定
页面结构如下所示:
<div
className={style.main}
id="main"
ref={(main) => { this.main = main; }}
onScroll={
((/detail/.test(this.props.location.pathname))) ? (() => this.throttle()()) : null
}
>
{this.props.children}
<Footer />
在Main组件中绑定onScroll事件,该事件触发相应的action,并通过Redux将状态变化传递给子组件。
具体的scroll事件触发函数如下(忽略冗长的if-else逻辑,这是解决一个耗时一下午的Bug的最终方案,此处不作详细展开):
handleScroll() {
const { changeScrollFlag } = this.props.actions;
// 根据滚动距离修改TitleBox的样式
const { basicinformation, holderinformation, mainpeople, changerecord } = {
basicinformation: document.getElementById('basicinformation').offsetTop - 121,
holderinformation: document.getElementById('holderinformation').offsetTop - 121,
mainpeople: document.getElementById('mainpeople').offsetTop - 121,
changerecord: document.getElementById('changerecord').offsetTop - 121,
};
if (window.screen.availHeight > this.main.scrollTop) {
document.getElementById('gototop').style.display = 'none';
} else {
document.getElementById('gototop').style.display = 'block';
}
// 得到基础信息区域、股东信息区域、主要人员区域、变更记录区域的offsetTop,我们把它用来跟main的scrollTop比较
// 比较的结果触发action,改变TitleBox组件样式
if (this.main.scrollTop < holderinformation) {
// 基础信息区域
if (basicinformation === -121) {
// 如果基础信息模块不存在,我们什么也不做(当然理论上基础信息模块应该是会有的)
return;
}
changeScrollFlag(1);
return;
} else if (this.main.scrollTop < mainpeople) {
// 股东信息区域
changeScrollFlag(2);
if (holderinformation === -121) {
// 如果股东信息栏目不存在,在滚动的时候我们不应该强行把TileBox的高亮按钮设置为holderinformation
// 因为holdinformation并不存在,我们跳到前一个按钮,让基础信息按钮高亮
changeScrollFlag(1);
return;
}
return;
} else if (this.main.scrollTop < changerecord) {
// 主要人员区域
changeScrollFlag(3);
if (mainpeople === -121) {
// 如果主要人员栏目不存在,在滚动的时候我们不应该强行把TileBox的高亮按钮设置为mainpeople
// mainpeople并不存在,我们跳到前一个按钮,让基础信息按钮高亮
changeScrollFlag(2);
if (holderinformation === -121) {
// 如果主要人员栏目不存在,而且连股东信息栏目也没有,我们跳到高亮基础信息栏目
changeScrollFlag(1);
return;
}
return;
}
return;
} else if (this.main.scrollTop > changerecord) {
// 与上面同理
// 变更记录区域
changeScrollFlag(4);
if (changerecord === -121) {
changeScrollFlag(3);
if (mainpeople === -121) {
changeScrollFlag(2);
if (holderinformation === -121) {
changeScrollFlag(1);
return;
}
return;
}
return;
}
return;
}
}
其中,changeScrollFlag()函数负责处理action。
函数节流实现机制
接下来介绍函数节流的具体实现。
throttle() {
// onScroll函数节流
let previous = 0;
// previous初始设置上一次调用 onScroll 函数时间点为 0。
let timeout;
const wait = 250;
// 250毫秒触发一次
return () => {
const now = Date.now();
const remaining = wait - (now - previous);
if (remaining <= 0) {
if (timeout) {
window.clearTimeout(timeout);
}
previous = now;
timeout = null;
this.handleScroll();
} else if (!timeout) {
timeout = window.setTimeout(this.handleScroll, wait);
}
};
}
我们的节流函数返回一个新的函数,该函数设定一个时间戳。如果当前时间与上次调用时间的差值较小,则不执行任何操作;如果差值较大,则清除定时器并触发scroll函数。从逻辑上看,这似乎相当简单。
子组件状态同步与生命周期
那么在子组件中还需要进行哪些操作呢?
子组件需要接收action。
二级容器型组件接收action,并通过传递props至三级展示型组件。
必须在componentWillReceiveProps生命周期中接收这个props。
需要注意的是,在componentWillReceiveProps中使用this.props并不能直接接收到props的变化!!!组件生命周期函数包含一个自己的参数。
componentWillReceiveProps(nextProps) {
// 在compoWillReceiveProps里接收到Main组件里所触发onScroll事件的改变activebtn样式的index
// 并且设置为本组件的state
this.setState({
activebtn: nextProps.scrollFlag.scrollIndex,
});
}
我们的state用于控制高亮按钮的索引,它是一个数字。
接下来更改导航条的样式。
在此处,我使用了React生态库classnames,具体用法可参考其API文档。
<span
className={classnames({
[style.informationactive]: (this.state.activebtn === 1),
})}
onClick={() => this.handleClick(1, 'basicinformation')}
>
开发建议与总结
至此,我们完成了一次从顶层组件触发事件,经过函数节流处理,并将事件层层传递至底层展示型组件的全过程。 近期关于前端开发的一些思考
- 避免在组件中反复调用同一个函数,这会造成巨大的性能消耗!如果可以通过三元运算符或模板字符串实现的功能,请勿编写新的函数。
- JSX代码不应过于冗余。尽量将页面结构写成变量形式,否则页面结构复杂,不利于排查Bug。
- 减少后端请求,能存储Cookie则存储Cookie,能使用localStorage则使用localStorage。
- 简单的组件尽量自行编写,请勿随意使用第三方组件,否则在需求变更或样式调整时会遇到巨大困难,并导致大量无意义的工作。
总结
以上就是本文的全部内容,希望本文的内容对大家的学习或工作能带来一定的帮助。如果有疑问,欢迎留言交流。