浅析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等
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容