简易实现 useState
let _state
const myUseState = initialValue => {
_state = _state === undefined ? initialValue : _state
const setState = newValue => {
_state = newValue
render()
}
return [_state, setState]
}
const render = () => {
ReactDom.render(<App/>, document.getElementById('root'))
}
一开始将 initialValue 赋值给全局变量 _state,setState 时,将 newValue 赋值给 _state 然后重新 rander
这样似乎实现了,但是有一个大问题,如果一个组件用了两个 useState 怎么办
我想了两种思路
- 把_state 做成一个对象 ?
但是这个思路很快就行不通,因为 useState 并不知道 key 要设置成什么
- 把 _state 做成数组 ?
let _state = []
let index = 0
const myUseState = initialValue => {
const currentIndex = index
_state[currentIndex] = _state[currentIndex] === undefined ? initialValue : _state[currentIndex]
const setState = newValue => {
_state[currentIndex] = newValue
render()
}
index += 1
return [_state[currentIndex], setState]
}
const render = () => {
index = 0
// 精髓,每次重新渲染时,要把index 进行重置,不然index 会错乱
ReactDom.render(<App/>, document.getElementById('root'))
}
把 _state 做成数组,每个 useState 的数组拥有自己的 currentIndex ,当 setState 时,使用自己的 currentIndex 对数据进行修改
这个有可能就是 useState 的实现思路,所有 react 的 useState 不允许一下的代码发生
const App = () => {
const [n, setN] = useState(0)
let M , setM
if (n % 2 === 1) {
// 在调整判断中赋值
[M, setM] = useState(0)
}
return (
<div>
{n}
<button onClick={() => {setN(n + 1)}}>+1</button>
{M}
<button onClick={() => {setM(n + 1)}}>+1</button>
</div>
)
}
这样使用,会报如下错误,错误的大致意思时,在每个组件中,必须以完全相同的顺序调用 useState,不然会导致 bug
ESLint: React Hook "useState" is called conditionally. React Hooks must be called in the exact same order in every component render.(react-hooks/rules-of-hooks)
上面的代码基本将 useState 的功能实现了,但是还有一个问题,App 用了 _state 和 index,那其他组件用什么?解决办法是,将 _state 和 index 放在对应的虚拟节点对象上
总结
- 每个函数组件对应一个 React 节点
- 每个节点保持着 state 和 index
- useState 会读取 state[index]
- index 由 useState 出现的顺序决定
- setState 会修改 state,并触发更新
看一个有趣的题目
n 的 分身
const App = () => {
const [n, setN] = myUseState(0)
const log = () => {
setTimeout(() => {
console.log(n)
}, 2000)
}
return (
<div>
{n}
<button onClick={() => {setN(n + 1)}}>+1</button>
<button onClick={log}>log</button>
</div>
)
}
当点击+1 时,n + 1,当点击 log 时,3秒后 log 出 n
现在有趣的来了,当 n 等于 0 时
先点+1,后点log,3秒后 log 出 1
先点 log ,后点 +1 ,3秒后 log 出 0
这是因为 setN 并不会立刻改变 n ,而是重新渲染后再改变n,所有先点 log 时,log 读取的是旧的 n, 我们每次修改state都不会修改当前的state,而是会重新生成一个新的state,旧的state和新的state有可能同时存在,之后旧的state会被垃圾回收掉
看看这个时序图,就明白了
第一条就是我们正常操作先点+1再点log,它会先触发setN,setN执行就出触发render,这时候就会第二次渲染App,生成一个新的n,n是1,再点击三秒后就会log(1);
第二条就是我们先点log,它会三秒后打印出第一次这个n,这时候的n的值还是0,然后点击+1触发render,第二次渲染App,生成一个新的n=1,然后三秒钟到打印出第一次的n也就是0
补更一个知识点
setState 会帮我们合并属性吗?
const App = () => {
const [user, setUser] = useState({name: 'Jacky', age: 18})
const onClick = () => {
setUser({age: 20})
}
return (
<div>
<h1>{user.name}</h1>
<h1>{user.age}</h1>
<button onClick={onClick}>click</button>
</div>
)
}
在 setUset({age: 23}) 后,name 属性会自动合并吗? 不会
所以我们要改成这样,先把旧属性拷贝进去,再覆盖原来的属性
const onClick = () => {
setUset({
...user,
age: 22
})
}
结论是:setState 不可以局部更新
总结
- 每次重新渲染,组件函数就会执行
- setState 不可以局部更新
- 对应的所有 state 都会出现 “分身”
- 如果你不希望出现分身,可以使用 useRef / useContext 等