笔者最近用时下正新鲜的hooks api重构了本身的项目,其中踩到了一些坑在此与你们分享一下。html
考虑这样的场景:网页内容分为数页(每页fixed定位),鼠标滚轮触发翻页,相似ppt的感受,咱们用currPage这个状态来记录当前页码,而后在componentDidMount中挂上滚轮事件。class实现的代码以下:react
class App extends React.Component{
state = {
currentPage: 0,
}
/** @description 用于防抖的实例私有字段 */
flipSwitch = true;
filpPage = (event) => {
if (this.flipSwitch) {
if(event.deltaY > 0){
this.setState({currentPage: (this.state.currentPage + 1) % 3});
}
if(event.deltaY < 0){
if(this.state.currentPage > 0){
this.setState({currentPage: this.state.currentPage - 1});
}
}
this.flipSwitch = false;
setTimeout(() => {this.flipSwitch = true;}, 1000);
}
}
componentDidMount(){
window.addEventListener('wheel', this.filpPage);
}
componentWillUnmount(){
window.removeEventListener('wheel', this.filpPage);
}
render(){
const classNames = ['unvisible','unvisible','unvisible'];
classNames[this.state.currentPage] = 'currPage';
return (
<div>
<CoverPage className={classNames[0]} />
<ProjectPage className={classNames[1]} />
<SkillsPage className={classNames[2]} />
</div>
)
}
}
复制代码
这里的重构用到两个hook:useState,useEffect。useState相信你们都了解了(不了解的戳这里:hooks官方文档),这里我说一说useEffect的用法:首先它对标的是class中的各个生命周期,接收的第一个参数是一个回调函数effectCallback,这个回调是这样的形式:() => (void | (() => void | undefined))
,若是useEffect没有第二个参数,effectCallback会在每次render(或者组件函数被调用)后被调用,至关于componentDidMount+componentDidupdate,值得注意的是effectCallback每每还会return一个函数,它的角色相似componentWillUnmount,会在下一次render前或销毁组件前运行。那么这里我只须要'像componentDidMount那样在组件第一次render后运行一次而不是每次render后都运行'要如何实现呢?这就须要传给useEffect第二个参数-一个数组了,它的角色有点相似于componentShouldUpdate,通常来讲它由state和props中的数据构成,每次render,useEffect会判断这个数组与上一次render是否彻底一致,若是彻底一致effectCallback就不会进入事件队列并运行了,想要实现componentDidmount的效果只需传一个空数组,这样数组每次都彻底一致了。编程
此外还有一个问题:函数防抖的变量flipSwitch放哪儿呢?答案是useRef。从命名上看它对标的是ref,也就是用来在render中收集dom元素,不过官方文档上已经说明了它的做用不止于此:**However, useRef() is useful for more than the ref attribute. It’s handy for keeping any mutable value around similar to how you’d use instance fields in classes.**它的使用方法与useState没太大的差异,惟一要注意的是它不会直接把值return给你,而是以{current:value}这样的形式。 here is code:api
function App () {
const [currPage, setCurrPage] = useState(0)
const classNames = Array(pageCount).fill('unvisible');
const flipSwitch = useRef(true)
classNames[currPage] = 'currPage';
useEffect(() => {
console.log('add listenner!')
const wheelListener = event => {
if (flipSwitch.current) {
if(event.deltaY > 0){
setCurrPage((currPage + 1) % pageCount);
}
if(event.deltaY < 0){
if(currPage > 0){
setCurrPage(currPage - 1)
}
}
flipSwitch.current = false;
setTimeout(() => {flipSwitch.current = true;}, 1000);
}
};
window.addEventListener('wheel', wheelListener);
// 在下次更新 listener 前运行return的函数
return () => {console.log('rm listenner!'),window.removeEventListener('wheel', wheelListener)}
},[]);
return (
<div>
<CoverPage className={classNames[0]} />
<ProjectPage className={classNames[1]} projects={projects} />
<SkillsPage className={classNames[2]} skills={skills} />
</div>
)
}
复制代码
重点来了,程序一跑就会发现翻页只能从第一页翻到第二页。ok,来看看哪里出了问题。wheelListener每次滚轮都有触发,那么问题极可能出在currPage这个变量上了,果真,经过打印currPage咱们会发现wheelListener每次触发它的值都同样是0。数组
原来wheelListener中除了event之外的变量都是第一次render中声明函数时经过闭包获得的,天然除了event之外的值一直都是第一次render时的值。bash
知道了缘由,接下来有两种可能的解决方式:闭包
这同时也解决了我以前的一个疑惑:'每次render都会声明一个新的回调函数给useEffect,这是否是很浪费?',看来这极可能就是官方设计api时设想好的用法,给useEffect第二个参数也省了。dom
那么,把useEffect的第二个参数去掉,或者为了更贴合使用场景把它设置为[currPage]
,如今它能够正常运行了:函数式编程
function App () {
const [currPage, setCurrPage] = useState(0)
const classNames = Array(3).fill('unvisible');
const flipSwitch = useRef(true)
classNames[currPage] = 'currPage';
useEffect(() => {
console.log('add listenner!')
const wheelListener = event => {
if (flipSwitch.current) {
if(event.deltaY > 0){
setCurrPage((currPage + 1) % 3);
}
if(event.deltaY < 0){
if(currPage > 0){
setCurrPage(currPage - 1)
}
}
flipSwitch.current = false;
setTimeout(() => {flipSwitch.current = true;}, 1000);
}
};
window.addEventListener('wheel', wheelListener);
// 在下次更新 callback 前运行return的函数
return () => {console.log('rm listenner!'),window.removeEventListener('wheel', wheelListener)}
}, [currPage]);
return (
<div>
<CoverPage className={classNames[0]} />
<ProjectPage className={classNames[1]} />
<SkillsPage className={classNames[2]} />
</div>
)
}
复制代码
PS: hooks api 仍是很香的:函数