React: useState是怎么回事?

335 阅读1分钟

useState实现

最简单的useState

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

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

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

  1. 首次渲染render
  2. 调用App,得到虚拟DIV,根据虚拟div创建真实div
  3. 用户点击Button,调用setN(n+1),再次render
  4. 调用App(),得到虚拟DIV,DOM Diff更新真实div
  5. 每次调用App(),都会运行useState()
  6. 每次点击button,都会打印run
  7. 每次点击button,打印出来的n都变化,那么他是怎么做到运行同样的代码,打印出来的东西不同的呢?

问几个问题

  1. 执行setN的时候发生了什么?n会变吗?app会重新执行吗?
  • n不会变
  • app会重新执行
  • 通过console.log得到,每次运行n的值不同
  1. 如果App()会重新执行?
  • n每次的值不同

分析几个问题

  1. setN
    • setN一定会修改数据x,将n+1存入x
    • setN一定会触发重新渲染(re-render)
  2. useState
    • useState会从x读取n的最新值
  3. x
    • 每个数组都有自己的数据x,我们将其命名为state

尝试实现React.useState

codesandbox.io/s/nostalgic…

  • let _state; // 定义在外面
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);

如果一个组件用了2个useState怎么办?

改进思路:

  1. 把_state设计为一个对象
    • 比如_state={n:0,m:1}
    • 不行,因为useState(0),并不知道变量是n还是m
  2. 把_state设计为一个数组
    • 比如_state = [0,0]
    • 貌似可以
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 => {
    console.log(currentIndex)
    _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数组方案缺点

  1. useState调用顺序
  • 若第一次渲染时n是第一个,m是第二个,k是第三个
  • 则第二次渲染时必须保证顺序完全一致
  • 所以react不允许出现在if for里
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] = React.useState(0);
  let m, setM;
  if (n % 2 === 1) {
    [m, setM] = React.useState(0);
  }
  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);

RhKHhV.png

总结

  1. 每个函数组件对应一个react节点(FiberNode)
  2. 每个节点保存着state(memorizedState)和index
  3. useState会读取state[index]
  4. index由useState出现顺序决定
  5. setState会修改state,并处触发更新

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. 先+1再log:1,1(无bug)
  2. 先log再+1:0,1(bug)
  3. 为什么第二种 情况会log出旧的n
  4. RhMUNq.png
  • 每次都有一个新的n
  1. 现在我就想有一个贯穿始终的状态,应该怎么做?
  • 全局变量
    • window.xxx
  • useRef
    • useRef不仅可以用来引用div,还可以用于任何数据
    • 例子见下文
    • 强制更新的例子见下文
  • useContext
    • useContext不仅能贯穿始终,还能贯穿不同组件
    • useContext例子

useRef

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

function App() {
  const nRef = React.useRef(0); // {current: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);
  • 这是不管先log后log,都是1.因为只有一个n
  • 发现这是展示出的n不能实时更新,因为使用useRef不会让app重新渲染
  • RhleYt.png
  • 如何强制更新呢?不推荐
import React from "react";
import ReactDOM from "react-dom";
const rootElement = document.getElementById("root");

function App() {
  const nRef = React.useRef(0);
  const update = React.useState()[1];
  const log = () => setTimeout(() => console.log(`n: ${nRef.current}`), 1000);
  return (
    <div className="App">
      <p>{nRef.current} 这里并不能实时更新</p>
      <p>
        <button onClick={() => ((nRef.current += 1), update(nRef.current))}>
          +1
        </button>
        <button onClick={log}>log</button>
      </p>
    </div>
  );
}

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

useContext

import React from "react";
import ReactDOM from "react-dom";
import "./styles.css";
const rootElement = document.getElementById("root");
// 1. 初始化上下文
const themeContext = React.createContext(null);

function App() {
  const [theme, setTheme] = React.useState("red");
  return (
    // 2. context作用域,不管多深都可以
    <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);

  1. 初始化上下文
const themeContext = React.createContext(null);
  1. provider: 上下文的作用域从这里开始,这个范围里的,不管多深,都可以用
  2. 子组件使用
  const { setTheme } = React.useContext(themeContext);

总结

  1. 每次中心渲染,组件函数就会执行
  2. 对应的所有state都会出现分身
  3. 如果不希望出现分身
  4. 使用useRef,useContext