React.useState工作原理

3,013 阅读4分钟

每次使用setState更新属性值,都会再次执行函数组件,并将函数返回的虚拟DOM与虚拟DOM树进行一个DOM Diff的操作。

注意React每次渲染都会执行函数组件,那函数组件中的React.useState(0)每次拿到的值都是0吗。答案很明显当然不是。

那么React。useState(0)在每次函数组件执行的时候都会调用,而得到却并非是初始值0。也就是说相同的代码在不同次数执行的时候得到的值是不一样的。

那么React是如何实现这个功能的呢?

实现React.useState

下面我们试着简单实现一下useState

第一版

let _state
const myUseState=(initialize)=>{
   _state = _state===undefined? initialize:_state;
    const setState=(newState)=>{
        _state = newState;
        render(); //每次更新都会重新渲染
    }
    return [_state,setState]
}

const render=()=>{
    ReactDOM.render(
        <React.StrictMode>
            <Component />
        </React.StrictMode>,
        document.getElementById('root')
    )
}

测试代码:

const Component = () => {
    const [n,setN] = myUseState(0)
    const [m,setM] = myUseState(0)
    return (
        <>
            <button onClick={()=>setN(n+1);console.log('点击了n')}>+1</button>
            <div>n: {n}</div>
            <button onClick={()=>setM(m+1);console.log('点击了m')}>+1</button>
            <div>m: {m}</div>
        </>
    )
}

在只有当个值的时候是可以正常运行的,但是如果函数组件中多次使用useState,例如代码使用两次myUseState就会因为使用了同一个全局变量保存数据,引发mn的值同步的现象。

第二版

那我们应该使用什么数据结构来保存呢?对象?数组?

其实使用对象来保存数据是不可行的,因为对象是无序的你如果需要获取某个值就必须要有一个key,显然在使用React.useState时并没有传入key

那换数组呢?答案是可行的,通过数组的特性顺序存取每个State的值,但这会引发一个问题后面会讲到。

在第一版的基础上添加数组来保存每个不同的值,以及下标。

+ let _state=[];
+ let index = 0

const render=()=>{
+	index = 0 // render在前需要将索引清零
    ReactDOM.render(
        <React.StrictMode>
            <Component />
        </React.StrictMode>,
        document.getElementById('root')
    )
}

const myUseState=(initialize)=>{
    let currentIndex = index //由于index需要自增这里保存一下index临时变量
    _state[currentIndex] = _state[currentIndex]===undefined? initialize:_state[currentIndex];
    const setState=(newState)=>{
        _state[currentIndex] = newState;
        render(); 
    }
    index++
    return [_state[currentIndex],setState]
}

这就完成了一个有基本功能的useState

React会将_state和index全局变量放到虚拟DOM中。也就是每个虚拟DOM对象都对应有自己的state数组,这样就不会导致函数组件产生冲突。

图中展示了首次执行App函数组件会通过useState将数组_state和索引index保存到返回的虚拟DOM中。当使用setN之后改变了n的值,React就会通过DOM Diff检测出变化,并使用Patch更新虚拟DOM树中的_state数组下标为index的值。

React中_state真实的名字应该叫做memorizeState,index的实现则使用了链表。

useState数组中保存的顺序非常重要

在执行函数组件的时候可以通过下标的自增获取对应的state值。由于是通过顺序获取的,这将会强制要求你不允许更改useState的顺序,例如使用条件判断是否执行useState这样会导致按顺序获取到的值与预期的值不同。这个问题也出现在了React.useState自己身上。

因此React是不允许你使用条件判断去控制函数组件中的useState的顺序的。这会导致获取到的值混乱。

如下代码React会直接报错

const Component =()=>{
    let state
    if(true){
        state= React.useState(0);
    }
    return (
        <div>{state}</div>
    )
}

我想你之后看到类似报错就不会有疑惑了

Vue3借鉴了React的Hook思想,并解决了这个问题。

使用回调函数来设置state

由于setState并不是立即生效的,多次使用setState只会执行最后一次的修改。

方案一

把多有操作都和为一次

方按二

使用回到函数,参数接受执行函数时count的值

const [count,setCount] = useState(0)

useEffect(()=>{
	setCount((c)=>c+1)
	setCount((c)=>c+1)
})

推荐都使用过回调函数的方式设置state

总结

React.useState通过线性的数据结构存储,并构按函数组件中Raect.useState执行的顺序存储state

存在的问题: 在函数组件中不能使用条件判断去改变是否执行React.useState,React会直接报错,因为这种操作会导致顺序state的顺序出现混乱。

其他文章

你认为React.setState改变了state的值吗