React的useState原理

211 阅读4分钟

React的useState原理

useState的用法

function App (){
    const [n,setN] = React.State(0)
    return(
    	<div>
            {n}
        <button onClick={()=>setN(N+1)}>+1</button>
        </div>
    )
}

点击button发生什么

1649990206540.png

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

会重新执行吗

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

function App (){
    const [n,setN] = React.State(0)
    return(
    	<div>
            {n}
        <button onClick={()=>setN(N+1)}>+1</button>
         <button onClick={()=>{console.log(N)}}>+1</button>
        </div>
    )
}

分析

setN一定会修改数据X,将n+1存入x

useN一定会触发重新渲染(re-render)

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

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

尝试实现(React.useState)

import React from "react";
import ReactDOM from "react-dom";

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

const render = ()=>{
    ReactDOM.render(
        <App/>,
        document.getElementById('root')
    );
}

const App = (PROPS)=>{
    const [n,setN] = useState(0)
    return(
        <div>
            <p>{n}</p>
            <button onClick={()=>setN(n+1)}>+1</button>
            <button onClick={()=>{console.log(n)}}>logn</button>
        </div>
    )
}
export default App

结果

完全没有变化,,因为usetState会将State重置,需要一个不会被usetState重置的变量,声明在useState外边即可

再次尝试

import React from "react";
import ReactDOM from "react-dom";
let _state
function useState(initialValue) {
     _state = _state===undefined?initialValue:_state;
    function setState(newState) {
        _state = newState;
        render();
    }
    return [_state, setState];
}

const render = ()=>{
    ReactDOM.render(
        <App/>,
        document.getElementById('root')
    );
}
const App = (PROPS)=>{
    const [n,setN] = useState(0)
    return(
        <div>
            <p>{n}</p>
            <button onClick={()=>setN(n+1)}>+1</button>
            <button onClick={()=>{console.log(n)}}>logn</button>
        </div>
    )
}
export default App

冲突

如果一个组件用了**两个usetState怎么办.由于所有数据都放在_state,所以会冲突

import React from "react";
import ReactDOM from "react-dom";
let _state
function useState(initialValue) {
     _state = _state===undefined?initialValue:_state;
    function setState(newState) {
        _state = newState;
        render();
    }
    return [_state, setState];
}
const render = ()=>{
    ReactDOM.render(
        <App/>,
        document.getElementById('root')
    );
}

const App = (PROPS)=>{
    const [n,setN] = useState(0)
    const [m,setM] = useState(0)
    return(
        <div>
           <div>
               <p>{n}</p>
               <button onClick={()=>setN(n+1)}>+1</button>
               <button onClick={()=>setTimeout(()=>{
                   console.log(n)
               },1000)}>logN</button>
           </div>
            <div>
                <p>{m}</p>
                <button onClick={()=>setM(n+1)}>+1</button>
                <button onClick={()=>setTimeout(()=>{
                        console.log(m)
                },1000)}>logM</button>
            </div>
        </div>
    )
}
export default App
注意:由于组件用了两个usetState功用一个_state,出现冲突,当n改变是m也会改变

改进思路

把_state做成一个对象

比如_state={n:0,m:0},这种思路不行,因为usetState(0)不知道是n还是m变了

把_state做成一个数组

比如_state=[0,0],貌似可以,尝试一下

尝试改变usetState

import React from "react";
import ReactDOM from "react-dom";
let _state=[]
let index=0
function useState(initialValue) {
    let currentIndex = index
    index += 1
     _state[currentIndex] = _state[currentIndex]||initialValue;
    const  setState=newState=> {
        _state[currentIndex] = newState;
        render();
    }
    return [_state[currentIndex], setState];
}
const render = ()=>{
    index = 0
    ReactDOM.render(
        <App/>,
        document.getElementById('root')
    );
}

const App = (PROPS)=>{
    const [n,setN] = useState(0)
    const [m,setM] = useState(0)
    return(
        <div>
           <div>
               <p>{n}</p>
               <button onClick={()=>setN(n+1)}>+1</button>
               <button onClick={()=>setTimeout(()=>{
                   console.log(n)
               },1000)}>logN</button>
           </div>
            <div>
                <p>{m}</p>
                <button onClick={()=>setM(m+1)}>+1</button>
                <button onClick={()=>setTimeout(()=>{
                        console.log(m)
                },1000)}>logM</button>
            </div>
        </div>
    )
}
export default App

注意:index、currentIndex和第16行

_state数组方案的缺点

usetState调用顺序

若第一次渲染是n是第一个,m是第二个,k是第三个

则第二次渲染是必须保证顺序完全一致

所以React不允许出现如下代码

1649996113378.png

现在的代码还有问题

  • App用了_state和index其他组件用什么

解决方法:给每个组件创建一个_state和index

  • 放在全局作用域重名怎么版

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

视图

1649996387209.png

总结

  • 每一个函数组件对应一个React节点
  • 每个节点保存着state和index
  • usetState会读取state[index]
  • index有usetState出现的顺序决定的
  • setState会秀给state,并触发更新

注意:上面的useState对React的实现做了简化

React节点应该是FiberNode,_state的真是名称为memorizedState,index的实现用到了链表,有兴趣可以查看

n的分身

const App = (PROPS)=>{
    const [n,setN] = React.useState(0)
    const log = ()=>setTimeout(()=>{console.log(n)},1000)
    return(
        <div>
           <div>
               <p>{n}</p>
               <button onClick={()=>setN(n+1)}>+1</button>
               <button onClick={log}>log</button>
           </div>
        </div>
    )
}

不同结果

  • 点击+1在点击log——无bug
  • 点击log在点击+1——有bug

为什么log出旧的数据

因为有多个n

1649998505900.png

贯穿始终的状态

useRef例子

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

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

强制更新例子

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(useContext不仅可以贯穿始终,还能贯穿不同的组件)

示例代码

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

const themeContext = React.createContext(null);

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都会出现(分身)
  • 如果不希望出现分身,可以使用useRef/useContext