Why don’t function components before react 16 have state?
As you know, function components did not have state before react 16, and component state could only be passed through props
.
Write two simple components, a class component and a function component.
1
2
3
4
5
6
7
8
9
10
11
12
13
|
const App = () =><span>123</span>;
class App1 extends React.Component {
constructor(props) {
super(props);
this.state = {
a: 1,
}
}
render() {
return (<p>312</p>)
}
}
|
Compile App1
with babel, and App1
is a function component after compilation.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
// 伪代码
var App1 = /*#__PURE__*/function (_React$Component) {
_inherits(App1, _React$Component);
var _super = _createSuper(App1);
function App1(props) {
var _this;
_classCallCheck(this, App1);
_this = _super.call(this, props);
_this.state = {
a: 1
};
return _this;
}
_createClass(App1, [{
key: "render",
value: function render() {
return/*#__PURE__*/(0, _jsxRuntime.jsx)("p", {
children: "312"
});
}
}]);
return App1;
}(React.Component);
|
So why don’t function components have state? The difference between a function component and a class component is whether or not there is a render
method on the prototype. react calls the render
method of the class component when it renders. The render
of a function component is the function itself, and after it is executed, the internal variables are destroyed, so when the component is re-rendered
, the previous state is not available. Unlike function components, class components generate an instance of a class component when they are rendered for the first time, and the render method is called render. When it is re-rendered, the instance reference of the class component is obtained and the corresponding method of the class component is called at a different life cycle.
There is also no difference between the class component and the function component in terms of their data structures after rendering.
Why do function components have state after react 16?
As we all know, the biggest change made in react 16 is the fiber, and the data structure of the node (fiber node
) has been changed significantly to fit the fiber. Modify the App
component, render it on the page, and get the fiber node
data structure as shown below.
1
2
3
4
5
|
const App = () => {
const [a, setA] = React.useState(0);
const [b, setB] = React.useState(1);
return<span>123</span>
};
|
(function component on the left, class component on the right)
How does react know which component the current state belongs to?
All function component states are injected through useState, how do you identify the corresponding component?
A breakpoint in the render
flow of react shows that function components have a special render
method renderWithHooks
. The method has 6 parameters: current
, workInProgress
, component
, props
, secondArg
, nextRenderExpirationTime
.
1
2
3
4
5
6
|
current: 当前正在页面渲染的node,如果是第一次渲染,则为空
workInProgress: 新的node,用于下一次页面的渲染更新
component: node对应的组件
props: 组件的props
secondArg: 不清楚...,不影响后续文章阅读
nextRenderExpirationTime: fiber渲染的过期时间
|
When executing renderWithHooks
, the current fiber node
is recorded with the variable currentlyRenderingFiber$1
. So when the function component is executed, the useState
method gets the state of the current node
. The state is inserted into the memoizedState
field of the corresponding node
. The returned method that triggered the state
change also knows which fiber node
it is when the change is executed because of the closure. The corresponding source code is as follows.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
function mountState(initialState) {
// 获取hook状态
var hook = mountWorkInProgressHook();
if (typeof initialState === 'function') {
// $FlowFixMe: Flow doesn't like mixed types
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState;
var queue = hook.queue = {
pending: null,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: initialState
};
// 绑定当前node和更新队列
var dispatch = queue.dispatch = dispatchAction.bind(null, currentlyRenderingFiber$1, queue);
return [hook.memoizedState, dispatch];
}
|
renderWithHooks
is only used for rendering function components.
The value of the memoizeState
field shows that the state
of the function component and the class component store different data structures. Class components are simple data objects, while function components are one-way chained tables.
1
2
3
4
5
6
7
8
9
10
11
12
|
interface State {
memoizedState: state数据,和baseState值相同,
baseState: state数据,
baseQueue: 本次更新之前没执行完的queue,
next: 下一个state,
queue: {
pending: 更新state数据(这个数据是一个对象,里面有数据,还有其他key用于做其他事情。),
dispatch: setState方法本身,
lastRenderedReducer: useReducer用得上,
lastRenderedState: 上次渲染的State.memoizedState数据,
}
}
|
What happens when the setA method is called?
Before we talk about updating the component state
, let’s look at the flow of the component mount.
When useState
is called, the currentlyRenderingFiber$1
is used to get the fiber node
of the current component and mount the data to the memoizedState
field on the node. This way the function component has a state.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
|
// react
function useState(initialState) {
var dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}
function resolveDispatcher() {
// ReactCurrentDispatcher 的值是react-dom注入的,后续会讲。
var dispatcher = ReactCurrentDispatcher.current;
if (!(dispatcher !== null)) {
{
throwError( "Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:\n1. You might have mismatching versions of React and the renderer (such as React DOM)\n2. You might be breaking the Rules of Hooks\n3. You might have more than one copy of React in the same app\nSee https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem." );
}
}
return dispatcher;
}
// react-dom 会根据当前组件的状态注入不同的useState实现方法,这里可以先忽略。
useState: function (initialState) {
currentHookNameInDev = 'useState';
mountHookTypesDev();
var prevDispatcher = ReactCurrentDispatcher.current;
ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnMountInDEV;
try {
// 挂载state
return mountState(initialState);
} finally {
ReactCurrentDispatcher.current = prevDispatcher;
}
},
function mountState(initialState) {
// 生成hook初始化数据,挂到fiber node节点上
var hook = mountWorkInProgressHook();
if (typeof initialState === 'function') {
// $FlowFixMe: Flow doesn't like mixed types
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState;
var queue = hook.queue = {
pending: null,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: initialState
};
var dispatch = queue.dispatch = dispatchAction.bind(null, currentlyRenderingFiber$1, queue);
return [hook.memoizedState, dispatch];
}
function mountWorkInProgressHook() {
var hook = {
memoizedState: null,
baseState: null,
baseQueue: null,
queue: null,
next: null
};
if (workInProgressHook === null) {
// node节点的memoizedState指向第一个hooks
currentlyRenderingFiber$1.memoizedState = workInProgressHook = hook;
} else {
// 上一个hooks的next,等于当前hooks,同时把当前workInProgressHook,等于当前hooks
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}
|
useState
also returns the corresponding state
and a method to modify state
. The method dispatchAction
that modifies state
is bound to the current fiber node
, along with the action queue
that updates the current state.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
// 这里删除了部分无关代码
function dispatchAction(fiber, queue, action) {
// 这些都是用于Fiber Reconciler,在这里不用太在意
var currentTime = requestCurrentTimeForUpdate();
var suspenseConfig = requestCurrentSuspenseConfig();
var expirationTime = computeExpirationForFiber(currentTime, fiber, suspenseConfig);
var update = {
expirationTime: expirationTime,
suspenseConfig: suspenseConfig,
action: action,
eagerReducer: null,
eagerState: null,
next: null
};
{
update.priority = getCurrentPriorityLevel();
}
// pending 是当前state是否有未更新的任务(比如多次调用更新state的方法)
var pending = queue.pending;
// queue是一个循环链表
if (pending === null) {
update.next = update;
} else {
update.next = pending.next;
pending.next = update;
}
queue.pending = update;
var alternate = fiber.alternate;
if (fiber === currentlyRenderingFiber$1 || alternate !== null && alternate === currentlyRenderingFiber$1) {
// Reconciler 计算是否还有时间渲染,省略
} else {
// 此处省略很多代码
// 标记当前fiber node需要重新计算。
scheduleWork(fiber, expirationTime);
}
}
|
As you can see from the above code, when the setA
method is called to update the component state, the data to be updated is generated, wrapped in a data structure and pushed to the queue
in the state
.
scheduleWork
will trigger the react update, so that the component needs to be re-rendered. The overall process is basically the same as when it was first mounted, but the implementation of the mountState
method body shows that the component is rendered using initialState
. This is definitely problematic.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
function mountState(initialState) {
// 挂载state
var hook = mountWorkInProgressHook();
if (typeof initialState === 'function') {
initialState = initialState();
}
// state的初始值是initialState,也就是组件传入的值
hook.memoizedState = hook.baseState = initialState;
var queue = hook.queue = {
pending: null,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: initialState
};
var dispatch = queue.dispatch = dispatchAction.bind(null, currentlyRenderingFiber$1, queue);
return [hook.memoizedState, dispatch];
}
|
From this, we can infer that in the previous step, there must be an implementation method that indicates that the current component is not initially mounted and needs to be replaced with useState
. The answer is found in renderWithHooks
.
To make it easier to understand, there are two key pieces of data in react: current and workInProgress, which represent the fiber node rendered by the current page, and the fiber node that calculates the difference after the update is triggered. for rendering.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
// 这里删除部分无关代码
// current 当前页面上组件对应的fiber node
// workInProgress 当前重新渲染对应的fiber node
// Component 函数方法体
// ...
function renderWithHooks(current, workInProgress, Component, props, secondArg, nextRenderExpirationTime) {
// currentlyRenderingFiber$1 是当前正在渲染的组件,后续渲染流程会从改变量获取state
currentlyRenderingFiber$1 = workInProgress;
workInProgress.memoizedState = null;
workInProgress.updateQueue = null;
workInProgress.expirationTime = NoWork; // The following should have already been reset
// currentHook = null;
// workInProgressHook = null;
// didScheduleRenderPhaseUpdate = false;
// TODO Warn if no hooks are used at all during mount, then some are used during update.
// Currently we will identify the update render as a mount because memoizedState === null.
// This is tricky because it's valid for certain types of components (e.g. React.lazy)
// Using memoizedState to differentiate between mount/update only works if at least one stateful hook is used.
// Non-stateful hooks (e.g. context) don't get added to memoizedState,
// so memoizedState would be null during updates and mounts.
{
// 如果当前current不为null,且有state,说明当前组件是更新,需要执行的更新state,否则就是初次挂载。
if (current !== null && current.memoizedState !== null) {
ReactCurrentDispatcher.current = HooksDispatcherOnUpdateInDEV;
} elseif (hookTypesDev !== null) {
// This dispatcher handles an edge case where a component is updating,
// but no stateful hooks have been used.
// We want to match the production code behavior (which will use HooksDispatcherOnMount),
// but with the extra DEV validation to ensure hooks ordering hasn't changed.
// This dispatcher does that.
ReactCurrentDispatcher.current = HooksDispatcherOnMountWithHookTypesInDEV;
} else {
ReactCurrentDispatcher.current = HooksDispatcherOnMountInDEV;
}
}
// 往后省略
}
|
In the renderWithHooks
method, the ReactCurrentDispatcher
is modified, which results in a different method body corresponding to useState
. The useState
method call in HooksDispatcherOnUpdateInDEV
is updateState
. This method ignores initState
and chooses to get the current state from the state
of the fiber node
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
|
useState: function (initialState) {
currentHookNameInDev = 'useState';
updateHookTypesDev();
var prevDispatcher = ReactCurrentDispatcher.current;
ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
try {
return updateState(initialState);
} finally {
ReactCurrentDispatcher.current = prevDispatcher;
}
},
function updateState(initialState) {
return updateReducer(basicStateReducer);
}
function updateReducer(reducer, initialArg, init) {
// 根据之前的state初始化新的state结构,具体方法在下面
var hook = updateWorkInProgressHook();
// 当前更新state的队列
var queue = hook.queue;
queue.lastRenderedReducer = reducer;
var current = currentHook; // The last rebase update that is NOT part of the base state.
var baseQueue = current.baseQueue; // The last pending update that hasn't been processed yet.
var pendingQueue = queue.pending;
if (pendingQueue !== null) {
// We have new updates that haven't been processed yet.
// We'll add them to the base queue.
if (baseQueue !== null) {
// Merge the pending queue and the base queue.
var baseFirst = baseQueue.next;
var pendingFirst = pendingQueue.next;
baseQueue.next = pendingFirst;
pendingQueue.next = baseFirst;
}
current.baseQueue = baseQueue = pendingQueue;
queue.pending = null;
}
if (baseQueue !== null) {
// We have a queue to process.
var first = baseQueue.next;
var newState = current.baseState;
var newBaseState = null;
var newBaseQueueFirst = null;
var newBaseQueueLast = null;
var update = first;
do {
// fiber Reconciler 的内容,省略
} else {
// This update does have sufficient priority.
if (newBaseQueueLast !== null) {
var _clone = {
expirationTime: Sync,
// This update is going to be committed so we never want uncommit it.
suspenseConfig: update.suspenseConfig,
action: update.action,
eagerReducer: update.eagerReducer,
eagerState: update.eagerState,
next: null
};
newBaseQueueLast = newBaseQueueLast.next = _clone;
} // Mark the event time of this update as relevant to this render pass.
// TODO: This should ideally use the true event time of this update rather than
// its priority which is a derived and not reverseable value.
// TODO: We should skip this update if it was already committed but currently
// we have no way of detecting the difference between a committed and suspended
// update here.
markRenderEventTimeAndConfig(updateExpirationTime, update.suspenseConfig); // Process this update.
if (update.eagerReducer === reducer) {
// If this update was processed eagerly, and its reducer matches the
// current reducer, we can use the eagerly computed state.
newState = update.eagerState;
} else {
// 执行状态更新,reducer是个包装函数:typeof action === 'function' ? action(state) : action;
var action = update.action;
newState = reducer(newState, action);
}
}
update = update.next;
} while (update !== null && update !== first);
if (newBaseQueueLast === null) {
newBaseState = newState;
} else {
newBaseQueueLast.next = newBaseQueueFirst;
} // Mark that the fiber performed work, but only if the new state is
// different from the current state.
if (!objectIs(newState, hook.memoizedState)) {
markWorkInProgressReceivedUpdate();
}
hook.memoizedState = newState;
hook.baseState = newBaseState;
hook.baseQueue = newBaseQueueLast;
queue.lastRenderedState = newState;
}
var dispatch = queue.dispatch;
return [hook.memoizedState, dispatch];
}
function updateWorkInProgressHook() {
var nextCurrentHook;
// 当前
if (currentHook === null) {
// alternate 指向的是当前页面渲染组件对应fiber node
var current = currentlyRenderingFiber$1.alternate;
if (current !== null) {
nextCurrentHook = current.memoizedState;
} else {
nextCurrentHook = null;
}
} else {
nextCurrentHook = currentHook.next;
}
var nextWorkInProgressHook;
if (workInProgressHook === null) {
nextWorkInProgressHook = currentlyRenderingFiber$1.memoizedState;
} else {
nextWorkInProgressHook = workInProgressHook.next;
}
if (nextWorkInProgressHook !== null) {
// There's already a work-in-progress. Reuse it.
workInProgressHook = nextWorkInProgressHook;
nextWorkInProgressHook = workInProgressHook.next;
currentHook = nextCurrentHook;
} else {
// Clone from the current hook.
if (!(nextCurrentHook !== null)) {
{
throwError( "Rendered more hooks than during the previous render." );
}
}
currentHook = nextCurrentHook;
var newHook = {
memoizedState: currentHook.memoizedState,
baseState: currentHook.baseState,
baseQueue: currentHook.baseQueue,
queue: currentHook.queue,
next: null
};
if (workInProgressHook === null) {
// 第一个hook currentlyRenderingFiber$1.memoizedState = workInProgressHook = newHook;
} else {
// 下一个hooks,关联前一个hooks
workInProgressHook = workInProgressHook.next = newHook;
}
}
return workInProgressHook;
}
|
At this point, it’s clear what react is doing internally by calling the setA
method. setA
inserts an update action
into the queue
of the current state
and notifies react that there is a component state that needs to be updated. When updating, the method body of useState
is different from the initial mounted method body, so when updating, it ignores the initState
passed by useState
, gets the initial data from the baseState
of the node data, and executes the update action
in the queue
step by step until the queue is empty, or the queue is finished.
Why do function components sometimes get a state that is not real-time?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
const App3 = () => {
const [num, setNum] = React.useState(0);
const add = () => {
setTimeout(() => {
setNum(num + 1);
}, 1000);
};
return (
<>
<div>{num}</div>
<button onClick={add}>add</button>
</>
);
}
|
When the button is clicked within one second, no matter how many times it is clicked, the final page return will be 1
. The reason: setTimeout closes the current state num
, and when executing update state
, the corresponding baseState has not been updated and is still old, i.e. 0
, so multiple clicks will still be 0 + 1 = 1
. The way to modify this is to change the argument passed in to a function, so that when react executes queue
, the state
value from the previous step is passed to the current function.
1
|
setNum((state) => state + 1);
|
Why can’t useState be declared in a judgment statement?
The official react website has this to say.
Suppose there are 3 states
, A, B, C. If B is in the judgment statement, then the states of A and B will be updated in time, but C will not be updated. Because 2 calls to useState
will only update state twice, in the chain of state, A.next->B, B.next->C, then only A and B will be updated, C will not be updated, leading to some unpredictable problems.
Why does state need to be associated with a linked table?
I don’t have an answer to this question, but the only thing I can parse is: it’s for everything (pure) functions, right?
state is still an object, and is updated by calling a method. This way and the class component in turn remains unified and better understood.
Conclusion
By reading the source code to understand the execution of useState
, we can deepen our understanding of the react function component state update. Feel free to point out any shortcomings or mistakes.
The parsing above is based on react@16, reac-dom@16.