React: Hooks原理解析

最简单的 useState 实现

useState 原理

import React from "react";
import ReactDOM from "react-dom";
const rootElement = document.getElementById("root");

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);

自己试试 - CodeSandbox

你以为的n一开始是0,然后通过setN改变n,这是一般人看这个代码脑补的过程,但是,这个过程是错误的。

这里是实际的情况:

脑补之后

问自己几个问题

  • 执行setN的时候会发生什么?n会变吗?App() 会重新执行吗?

要重新渲染UI,因为把n变了。n不会变,而是说我要把n变了,这个时候n是不变的,也就是说setN并不会改变nApp() 当然会重新执行

  • 如果 App() 会重新执行,那么 useState(0) 的时候,n每次的值会有不同吗?

通过console.log你会发现,n每次的值会有不同 : 自己试试

分析

setN

  • setN一定会修改数据x,将n+1存入x(而不是直接把他复制给n)
  • setN一定会触发****重新渲染(re-render)

useState

  • useState肯定会从x读取n的最新值

x

  • 每个组件有自己的数据x,我们将其命名为state

尝试实现 React.useState

import React from "react";
import ReactDOM from "react-dom";
const rootElement = document.getElementById("root");

function myUseState(initialValue) {
  var state = initialValue;
  function setState(newState) {
    state = newState;
    render();
  }
  return [state, setState];
}

// 不用在意 render 的实现
const render = () => ReactDOM.render(<App />, rootElement);

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);

自己试试

参考资料: React Hooks 原理

完全没有变化啊

  • 因为myUseState会将state重置
  • 我们需要一个不会被myUseState重置的变量
  • 那么这个变量只要声明在myUseState外面即可

再尝试实现 React.useState

import React from "react";
import ReactDOM from "react-dom";
const rootElement = document.getElementById("root");

let _state;

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

// 不用在意 render 的实现
const render = () => ReactDOM.render(<App />, rootElement);

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 不冲突

Q: 看了上面的步骤, useState就这么简单?

A: 别急,还有问题

Q: 如果一个组件用了两个 useState怎么办?

A: 由于所有数据都放在**_state**,所以会冲突

改进思路

把 _state 做成一个对象

  • 比如_state = {n: 0, m: 0)}
  • 不行,因为useState(0)并不知道变量叫n还是m

把 _state 做成数组

  • 比如_state = [0, 0]
  • 貌似可行,我们来试试看

多个 useState

import React from "react";
import ReactDOM from "react-dom";
const rootElement = document.getElementById("root");

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);

运行看看,注意 index , currentIndex第 21 行

_state 数组方案缺点

useState 调用顺序

  • 若第一次渲染时n是第一个,m是第二个,k是第三个
  • 则第二次渲染时必须保证顺序完全一致
  • 所以React不允许出现下面的代码

不允许出现的代码

点我看看错误代码

现在的代码还有一个问题

  • App 用了 _stateindex ,那其他组件用什么?

    解决办法: 给每个组件创建一个 _stateindex

  • 又有问题: 放在全局作用域里重名了咋整?

    解决办法: 放在组件对应的虚拟节点对象上

图示

上图只画了 App 组件的更新过程,两个 ChildA 组件也有相同的过程

总结

  • 每个函数组件对应一个 React 节点
  • 每个节点保存着 stateindex
  • useState 会读取 state[index]
  • indexuseState 出现的顺序决定
  • setState 会修改 state,并触发更新

    这里对 React 的实现做了简化,React 节点应该是 FiberNode, _state 的真实名称为 memorizedStateindex 的实现则用到了链表,有兴趣的可以看这里

useRef

n 的分身

import React from "react";
import ReactDOM from "react-dom";
const rootElement = document.getElementById("root");

function 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出了旧数据?

setN不会改变n

因为有多个 n

贯穿始终的状态

Q: 看了这么多,如果我就希望有一个贯穿始终的状态,怎么做?

A: 办法很多

全局变量

  • window.xxx 即可,但太 low 了

useRef

useContext

总结

  • 每次重新渲染,组件函数就会执行
  • 对应的所有 state 都会出现分身
  • 如果你不希望出现分身, 可以用 useRefuseContext
comments powered by Disqus