react hooks api 踩坑經歷(useEffect的閉包陷阱和useRef)

筆者最近用時下正新鮮的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

知道了緣由,接下來有兩種可能的解決方式:閉包

  1. 設法在wheelListener取到currPage即時的值,由於沒有了class語法中的this引用這條路看來是走不通了
  2. 每次render後更新wheelListener並刪除舊的。

這同時也解決了我以前的一個疑惑:'每次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 仍是很香的:函數

  1. 函數式組件看起來清爽多了。
  2. 對比class形式 render-vm-state 的結構,hooks api 把結構變成了更簡潔的 render-states, 其中的state,react還幫你代勞了,你只需給它初始值便可。
  3. 自定義hooks能夠複用一些在class語法下難以複用的邏輯(通常是由於this指向問題)。
  4. 函數式組件結合函數式編程有了更多的想象空間(結合compose、柯里化什麼的)。
相關文章
相關標籤/搜索