Implementing a Simple React

Today I saw a good open source project on Github, which is to implement a simple react demo based on the principle of react. Here is just to make your own comments on its code to deepen your understanding. Here is项目链接

You can also learn step by step from this website: https://pomb.us/build-your-own-react/

First of all, it is clear that the element in the code refers to the fiber node, and the dom is the real node of the dom tree

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
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
//Create a fiber node, pass in props and children when needed
function createElement(type, props, ...children) {
return {
type,
props: {
...props,
children: children.map(child =>
typeof child = "object"
? child
: createTextElement(child)
),
},
}
}

//create a text fiber node, type is TEXT_ELEMENT, only nodeValue in the attribute, no children
function createTextElement(text) {
return {
type: "TEXT_ELEMENT",
props: {
nodeValue: text,
children: [],
},
}
}

//really create the dom node, create different dom nodes depending on the type of fiber node
function createDom(fiber) {
const dom =
fiber.type "TEXT_ELEMENT"
? document.createTextNode("")
: document.createElement(fiber.type)

/* The three parameters are respectively, dom node, the previous property on the node, and the current property of the node
The function is to update the property on dom from the previous property to the current property
Because it is a new node, the previous property is empty
*/
updateDom(dom, {}, fiber.props)

return dom
}

//Determine whether the property is a listener
const isEvent = key => key.startsWith("on")
//Determine whether the attribute is a children attribute
const isProperty = key =>
key ! "children" && !isEvent(key)

Curry the function and return a function. The returned function is used to determine whether the new and old attribute values are the same
const isNew = (prev, next) => key =>
prev[key] ! next[key]

Curry the function and return a function. The returned function is used to determine whether there is still a key attribute in the new props
const isGone = (prev, next) => key => !(key in next)
function updateDom(dom, prevProps, nextProps) {
//Remove old or changed event listeners
Object.keys(prevProps)
.filter(isEvent)
.filter(
key =>
!(key in nextProps) ||
isNew(prevProps, nextProps)(key)
)
.forEach(name => {
const eventType = name
.toLowerCase()
.substring(2)
dom.removeEventListener(
eventType,
prevProps[name]
)
})

// Remove old properties
Object.keys(prevProps)
.filter(isProperty)
.filter(isGone(prevProps, nextProps))
.forEach(name => {
dom[name] = ""
})

// Set new or changed properties
Object.keys(nextProps)
.filter(isProperty)
.filter(isNew(prevProps, nextProps))
.forEach(name => {
dom[name] = nextProps[name]
})

// Add event listeners
Object.keys(nextProps)
.filter(isEvent)
.filter(isNew(prevProps, nextProps))
.forEach(name => {
const eventType = name
.toLowerCase()
.substring(2)
dom.addEventListener(
eventType,
nextProps[name]
)
})
}

function commitRoot() {
//The coordination phase will put all fiber nodes that need to be deleted into the deletions array
deletions.forEach(commitWork)
/* wipRoot will be assigned at the beginning of render
wipRoot = {
dom: container,
props: {
children: [element],
},
alternate: currentRoot,
}
That is, the root node of the entire app
*/
commitWork(wipRoot.child)
//commitWork recursion After modifying wipRoot, change the current currentRoot to the modified wipRoot, and wipRoot becomes null
currentRoot = wipRoot
wipRoot = null
}

//recursion traverses the fiber tree with the fiber node as the root node
function commitWork(fiber) {
if (!fiber) {
return
}

//keep recursion up to find the first fiber node by dom node
let domParentFiber = fiber.parent
while (!domParentFiber.dom) {
domParentFiber = domParentFiber.parent
}
//domParent points to the nearest parent dom node
const domParent = domParentFiber.dom

if (
fiber.effectTag = "PLACEMENT" &&
fiber.dom != null
) {
//If effectTag is PLACEMENT, and the new node dom that needs to be added is not empty, it indicates that a new node needs to be added
domParent.appendChild(fiber.dom)
} else if (
fiber.effectTag = "UPDATE" &&
fiber.dom != null
) {
//If effectTag is UPDATE, indicate to update node properties
updateDom(
fiber.dom,
fiber.alternate.props,
fiber.props
)
} else if (fiber.effectTag = "DELETION") {
//delete node if effectTag is UPDATE
commitDeletion(fiber, domParent)
}

//recursion update fiber tree
commitWork(fiber.child)
commitWork(fiber.sibling)
}

//delete the dom corresponding to the fiber node from the domParent
function commitDeletion(fiber, domParent) {
if (fiber.dom) {
domParent.removeChild(fiber.dom)
} else {
commitDeletion(fiber.child, domParent)
}
}

//render function, element is an object, that is, the object compiled by JSX, with type attribute and children attribute, which can be understood as VNode, but not fiber node, container is a dom node
function render(element, container) {
//initialize wipRoot and deletions and assign nextUnitOfWork to wipRoot
wipRoot = {
dom: container,
props: {
children: [element],
},
alternate: currentRoot,
}
deletions = []
nextUnitOfWork = wipRoot
}

