实现一个简单的React

今天在Github上看到一个很好的开源项目,是根据react的原理实现一个简单的react demo。这里只是对它的代码做自己的注释,加深自己的理解。这里是项目链接

也可以根据这个网站一步步去学习:https://pomb.us/build-your-own-react/

首先明确代码中的element指的是fiber节点,dom才是真正的dom树上的节点

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
// 创建一个fiber节点,需要创建时就传入props和children
function createElement(type, props, ...children) {
return {
type,
props: {
...props,
children: children.map(child =>
typeof child === "object"
? child
: createTextElement(child)
),
},
}
}

// 创建一个文本fiber节点,类型是TEXT_ELEMENT,属性中只有nodeValue,没有children
function createTextElement(text) {
return {
type: "TEXT_ELEMENT",
props: {
nodeValue: text,
children: [],
},
}
}

// 真正创建dom节点,根据fiber节点的type不同,创建不同的dom节点
function createDom(fiber) {
const dom =
fiber.type == "TEXT_ELEMENT"
? document.createTextNode("")
: document.createElement(fiber.type)

/*三个参数分别为,dom节点,节点上之前的属性,节点现在的属性
函数作用就是将dom上的属性由之前的属性更新为现在的属性
因为是新的节点,所以之前的属性为空
*/
updateDom(dom, {}, fiber.props)

return dom
}

// 判断属性是否是监听器
const isEvent = key => key.startsWith("on")
// 判断属性是否是children属性
const isProperty = key =>
key !== "children" && !isEvent(key)

// 柯里化函数,又返回一个函数,返回的函数用于判断新旧属性值是否相同
const isNew = (prev, next) => key =>
prev[key] !== next[key]

// 柯里化函数,又返回一个函数,返回的函数用于判断新的props中是否还有key属性
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() {
// 协调阶段会把所有需要删除的fiber节点放进deletions数组中
deletions.forEach(commitWork)
/* render开始阶段会把wipRoot赋值为
wipRoot = {
dom: container,
props: {
children: [element],
},
alternate: currentRoot,
}
也就是整个app的根节点
*/
commitWork(wipRoot.child)
// commitWork递归修改wipRoot之后,将当前currentRoot变为修改之后的wipRoot,wipRoot变为null
currentRoot = wipRoot
wipRoot = null
}

// 递归遍历以fiber节点为根节点的fiber树
function commitWork(fiber) {
if (!fiber) {
return
}

// 一直递归向上找到第一个由dom节点的fiber节点
let domParentFiber = fiber.parent
while (!domParentFiber.dom) {
domParentFiber = domParentFiber.parent
}
// domParent指向距离自己最近的父级dom节点
const domParent = domParentFiber.dom

if (
fiber.effectTag === "PLACEMENT" &&
fiber.dom != null
) {
// 如果effectTag是PLACEMENT,同时需要添加的新节点dom不为空,表明需要添加新的节点
domParent.appendChild(fiber.dom)
} else if (
fiber.effectTag === "UPDATE" &&
fiber.dom != null
) {
// 如果effectTag是UPDATE,表明要更新节点属性
updateDom(
fiber.dom,
fiber.alternate.props,
fiber.props
)
} else if (fiber.effectTag === "DELETION") {
// 如果effectTag是UPDATE,删除节点
commitDeletion(fiber, domParent)
}

// 递归更新fiber树
commitWork(fiber.child)
commitWork(fiber.sibling)
}

// 从domParent上删除fiber节点对应的dom
function commitDeletion(fiber, domParent) {
if (fiber.dom) {
domParent.removeChild(fiber.dom)
} else {
commitDeletion(fiber.child, domParent)
}
}

// render函数,element是一个对象,就是JSX编译出来的对象,有type属性,children属性,可以理解为VNode,但还不是fiber节点,container是一个dom节点
function render(element, container) {
// 初始化wipRoot和deletions,并将nextUnitOfWork赋值为wipRoot
wipRoot = {
dom: container,
props: {
children: [element],
},
alternate: currentRoot,
}
deletions = []
nextUnitOfWork = wipRoot
}

// 下一个要处理的fiber节点
let nextUnitOfWork = null
// 当前正在展示的页面对应的fiber树根节点
let currentRoot = null
// 正在处理的,或者说新的页面对应的fiber树根节点
let wipRoot = null
// 需要被删除的fiber节点的数组
let deletions = null

function workLoop(deadline) {
let shouldYield = false
// 只要 nextUnitOfWork 不为 null 就处理新的 nextUnitOfWork
// performUnitOfWork 中会不断根据当前的fiber节点生成新的要处理的 nextUnitOfWork,直到整个wipRoot指向的fiber树都遍历完成
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(
nextUnitOfWork
)
shouldYield = deadline.timeRemaining() < 1
}

if (!nextUnitOfWork && wipRoot) {
// 如果 nextUnitOfWork 为空,说明fiber树已经生成结束,进入commit阶段
commitRoot()
}

requestIdleCallback(workLoop)
}

// requestIdleCallback 是WebAPI,意思是主线程空闲的时候执行传入的参数,参数是一个函数,函数的参数是距离下次主线程忙还有多久
requestIdleCallback(workLoop)

// 处理当前传入的fiber节点,处理过程中会生成下一个要处理的fiber节点,最后返回下一个要处理的fiber节点给 nextUnitOfWork
function performUnitOfWork(fiber) {
const isFunctionComponent =
fiber.type instanceof Function
if (isFunctionComponent) {
// 更新函数式组件
updateFunctionComponent(fiber)
} else {
// 更新class组件
updateHostComponent(fiber)
}
if (fiber.child) {
// 如果当前fiber节点有child节点,返回该节点作为下一个 performUnitOfWork 的节点
return fiber.child
}
// 如果没有child,就找自己的下一个兄弟节点,如果还没有就找自己父节点的兄弟节点
let nextFiber = fiber
while (nextFiber) {
if (nextFiber.sibling) {
return nextFiber.sibling
}
nextFiber = nextFiber.parent
}
}

// 当前处理的fiber节点
let wipFiber = null
let hookIndex = null

// 更新函数式组件,需要额外处理hooks
function updateFunctionComponent(fiber) {
wipFiber = fiber
hookIndex = 0
wipFiber.hooks = []
const children = [fiber.type(fiber.props)]
reconcileChildren(fiber, children)
}

// 更新class组件
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 = {
dom: currentRoot.dom,
props: currentRoot.props,
alternate: currentRoot,
}
nextUnitOfWork = wipRoot
deletions = []
}

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

// 比较所有子节点,并根据elements在wipFiber下面生成新的fiber树
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)

总的来说,整个流程是这个样子的:

  • render函数传入根fiber节点以及最后整个dom树要挂在的dom节点
  • 通过增量式的方式构建以wipRoot为根节点的fiber树,每个fiber节点有指向父节点,第一个子节点以及下一个兄弟节点的指针。增量式构建的方式是每次处理当前fiber也就是wipFiber时,会创建下一个要处理的fiber节点,这个节点可能是child,也可能是uncle节点
  • 一旦某次处理某个fiber之后,返回的下一个要处理的节点为空,那就意味着要开始更新了,进入commit阶段
  • commit阶段会根据wipRoot为根节点的树一个个比较并为fiber节点创建对应的dom节点
  • 最终把wipRoot赋值给currentRoot,然后wipRoot置为null,等待下一次render回到第二步