美文网首页
useState和useReducer研究

useState和useReducer研究

作者: 木匠_说说而已 | 来源:发表于2019-05-30 17:18 被阅读0次

    准备

    开发环境,在Chrome浏览器下,以及安装了React DevTools情况下,App组件总会render两次(原因不详,依稀记得之前有看过React新版在开发环境会主动调用组件两次,帮助排查生命周期一些不易发现的bug,以及为后面的大升级铺路),并且不光是进入页面,发生事件改变状态也类似。


    App组件render两次

    但不利于此文的调试,所以请打开Chrome无痕模式进行调试。

    需求

    money = price * num

    使用useState

    setXxx是异步的

    function App() {
      const [price, setPrice] = useState(0);
      const [num, setNum] = useState(0);
      const [money, setMoney] = useState(0);
    
      function changePrice() {
        setPrice(8);
        setMoney(price * num);
      }
    
      function changeNum() {
        setNum(2);
        setMoney(price * num);
      }
    
      function changePriceAndNum() {
        setPrice(6.6);
        setNum(3);
        setMoney(price * num);
      }
      
      return (
        <div id="App" className="App">
          <p>Price: {price}<button onClick={changePrice}>change price</button></p>
          <p>Num: {num}<button onClick={changeNum}>change num</button></p>
          <p>Money: {money}</p>
          <button onClick={changePriceAndNum}>change price and num</button>
        </div>
      );
    }
    

    代码看起来很美好,但是很不幸,这段代码都不能工作,当点击“change price and num”时候,结果是这样:


    结果并不正确

    因为setPrice和setNum都是异步的,changePriceAndNum函数执行时候,取得的price和num还是当前状态,并不是setPrice和setNum之后的状态。
    当再次点击按钮,结果就正确了。

    进行改造

    function App() {
      const [price, setPrice] = useState(0);
      const [num, setNum] = useState(0);
      const [money, setMoney] = useState(0);
    
      function changePrice() {
        const newPrice = 8; // 设置价格变量
        setPrice(newPrice);
        setMoney(newPrice * num);
      }
    
      function changeNum() {
        const newNum = 2;  // 设置数量变量
        setNum(newNum);
        setMoney(price * newNum);
      }
    
      function changePriceAndNum() {
        const newPrice = 6.6, newNum = 3;  // 设置价格和数量变量
        setPrice(newPrice);
        setNum(newNum);
        setMoney(newPrice * newNum);
      }
      
      return (
        <div id="App" className="App">
          <p>Price: {price}<button onClick={changePrice}>change price</button></p>
          <p>Num: {num}<button onClick={changeNum}>change num</button></p>
          <p>Money: {money}</p>
          <button onClick={changePriceAndNum}>change price and num</button>
        </div>
      );
    }
    
    解决useState的bug

    好了,这下正常工作了,因为把值和状态解耦了,不存在异步设置状态而获取不到的问题了,但是:

    • 代码不够直观,比较复杂
    • 上面代码,一个事件只是在一个函数体里执行,可以这么定义变量。这在真实业务中是不可能的,多层函数调用,想按这思路解决,就得当参数一层层传过去。
    • 逻辑无法从组件拆分出来

    使用useReducer

    我们使用useReducer改写这个代码

    const initialState = {
      price: 0,
      num: 0,
      money: 0,
    };
    
    function myReducer(state, action) {
      console.log(JSON.parse(JSON.stringify(state)))
      switch(action.type) {
        case 'changePrice':
          return {
            ...state,
            price: action.price,
            money: action.price * state.num
          }
        case 'changeNum':
          return {
            ...state,
            num: action.num,
            money: state.price * action.num
          }
        default:
          throw new Error();
      }
    }
    
    function App() {
      const [state, dispatch] = useReducer(myReducer, initialState);
      
      return (
        <div id="App" className="App">
          <p>Price: {state.price}<button onClick={() => dispatch({type:'changePrice', price: 8})}>change price</button></p>
          <p>Num: {state.num}<button onClick={() => dispatch({type:'changeNum', num: 2})}>change num</button></p>
          <p>Money: {state.money}</p>
          <button onClick={() => {
              dispatch({type:'changePrice', price: 6.6});
              dispatch({type:'changeNum', num: 3});
            }}>change price and num</button>
        </div>
      );
    }
    

    对于复杂逻辑,使用useReducer改造代码后,不用考虑状态异步的问题,性能也好。相关的业务逻辑也可以拆分到单独文件去了,使组件更干净。

    奇怪的事情

    1 useState 状态不改变,组件函数依然执行

    function App() {
      console.log("App render -->"+ performance.now())
      const [num, setNum] = useState(0);
      function changeNum() {
        setNum(8);
      }
      return (
        <div id="App" className="App">
            num: {num}
          <button onClick={changeNum}>change num</button>
        </div>
      );
    }
    
    1. num初始为0
    2. 点击按钮,num变成8,打印了App render -->,这是肯定的
    3. 再次点击按钮,num还是8,没变,但仍然打印了App render-->,不好理解
    4. 再次点击按钮,就不会打印App render -->了
      查询了一下,没有查到特别精确的解释,感觉跟下面这个issue可能类似。而且步骤2会真真切切会重新渲染,步骤3只是执行一下App函数,并不会重新渲染App组件。并且如果有子组件,子组件函数也不会在步骤3执行。所以不用太担心性能(所以一个相对解决办法是把复杂代码写子组件去)。
      useState not bailing out when state does not change #14994

    2 useState将状态改变写在setTimeout里,一组操作会触发多次组件函数执行

    此情况是我在hashChange事件中发现的现象,然后发现setTimeout行为一致。可以猜想,异步调用譬如获取数据应该都是类似机制

    function App() {
      console.log("App render -->"+ performance.now())
      const [a, setA] = useState('A');
      const [b, setB] = useState('B');
      const [c, setC] = useState('C');
      function changeState() {
        console.log('begin')
        setA('AA');
        console.log('a end')
        setB('BB');
        console.log('b end')
        setC('CC');
        console.log('c end')
      }
      return (
        <div id="App" className="App">
            a: {a}, b: {b}, c: {c}
          <button onClick={changeState}>change state</button>
        </div>
      );
    }
    

    点击按钮的时候,改变了三个状态,只会触发一次App渲染:


    普通调用

    但是如果将代码包在setTimeout里,就会触发三次App渲染(真真切切的三次渲染,不是像上面那样执行一下App函数而已):

    function App() {
      console.log("App render -->"+ performance.now())
      const [a, setA] = useState('A');
      const [b, setB] = useState('B');
      const [c, setC] = useState('C');
      function changeState() {
        setTimeout(() => {
          console.log('begin')
          setA('AA');
          console.log('a end')
          setB('BB');
          console.log('b end')
          setC('CC');
          console.log('c end')
        })
      }
      return (
        <div id="App" className="App">
            a: {a}, b: {b}, c: {c}
          <button onClick={changeState}>change state</button>
        </div>
      );
    }
    
    setTimeout调用

    不知道对于异步代码,做了什么样的处理,虽然是setTimeout异步执行,但是这三行代码是同步的啊。

    相关文章

      网友评论

          本文标题:useState和useReducer研究

          本文链接:https://www.haomeiwen.com/subject/fyemtctx.html