The next fiber node to be processed
let nextUnitOfWork = null
//The fiber root node corresponding to the page currently being displayed
let currentRoot = null
//The root node of the fiber tree corresponding to the new page being processed, or
let wipRoot = null
Array of fiber nodes that need to be deleted
let deletions = null

function workLoop(deadline) {
let shouldYield = false
//process the new nextUnitOfWork as long as nextUnitOfWork is not null
//performUnitOfWork will continuously generate new nextUnitOfWork to be processed based on the current fiber node until the entire fiber tree pointed to by wipRoot is traversed
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(
nextUnitOfWork
)
shouldYield = deadline.timeRemaining() < 1
}

if (!nextUnitOfWork && wipRoot) {
//If nextUnitOfWork is empty, it means that the fiber tree has been generated and entered the commit stage
commitRoot()
}

requestIdleCallback(workLoop)
}

//requestIdleCallback is a WebAPI, which means that the main thread executes the passed parameters when it is idle. The parameter is a function, and the parameter of function is how long it is until the next time the main thread is busy
requestIdleCallback(workLoop)

//Process the current incoming fiber node, the next fiber node to be processed will be generated during the processing, and finally the next fiber node to be processed will be returned to nextUnitOfWork
function performUnitOfWork(fiber) {
const isFunctionComponent =
fiber.type instanceof Function
if (isFunctionComponent) {
//Update function components
updateFunctionComponent(fiber)
} else {
//Update the class component
updateHostComponent(fiber)
}
if (fiber.child) {
//If the current fiber node has a child node, return that node as the node of the next performUnitOfWork
return fiber.child
}
If there is no child, find your next sibling node. If there is no child, find the sibling node of your parent node
let nextFiber = fiber
while (nextFiber) {
if (nextFiber.sibling) {
return nextFiber.sibling
}
nextFiber = nextFiber.parent
}
}

//currently processed fiber node
let wipFiber = null
let hookIndex = null

//Update function components, need additional processing hooks
function updateFunctionComponent(fiber) {
wipFiber = fiber
hookIndex = 0
wipFiber.hooks = []
const children = [fiber.type(fiber.props)]
reconcileChildren(fiber, children)
}

//Update the class component
function updateHostComponent(fiber) {
if (!fiber.dom) {
fiber.dom = createDom(fiber)
}
reconcileChildren(fiber, fiber.props.children)
}

function useState(initial) {
const oldHook =
wipFiber.alternate &&
wipFiber.alternate.hooks &&
wipFiber.alternate.hooks[hookIndex]
const hook = {
state: oldHook ? oldHook.state : initial,
queue: [],
}

const actions = oldHook ? oldHook.queue : []
actions.forEach(action => {
hook.state = action(hook.state)
})

const setState = action => {
hook.queue.push(action)
wipRoot = {
stupid: currentRoot.dom,
props: currentRoot.props,
alternate: currentRoot,
}
nextUnitOfWork = wipRoot
deletions = []
}

wipFiber.hooks.push(hook)
hookIndex++
return [hook.state, setState]
}

//compare all sub-nodes and generate a new fiber tree under wipFiber based on elements
function reconcileChildren(wipFiber, elements) {
let index = 0
let oldFiber =
wipFiber.alternate && wipFiber.alternate.child
let prevSibling = null

while (
index < elements.length ||
oldFiber != null
) {
const element = elements[index]
let newFiber = null

const sameType =
oldFiber &&
element &&
element.type oldFiber.type

if (sameType) {
newFiber = {
type: oldFiber.type,
props: element.props,
dom: oldFiber.dom,
parent: wipFiber,
alternate: oldFiber,
effectTag: "UPDATE",
}
}
if (element && !sameType) {
newFiber = {
type: element.type,
props: element.props,
dom: null,
parent: wipFiber,
alternate: null,
effectTag: "PLACEMENT",
}
}
if (oldFiber && !sameType) {
oldFiber.effectTag = "DELETION"
deletions.push(oldFiber)
}

if (oldFiber) {
oldFiber = oldFiber.sibling
}

if (index = 0) {
wipFiber.child = newFiber
} else if (element) {
prevSibling.sibling = newFiber
}

prevSibling = newFiber
index++
}
}

const Didact = {
createElement,
render,
useState,
}

/** @jsx Didact.createElement */
function Counter() {
const [state, setState] = Didact.useState(1)
return (
<h1 onClick={() => setState(c => c + 1)}>
Count: {state}
</h1>
)
}
const element = <Counter />
const container = document.getElementById("root")
Didact.render(element, container)

In general, the whole process looks like this:

  • The render function passes in the root fiber node and the last dom node where the entire dom tree will hang
  • Build a fiber tree with wipRoot as the root node incrementally. Each fiber node has pointers to the parent node, the first sub-node, and the next sibling node. The incremental construction method is that every time the current fiber is processed, the next fiber node to be processed is created. This node may be a child or an uncle node
  • Once a fiber is processed, the next node returned to be processed is empty, which means it is time to start updating and enter the commit phase
  • The commit phase will compare one by one according to the tree where wipRoot is the root node and create the corresponding dom node for the fiber node
  • Finally assign wipRoot to currentRoot, then set wipRoot to null and wait for the next render to return to the second step