React Hooks - useState、useReducer

154 阅读3分钟

源码分析

1、useState 的使用

  • useState只是预置了reduceruseReducer

  • useState 的参数

    • 非函数:作为初始化的值
    • 函数:只在初始化时执行一次
    • 计算内容:每次 reander 时都执行、返回值始终为首次执行后的值
  • setState 的调用

    • 更新是异步的、在当前执行上下文中是获取不到最新的 state
    • 传入的值会与上次执行后的值进行 Object.is() 浅比较、来判断是否进行重新 render 组件
    • 参数可以为函数,接受的参数是最新的 state
import React, {useState} from 'react';

// 定义全局变量,只在初始化时加载一次
let num = 0;
// 每次点击 handleClick 都会调用
function getNum() {
    // 虽然 num 每次都会 +1,但是每次执行 getNum() 始终返回 1
    num = num + 1;
    return num;
}
    
const App = () => {
    // useState函数可以是函数、非函数
    // 1、参数为非函数、作为 count 的初始值
    const [count, setCount] = React.useState(0);
    // 2、参数为函数,只在初始化时执行一次
    const [count, setCount] = useState(()=>{ return 0; });
    // 3、参数是一个计算内容,每次组件渲染都会执行 getNum 函数,但是每次返回的都是第一次调用后的值
    const [count2, setCount2] = useState(getNum() * 10);
    // 4、如果初始化时参数为对象 a、set 时传入另一个对象 b(则返回值为b并非 a&b)
    const [person, setPerson] = useState({ name: 'LinDaiDai', sex: 'boy' });


    function changeCount() {
        // 进行 setState 时会使用类似于 `Object.is()` 这样浅比较,而不是 `===`
        // 1、`Object.is(+0, -0)的结果为false`,会重新渲染组件
        // 2、`Object.is(NaN, NaN)的结果为true`;不会重新渲染组件
        // 3、`Object.is({a:'a'}, {a:'a'})的结果为true`;不会重新渲染组件
        setCount(count + 1);
        
        // 多次同步调用 setCount 实际上只会执行一次
        setCount(count + 1);
        setCount(count + 1);
        setCount(count + 1);
        setCount(count + 1);
        setCount(count + 1);
        
        // setCount的更新是异步的、获取的是上次更新后的值
        console.log('我是点击时的count:', count);
    }
    
    function changePerson(){
        // 更新后的 person 值为 { name: "LiMei" }
        setPerson({ name: "LiMei" })
    }

    return (
        <div>
            <p>You clicked {count} times</p>
            <button onClick={changeCount}>
                Click me
            </button>
            <button onClick={changePerson}>
                Click me
            </button>
        </div>
    );
}

export default App;

2、useReducer 的使用

import React, { useReducer } from 'react'
const initialState = {firstCounter: 0}
const reducer = (state:{firstCounter: number}, action:{type: string}) => {
    switch (action.type) {
        case 'increment':
            return {firstCounter: state.firstCounter + 1}
        case 'decrement':
            return {firstCounter: state.firstCounter - 1}
        case 'reset':
            return initialState
        default:
            return state
    }
}

function CounterTwo() {
    // useReducer 接收两个参数,参数1:reducer、参数2:init
    const [count, dispatch] = useReducer(reducer, initialState)
    return (
        <div>
            <div>Count - {count.firstCounter}</div>
            
            <button onClick={() => dispatch({ type: 'increment' })}>
                Increment
            </button>
            
            <button onClick={() => dispatch({ type: 'decrement' })}>
                Decrement
            </button>
            
            <button onClick={() => dispatch({ type: 'reset' })}>
                Reset
            </button>
        </div>
    )
}

export default CounterTwo;

3、类组件 state 的使用

this.setState(stateChange, [callback])
  • 特点:
    • 生命周期、合成事件中是异步批量更新
    • setTimeout、原⽣事件中都是同步更新
  • stateChange:可以为对象、函数
    • 对象: this.setState({ isHot: !isHot })
    • 函数:this.setState( (state, props)=> ({count:state.count+1}))
  • callback:
    • 界面也更新后才被调用(获取更新后的值)
异步、同步更新策略

在 React 的 setState 函数实现中,会根据一个变量 isBatchingUpdates 判断是直接更新 this.state 还是放到队列中延时更新。isBatchingUpdates 默认是 false,表示 setState 会同步更新 this.state,batchedUpdates 函数会把 isBatchingUpdates 修改为 true,而当 React 在调用 合成事件处理函数之前 就会先调用 batchedUpdates 函数。

