筆者最近用時下正新鮮的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 仍是很香的:函數