useState及其原理

320 阅读4分钟

实现一个简单的 useState

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

function App() {
	const [a, setA] = myUseState(0);
	return (
		<div className="App">
			<div>
				<span>{a}</span>
				<button onClick={() => setA(a + 1)}>+</button>
			</div>
		</div>
	);
}

// 定义一个重渲染函数
// 这里我们是简化了react重渲染的
// 这样当你更新useState的状态值,通过重渲染来更新UI视图
const rerender = () => {
	ReactDOM.render(
		<App />,
		document.getElementById('root')
	);
};

let _state; // 定义一个state状态值

// 使用myUseState函数来模拟useState
// 首先传入初始state状态值 - initialState
// 判断_state是否为undefined,如果为undefined则取初始值,否则直接使用_state值
// 因为_state为更新的状态值,如果未更新的话初始化的时候则默认取传入的initialState
// 当调用了setState之后,则更新_state,并进行重渲染来渲染UI视图
const myUseState = initialState => {
	_state = _state === undefined ? initialState : _state;
	const setState = newState => {
		_state = newState;
		rerender();
	};
	return [_state, setState];
};

export default App;

代码如上会存在一些问题,首先就是myUseState如果定义了多个state,那么_state就满足不了了。

如何才可以解决多个state状态值的问题呢?加强我们的myUseState咯?

主要的思路就是给组件每个调用myUseState的地方绑定一个内部的state值。 如下所示:

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

function App() {
	const [a, setA] = myUseState(0);
	const [b, setB] = myUseState(0);
	return (
		<div className="App">
			<div>
				<span>{a}</span>
				<button onClick={() => setA(a + 1)}>+</button>
			</div>
			<div>
				<span>{b}</span>
				<button onClick={() => setB(b + 1)}>+</button>
			</div>
		</div>
	);
}

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

let _state = [];

let index = 0;

const myUseState = initialState => {
	const curIndex = index;
	index++;
	_state[curIndex] = _state[curIndex] === undefined ? initialState : _state[curIndex];
	const setState = newState => {
		_state[curIndex] = newState;
		rerender();
	};
	return [_state[curIndex], setState];
};

export default App;

然后当你测试的时候你会发现并没有按照我们的思路走。

通过debug调试你会发现,初次render的时候,App里面打印出_state为[0, 0]。然后通过setA之后,变成了[1, 0, 0, 0]。

因为在setA的时候 _state[curIndex] = newState ,而此时的curIndex为0(其实就是一个闭包吧),newState为1。然后接着往下走rerender函数,又调用了两次myUseState。那么此时在调用myUseState这两次之前,index分别2,3。

那么你可能有疑问为什么_state为[1,0,0,0],而a没有更新为1呢?

因为rerender之后,对应的状态值a,b都为0了。如下所示,步骤1即返回给了a,而步骤2返回了b。

执行如下所示:

// 1. setA操作myUseState第一次调用
const myUseState = initialState => {
	const curIndex = index; // curIndex为2了,自行体会下。不难理解
	index++; // 下次读取index,他就是3了哦
	_state[curIndex] = _state[curIndex] === undefined ? initialState : _state[curIndex];
	const setState = newState => {
		_state[curIndex] = newState;
		rerender();
	};
	return [_state[curIndex], setState]; // _state[2]值为0
};

// 2. myUseState第二次调用
const myUseState = initialState => {
	const curIndex = index; // curIndex 为 3 
	index++; // 下次读取index,他就是4了哦
	_state[curIndex] = _state[curIndex] === undefined ? initialState : _state[curIndex];
	const setState = newState => {
		_state[curIndex] = newState;
		rerender();
	};
	return [_state[curIndex], setState]; // _state[3]值为0
};

所以这就是为什么_state值为[1,0,0,0]的原因了。

那么如何解决呢?

思路就是在rerender之前,将index置为0就可以对应起来了。

所以将rerender修改下即可:

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

测试通过。

当然在react可不止这么简单,这边只是大致实现的思路。

在react内部,其实_state和index都会挂载到fiberNode上的对应属性上并进行维护,即链表形式的一种数据结构。

由于其实现机制需要记录次序,所以也就是为什么不可以在条件判断语句中(if)使用hook的原因了。

完整代码:

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

function App() {
	const [a, setA] = myUseState(0);
	const [b, setB] = myUseState(0);
	return (
		<div className="App">
			<div>
				<span>{a}</span>
				<button onClick={() => setA(a + 1)}>+</button>
			</div>
			<div>
				<span>{b}</span>
				<button onClick={() => setB(b + 1)}>+</button>
			</div>
		</div>
	);
}

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

let _state = [];

let index = 0;

const myUseState = initialState => {
	const curIndex = index;
	index += 1;
	_state[curIndex] = _state[curIndex] === undefined ? initialState : _state[curIndex];
	const setState = newState => {
		_state[curIndex] = newState;
		rerender();
	};
	return [_state[curIndex], setState];
};

export default App;

总结

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

注意:此内容对react实现做了简化,react节点应该是FIberNode,_state真实名称对为memoizedState,index实现则用到链表。