浅析React中的useState

浅析React中的useState

1. 简单的 useState 实现

function App() {    // 简单的 +1 案例
    const [n, setN] = React.useState(0)
    return (
        <div className='App'>
            <p>{n}</p>
            <p>
                <button onClick={() => setN(n + 1)}>n+1</button>
            </p>
        </div>
    )
}

const rootElement = document.getElementById("root");
ReactDOM.render(
    <App/>,
    rootElement
);
  1. 首次渲染页面展示内容 0 和 按钮,会调用App函数
  2. 调用 App 函数会获得一个对象,可以认为这个对象是一个虚拟的DOM
  3. 当用户点击 按钮时会调用 setN 函数,并且再次调用 App函数 渲染App组件
  4. 每次调用React.useState时返回的n值应该不一样

尝试实现 React.useState

let _state = undefined    // 模拟 state

function myUseState(initialValue) {   // 模拟useState
    _state = _state === undefined ? initialValue : _state // 只在第一次使用初始值

    function setState(newValue) {
        _state = newValue
        render()
    }

    return [_state, setState]
}
//   这是对 render 的简化
const render = () => ReactDOM.render(<App/>, document.getElementById("root"))

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

const rootElement = document.getElementById("root");
ReactDOM.render(
    <App/>,
    rootElement
);

会发现可以实现 n + 1 功能

但是有这样一个问题,如果一个组件用了两个useState怎么办?由于所有数据都放在_state,所以会冲突

const [n,setN] = myUseState(0)
const [m,setM] = myUseState(0)   // _state 会变成后面的 m
改进思路
  • 把_state做成一个对象
  • 比如_state ={n: 0, m: 0}
  • 不行,因为useState(0)并不知道变量叫n还是m
  • 把_state做成数组
  • 比如_state =[0, 0]
  • 貌似可行,我们来试试看
let _state = []   // 存放多个数据
let index = 0     //  数据下标

function myUseState(initialValue) {
    const currentIndex = index    // 保留当前下标
    _state[currentIndex] = _state[currentIndex] === undefined ? initialValue : _state[currentIndex]   // 只有首次才使用初始值

    function setState(newValue) {     // set函数
        _state[currentIndex] = newValue
        render()
    }

    index++
    return [_state[currentIndex], setState]
}

const render = () => {
    index = 0     // 每次运行 App 函数之前需要重置index 否则 会增加_state 数组长度
    ReactDOM.render(<App/>, document.getElementById("root"))
}

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

const rootElement = document.getElementById("root");
ReactDOM.render(
    <App/>,
    rootElement
);
_state 数组方案的缺点

依赖useState的调用顺序

  • 若第一次渲染时n是第一个,m是第二个,k是第三个
  • 则第二次渲染时必须保证顺序完全一致
  • 所以React不允许出现如下代码,不能在判断里面使用useState
function App(){
    const [n,setN] = React.useState(0)
    let m,setM
    if(n % 2 === 1){
        [m,setM] = React.useState(0)   // 这句会报错
    }
}

代码还有一个问题,App用了_state 和 index,那其他组件用什么 ?

答:给每个组件创建一个_state 和index

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

答:放在组件对应的虚拟节点对象上

2. useState简单原理总结

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

注意:以上代码对React的实现做了简化,React 节点应该是 FiberNode,_state的真实名称为memorizedState,index的实现则用到了链表。

3. useRef 与 useContext

新手 对 n 值的分身问题疑惑问题
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>
  );
}

当我先点击加一,再 log,与先log立即点击加一 打印结果不一样,后者会打印旧的值

这是因为先点击加一按钮时会触发 render 函数,相当于再次运行 App 函数,此时的 n 为加一后的值;当我先点击log再立即点击加一时,log函数中使用的 n 值保留的是旧的值(或者理解为上一个App函数中的旧的变量),因此不会打印新的值,当然没有对旧值的引用时,旧n会被垃圾回收掉

那假如我希望有一个 n 能够贯穿始终,在 React中应该怎么办呢?

  • 使用全局变量

这样可以,但是太low了

  • useRef

useRef 不仅可以用于div,还能用于任意数据

function App() {
  const nRef = React.useRef(0);
  const log = () => setTimeout(() => console.log(`n: ${nRef.current}`), 1000);
  const uadate = React.useState(null)[1]
  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>
  );
}
//  update 为了让App 重新渲染
  • useContext 不仅能贯穿始终,还能贯穿不同组件

点击换颜色例子

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>    // themeContext 作用域在这个标签内
  );
}

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>
  );
}
总结:
  • 每次重新渲染,组件函数就会执行
  • 对应的所有state都会出现 「分身」
  • 如果你不希望出现分身
  • 可以用usaRef / useContext等
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容