React-Hooks原理解析

1,163 阅读3分钟

useState原理和源码

useState的运行过程

function App() {
  const [n, setN] = React.useState(0);
  return (
    <div className="App">
      <p>{n}</p>
      <p>
        <button onClick={() => setN(n + 1)}>+1</button>
      </p>
    </div>
  );
}

ReactDOM.render(<App />, rootElement);

运行过程:

  1. 首次渲染 render<App/>
  2. 调用App(),得到虚拟Div对象, 创建真实DIV
  3. 当用户点击button时,调用setN(n+1),再次render<App/>

分析

  • setN一定会修改数据x, 将n+1存入x
  • setN一定会触发<App/>,进行重新渲染
  • useState肯定会从x读取n的最新值
  • 每个数据都有自己的数据x, 我们将其命名为state

手动实现React.useState

let _state;

// 类似render原理实现
const render = () => ReactDOM.render(<App />, rootElement);

function myUseState(initialValue) {
  _state = _state === undefined ? intialValue : _state 
  function setState(newState) {
    _state = newState;
    render();
  }
  return [_state, setState];
}

function App() {
  const [n, setN] = myUseState(0);
  return (
    <div className="App">
      <p>{n}</p>
      <p>
        <button onClick={() => setN(n + 1)}>+1</button>
      </p>
    </div>
  );
}

ReactDOM.render(<App />, rootElement);

实现两个useState

改进思路

  1. 试着把_state做成一个对象,比如_state= {n:0, m :0}。但是这样做的话,useState(0)就不知道变量叫n还是m
  2. 试着把_state做成数组,比如_state= [0,0], 这种方法似乎可行

代码实例

let _state = [];
let index = 0;

function myUseState(initialValue) {
  const currentIndex = index;
  index += 1;
  _state[currentIndex] = _state[currentIndex] || initialValue;
  const setState = newState => {
    _state[currentIndex] = newState;
    render();
  };
  return [_state[currentIndex], setState];
}

// 类似render原理实现
const render = () => {
  index = 0;
  ReactDOM.render(<App />, rootElement);
};

function App() {
  const [n, setN] = myUseState(0);
  const [m, setM] = myUseState(0);
  console.log(_state);
  return (
    <div className="App">
      <p>{n}</p>
      <p>
        <button onClick={() => setN(n + 1)}>+1</button>
      </p>
      <p>{m}</p>
      <p>
        <button onClick={() => setM(m + 1)}>+1</button>
      </p>
    </div>
  );
}

ReactDOM.render(<App />, rootElement);

_state数组方案的缺点:

  • useState调用顺序。如果第一次渲染是n是第一个,m是第二个,k是第三个。则要求第二次渲染时必须保障顺序一致。也就是不能使用if...else打乱顺序。
  • App用了_sateindex,其他组件用什么? 解决方法: 给每个组件创建一个_stateindex
  • _stateindex放在全局作用域重名了怎么办? 解决方法:放在组件对应的虚拟节点对象上

小结

  • 每个函数组件对应一个React节点, 即FiberNode
  • 每个节点保存着stateindex, statememorizedState, index的实现使用了链表结构
  • useState会读取state[index]
  • indexuseState调用的顺序决定
  • setState会修改state,并触犯更新

useRef和useContext

代码实例

unction App() {
  const [n, setN] = React.useState(0);
  const log = () => setTimeout(() => console.log(`n: ${n}`), 3000);
  return (
    <div className="App">
      <p>{n}</p>
      <p>
        <button onClick={() => setN(n + 1)}>+1</button>
        <button onClick={log}>log</button>
      </p>
    </div>
  );
}

ReactDOM.render(<App />, rootElement);

上面的代码出现了问题

  • 点击+1再点击log 没有bug
  • 点击log再点击+1,出现bug,为什么log打印的是上一次的数据

疑惑解答: 因为有多个n

改进思路: 希望有一个贯穿始终的状态

  • 使用全局变量,比如window.xxx
  • 使用useRefuseRef不仅可以用于div,还能用于任意数据。但是useRef不会在属性变动时自动触发更新,只能手动设置更新,但是不推荐使用手动更新
function App() {
  const nRef = React.useRef(0);
  const log = () => setTimeout(() => console.log(`n: ${nRef.current}`), 1000);
  return (
    <div className="App">
      <p>{nRef.current} 这里并不能实时更新</p>
      <p>
        <button onClick={() => (nRef.current += 1)}>+1</button>
        <button onClick={log}>log</button>
      </p>
    </div>
  );
}

ReactDOM.render(<App />, rootElement);
  • 使用useContext, useContext不仅可以贯穿始终,还可以贯穿不同组件
function App() {
  const [theme, setTheme] = React.useState("red");
  return (
    <themeContext.Provider value={{ theme, setTheme }}>
      <div className={`App ${theme}`}>
        <p>{theme}</p>
        <div>
          <ChildA />
        </div>
        <div>
          <ChildB />
        </div>
      </div>
    </themeContext.Provider>
  );
}

function ChildA() {
  const { setTheme } = React.useContext(themeContext);
  return (
    <div>
      <button onClick={() => setTheme("red")}>red</button>
    </div>
  );
}

function ChildB() {
  const { setTheme } = React.useContext(themeContext);
  return (
    <div>
      <button onClick={() => setTheme("blue")}>blue</button>
    </div>
  );
}

ReactDOM.render(<App />, rootElement);

小结

  • 每次重新渲染,函数组件就会执行
  • 函数组件对应的所有state就会被重新复制
  • 如果不想出现复制的state,可以使用useRef,或者useContext解决

更多信息

阅读源码后,来讲讲React Hooks是怎么实现的