image.png

  • setState 只在合成事件和钩子函数中是“异步”的,在原生事件和 setTimeout 中都是同步的。
  • setState的“异步”并不是说内部由异步代码实现,其实本身执行的过程和代码都是同步的,只是合成事件和钩子函数的 调用顺序在组件更新之前,导致在合成事件和钩子函数中没法立马拿到更新后的值,形式了所谓的“异步”,当然可以通过第二个参数 setState(partialState, callback) 中的callback拿到更新后的结果
  • setState 的批量更新优化也是 建立在“异步”(合成事件、钩子函数)之上的,在原生事件和setTimeout 中不会批量更新,在“异步”中如果对同一个值进行多次 setState , setState 的批量更新策略会对其进行覆盖,取最后一次的执行,如果是同时 setState 多个不同的值,在更新时会对其进行合并批量更新

4、扩展 useState 的极简实现

<script>
  let isMount = true; // 是否为首次挂载阶段
  let workInProgressHook = null; // 表示正在执行的 hook

  // 极简的 fiber 结构
  const fiber = {
    memoizedState: null, // 保存函数组件的 hooks
    stateNode: App,
  };

  // 创建 update 对象,重新 render
  function dispatchAction(queue, action) {
    // 每个触发更新的动作,都会产生一个update对象
    // 以环状链表的形式存在queue中
    const update = {
      action,
      next: null,
    };


    if (queue.pending === null) {
      // 首次,自己指向自己
      update.next = update;
    } else {
      // 中插入一个新的update
      // 例如:3 -> 0 -> 1 -> 2 -> 3  ===>  4 -> 0 -> 1 -> 2 -> 3 -> 4
      // 1、update 表示当前要插入的 update 4
      // 1、因为 update.pending 表示最后一个 update 3
      // 2、所以 update.pending.next 表示了第一个 update 0
      // 3、第一步 让 4 要指向 0, 第二步 让 3 指向 4
      update.next = queue.pending.next;
      queue.pending.next = update;
    }

    // queue.pending 指向最新值
    queue.pending = update;

    // 重新 render
    run();
  }

  // 1、创建一个 hook
  // 2、以单链表形式存在
  // 3、返回[baseState, dispatchAction]
  function useState(initialState) {
    let hook;

    if (isMount) {
      // 挂在过程
      hook = {
        queue: {
          pending: null, // 存储 update
        },
        memoizedState: initialState,
        next: null, // 同时执行多个 useState 会以链表形式保存
      };

      if (!fiber.memoizedState) {
        fiber.memoizedState = hook;
      } else {
        workInProgressHook.next = hook;
      }
      workInProgressHook = hook;
    } else {
      // 更新过程
      hook = workInProgressHook;
      workInProgressHook = workInProgressHook.next;
    }

    // 计算state
    let baseState = hook.memoizedState;
    if (hook.queue.pending) {
      // 更新阶段存在 update 连接的环状链表
      // hook.queue.pending:指向末尾 update
      // hook.queue.pending.next:指向第一个 update
      let firstUpdate = hook.queue.pending.next;

      do {
        const action = firstUpdate.action;
        baseState = action(baseState);
        firstUpdate = firstUpdate.next;
      } while (firstUpdate !== hook.queue.pending.next);

      // 指向完,重置 pending
      hook.queue.pending = null;
    }

    hook.memoizedState = baseState;
    return [baseState, dispatchAction.bind(null, hook.queue)];
  }

  function App() {
    // 1、新的 state 由 baseState 和 update 计算得出
    // 2、调用 updateNum 产生新的 update
    const [num, updateNum] = useState(0);
    const [status, triggerStatus] = useState(false);

    console.log("num", num);
    console.log("status", status);

    return {
      onClick() {
        // 同一个setState执行多次,会以链表形式挂在到 hook.queue 上
        // updateNum(num=>num + 1)
        // updateNum(num=>num + 1)
        updateNum((num) => num + 1);
      },
      trigger() {
        triggerStatus((status) => !status);
      },
    };
  }
  
  // 入口函数
  function run() {
    workInProgressHook = fiber.memoizedState;
    // 模拟render阶段
    const app = fiber.stateNode();
    isMount = false;
    return app;
  }

  window.app = run();
</script>