0%

【React进阶-3】从零实现一个React(下)

这篇文章给大家介绍一下我们如何自己去实现一个类似于React这样的框架,当然,我们并不会去实现React里面所有的内容,只是去将React基础性的功能实现一遍,让大家对React的认识更加的深入。因为篇幅有限,所以我将这篇文章拆分成了上、下两部分,此文是下篇。

写在前面

本文继续上一节文章,来介绍下剩余的知识,如下:

  • Render和Commit阶段
  • 调和过程
  • 函数组件
  • Hooks

我们接下来的部分就依次介绍下这些知识点。

代码获取

本文涉及到的代码全部上传至码云,需要的同学请在下面地址中获取:

1
https://github.com/xuqwCloud/zerocreate_react

Render和Commit阶段

我们在之前完成的代码中其实有一个问题,在workLook()中每次循环调用performUnitOfWork()方法时,我们都会往fiber父节点中添加一个新的dom元素,就像下面的代码:

img

之前我们也介绍过,自从react引入fiber之后,我们的渲染任务是会被分割成若干个小的任务单元的,每次这些小的任务单元完成后如果有优先级高的任务,浏览器就会打断这些任务单元的执行,而是去执行优先级高的任务,等执行完之后再回来继续从头开始执行这些小的任务单元,所以在浏览器打断的这个过程中,我们在前端页面有时候会看到页面渲染空白、不完整等这样的情况,所以我们接下来优化一下我们之前的代码。

我们先在performUnitOfWork()方法中删除添加新dom元素到fiber上的这段代码,如下:

img

然后在render()方法中,我们给root fiber去一个别名,叫它wipRoot,然后将其赋值给nextUnitOfWork,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
let nextUnitOfWork = null;
let wipRoot = null;

function render(element, container) {
wipRoot = {
dom: container,
props: {
children: [element]
}
};
nextUnitOfWork = wipRoot;
}

接下来,如果”下一个任务单元”没有任何指向的时候就说明我们完成了所有的工作,所以在此时我们将整个fiber树提交给DOM,这就是渲染和提交阶段的一个简单介绍,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let nextUnitOfWork = null;
let wipRoot = null;

function commitRoot() {

}

function render(element, container) {
wipRoot = {
dom: container,
props: {
children: [element]
}
};
nextUnitOfWork = wipRoot;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
function workLoop(deadline) {
let shouldYield = false;
while(nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
shouldYield = deadline.timeRemaining() < 1;
}

if(!nextUnitOfWork && wipRoot) {
commitRoot();
}

requestIdleCallback(workLoop);
}

接下来完善一下commitRoot()方法,在此处我们递归地将所有元素添加至dom,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function commitRoot() {
commitWork(wipRoot.child);
wipRoot = null;
}

function commitWork(fiber) {
if(!fiber) {
return;
}
const domParent = fiber.parent.dom;
domParent.appendChild(fiber.dom);
commitWork(fiber.child);
commitWork(fiber.sibling);
}

这样一来我们就将开始时所描述的那种情形得到了优化,我们最终会将一整棵fiber树递归到添加到dom中,所以这就避免了渲染中被浏览器打断从而出现页面不完整的问题。

调和过程

到目前为止的话我们仅仅实现了DOM元素的渲染和添加这些过程,如果我们的元素要删除、更新的话应该怎么做呢,这就是接下来要介绍的,也就是调和过程。在此过程中我们需要对比两棵fiber树:render()方法接收的新fiber树和我们最后提交到DOM的旧fiber树。

所以在开始之前我们需要一个引用,用来存放最后一个提交的fiber树,而且还要为每一个fiber元素添加alternate属性,用来连接到旧的fiber上,代码如下:

1
2
3
4
5
6
7
8
9
let nextUnitOfWork = null;
let wipRoot = null;
let currentRoot = null;

function commitRoot() {
commitWork(wipRoot.child);
currentRoot = wipRoot;
wipRoot = null;
}
1
2
3
4
5
6
7
8
9
10
function render(element, container) {
wipRoot = {
dom: container,
props: {
children: [element]
},
alternate: currentRoot
};
nextUnitOfWork = wipRoot;
}

接下来我们提取performUnitOfWork()方法中创建新fiber的代码片段到一个新的函数中,这个新的函数叫做reconcileChildren()方法,最后这两个方法中的代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function performUnitOfWork(fiber) {
if(!fiber.dom) {
fiber.dom = createDom(fiber);
}

// if(fiber.parent) {
// fiber.parent.dom.appendChild(fiber.dom);
// }

const elements = fiber.props.children;
reconcileChildren(fiber, elements);

if(fiber.child) {
return fiber.child;
}
let nextFiber = fiber;
while(nextFiber) {
if(nextFiber.sibling) {
return nextFiber.sibling;
}
nextFiber = nextFiber.parent;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function reconcileChildren(wipFiber, elements) {
let index = 0;
let prevSibling = null;

while(index < elements.length) {
const element = elements[index];
const newFiber = {
type: element.type,
props: element.props,
parent: fiber,
dom: null,
}

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

prevSibling = newFiber;
index++;
}
}

在reconcileChildren()方法中实现旧fiber和新元素的调和过程,不过在此处我们目前的reconcileChildren()方法是不能直接运行的,接下来还要优化。

我们同时遍历旧fiber的children(wipFiber.alternate)和要协调的元素数组。在此过程中我们忽略掉一些其他的信息之后,其实仅仅关心oldFiber和element。element是我们要添加到DOM的元素,oldFiber是我们最后一次提交渲染过的fiber,通过比较我们可以了解到是否需要对DOM进行更改,所以reconcileChildren()方法中的代码可以暂时优化成这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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;

//比较旧fiber和新元素

if(oldFiber) {
oldFiber = oldFiber.sibling;
}
}
}

以上的代码中并没有添加对比的过程,所以接下来我们按如下规则添加对比的代码片段:

  • 如果旧fiber和新元素有相同的类型,我们只需要用新的属性去更新这个dom即可;
  • 如果类型不同,就说明它是一个新元素,所以我们要增加这个新的dom节点;
  • 如果类型不同并且它是一个旧fiber,我们需要删除这个旧的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
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) {
//更新dom
}
if(element && !sameType) {
//添加dom
}
if(oldFiber && !sameType) {
//删除dom
}

if(oldFiber) {
oldFiber = oldFiber.sibling;
}
}
}

在上述过程中,react中同时也用了key,以便有一个更好地调和过程,但在本文中为了简单,我们不做介绍。

对于要更新的dom节点,我们可以这样来做:通过旧的fiber创建一个新的fiber,它的props属性从新的element元素赋值,并且为这个新的fiber添加一个effectTag属性,我们在后期提交阶段来使用,代码如下:

1
2
3
4
5
6
7
8
9
10
11
if(sameType) {
//更新dom
newFiber = {
type: oldFiber.type,
props: element.props,
dom: oldFiber.dom,
parent: wipFiber,
alternate: oldFiber,
effectTag: 'UPDATE',
}
}

对于要添加dom的情况,更上述类似,直接看代码:

1
2
3
4
5
6
7
8
9
10
11
if(element && !sameType) {
//添加dom
newFiber = {
type: element.type,
props: element.props,
dom: null,
parent: wipFiber,
alternate: null,
effectTag: 'PLACEMENT',
}
}

对于要删除的dom节点,我们没有必要再创建一个新的fiber,我们只需要给原来的fiber添加一个effectTag标记即可,但是当我们将fiber树提交给dom的时候它是从正在工作的root fiber中进行的,root fiber并没有旧的fiber,所以我们需要一个数组去存这些要删除的dom节点,所以还需要定义一个数组,代码如下:

1
2
3
4
5
if(oldFiber && !sameType) {
//删除dom
oldFiber.effectTag = 'DELETION';
deletions.push(oldFiber);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
let deletions = null;

function render(element, container) {
wipRoot = {
dom: container,
props: {
children: [element]
},
alternate: currentRoot
};
deletions = [];
nextUnitOfWork = wipRoot;
}

然后,我们将变化后的fiber提交至dom时,我们也要用这个数组中的fiber,所以还需要优化一下commitRoot()方法,代码如下:

1
2
3
4
5
6
function commitRoot() {
deletions.forEach(commitWork);
commitWork(wipRoot.child);
currentRoot = wipRoot;
wipRoot = null;
}

接下来我们在commitWork()方法中处理一下我们新增的effectTag标签。如果effectTag标签标记的是增加dom,我们的操作还是和原来一样,将这个dom节点添加至父fiber中,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function commitWork(fiber) {
if(!fiber) {
return;
}

const domParent = fiber.parent.dom;

if(fiber.effectTag === 'PLACEMENT' && fiber.dom != null) {
domParent.appendChild(fiber.dom);
}

commitWork(fiber.child);
commitWork(fiber.sibling);
}

如果标记为删除dom,我们就将这个dom从它的父fiber中删除,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function commitWork(fiber) {
if(!fiber) {
return;
}

const domParent = fiber.parent.dom;

if(fiber.effectTag === 'PLACEMENT' && fiber.dom != null) {
domParent.appendChild(fiber.dom);
}else if(fiber.effectTag === 'DELETION') {
domParent.removeChild(fiber.dom);
}

commitWork(fiber.child);
commitWork(fiber.sibling);
}

如果标记为更新dom,我们就要用目前的元素属性去更新现有的dom节点,所以我们在此处直接调用一个更新节点的函数,这个函数我们稍后定义,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function commitWork(fiber) {
if(!fiber) {
return;
}

const domParent = fiber.parent.dom;

if(fiber.effectTag === 'PLACEMENT' && fiber.dom != null) {
domParent.appendChild(fiber.dom);
}else if(fiber.effectTag === 'DELETION') {
domParent.removeChild(fiber.dom);
}else if(fiber.effectTag === 'UPDATE' && fiber.dom != null) {
updateDom(fiber.dom, fiber.alternate.props, fiber.props);
}

commitWork(fiber.child);
commitWork(fiber.sibling);
}

接下来定义updateDom()这个方法,要实现这个方法,我们其实是在做新旧fiber的对比操作,进而去删除没用的属性或者更新、设置改变后的属性。所以我们在定义updateDom()方法之前还要定以几个额外的方法来辅助我们进行判断,进而在updateDom()方法中来实现属性删除和更新操作,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const isProperty = key => key != 'children';
const isNew = (prev, next) => key => prev[key] != next[key];
const isGone = (prev, next) => key => !(key in next);

function updateDom(dom, prevProps, nextProps) {
//删除旧属性
Object.keys(prevProps).filter(isProperty).filter(isGone(prevProps, nextProps)).forEach(name => {
dom[name] = '';
});

//设置新属性或者改变属性
Object.keys(nextProps).filter(isProperty).filter(isNew(prevProps, nextProps)).forEach(name => {
dom[name] = nextProps[name];
});
}

上述代码在处理属性的时候,我们其实还遗漏了节点上挂载的事件,所以我们要继续优化一下前面几个辅助判断的方法,对前缀是”on”的属性我们要做特殊处理,代码如下:

1
2
3
4
const isEvent = key => key.startsWith('on');
const isProperty = key => key != 'children' && !isEvent(key);
const isNew = (prev, next) => key => prev[key] != next[key];
const isGone = (prev, next) => key => !(key in next);

进而,我们还需要在updateDom()方法中做一下优化,最后代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function updateDom(dom, prevProps, nextProps) {
//删除或改变事件监听
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]);
});

//删除旧属性
Object.keys(prevProps).filter(isProperty).filter(isGone(prevProps, nextProps)).forEach(name => {
dom[name] = '';
});

//设置新属性或者改变属性
Object.keys(nextProps).filter(isProperty).filter(isNew(prevProps, nextProps)).forEach(name => {
dom[name] = nextProps[name];
});

//添加新事件监听
Object.keys(nextProps).filter(isEvent).filter(isNew(prevProps, nextProps)).forEach(name => {
const eventType = name.toLowerCase().substring(2);
dom.addEventListener(eventType, nextProps[name]);
})
}

到此为止,我们就完成了调和过程的介绍,其实调和就是在做dom节点的更新和删除等操作,对应到我们的代码中的话,它其实就是对新旧fiber进行的操作,我们现在保存代码在前端查看时,可以看到原来的输出,代码也并没有任何报错。我们改变一下之前的JSX编写的组件,为其添加一个href属性,我们在前端页面可以看到它是相应的进行了更新,并且这个超链接也是工作正常的,如下:

1
2
3
4
5
6
7
/** @jsx XbcbLib.createElement */
const element = (
<div id='xbcb'>
<a href="http://www.xbcb.top">X北辰北</a>
<br />
</div>
);

img

到目前为止,所有的index.js文件代码如下,大家可以参考对比一下:

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
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';

function createElement(type, props, ...children) {
return {
type,
props: {
...props,
children: children.map( value => {
//typeof value == "object" ? value : createTextElement(value)
if(typeof value == 'object') {
return value;
}else {
return createTextElement(value)
}
})
}
}
}
function createTextElement(text) {
return {
type: 'TEXT_ELEMENT',
props: {
nodeValue: text,
children: []
}
}
}

function createDom(fiber) {
const dom = fiber.type === 'TEXT_ELEMENT' ? document.createTextNode('') : document.createElement(fiber.type);

const isProperty = key => key != 'children';
Object.keys(fiber.props).filter(isProperty).forEach(name => {
dom[name] = fiber.props[name];
});

return dom;

// element.props.children.forEach(child => {
// render(child, dom);
// });

// container.appendChild(dom);
}

let nextUnitOfWork = null;
let wipRoot = null;
let currentRoot = null;
let deletions = null;

const isEvent = key => key.startsWith('on');
const isProperty = key => key != 'children' && !isEvent(key);
const isNew = (prev, next) => key => prev[key] != next[key];
const isGone = (prev, next) => key => !(key in next);

function updateDom(dom, prevProps, nextProps) {
//删除或改变事件监听
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]);
});

//删除旧属性
Object.keys(prevProps).filter(isProperty).filter(isGone(prevProps, nextProps)).forEach(name => {
dom[name] = '';
});

//设置新属性或者改变属性
Object.keys(nextProps).filter(isProperty).filter(isNew(prevProps, nextProps)).forEach(name => {
dom[name] = nextProps[name];
});

//添加新事件监听
Object.keys(nextProps).filter(isEvent).filter(isNew(prevProps, nextProps)).forEach(name => {
const eventType = name.toLowerCase().substring(2);
dom.addEventListener(eventType, nextProps[name]);
})
}

function commitRoot() {
deletions.forEach(commitWork);
commitWork(wipRoot.child);
currentRoot = wipRoot;
wipRoot = null;
}

function commitWork(fiber) {
if(!fiber) {
return;
}

const domParent = fiber.parent.dom;

if(fiber.effectTag === 'PLACEMENT' && fiber.dom != null) {
domParent.appendChild(fiber.dom);
}else if(fiber.effectTag === 'DELETION') {
domParent.removeChild(fiber.dom);
}else if(fiber.effectTag === 'UPDATE' && fiber.dom != null) {
updateDom(fiber.dom, fiber.alternate.props, fiber.props);
}

commitWork(fiber.child);
commitWork(fiber.sibling);
}

function render(element, container) {
wipRoot = {
dom: container,
props: {
children: [element]
},
alternate: currentRoot
};
deletions = [];
nextUnitOfWork = wipRoot;
}

function workLoop(deadline) {
let shouldYield = false;
while(nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
shouldYield = deadline.timeRemaining() < 1;
}

if(!nextUnitOfWork && wipRoot) {
commitRoot();
}

requestIdleCallback(workLoop);
}

requestIdleCallback(workLoop);

function performUnitOfWork(fiber) {
if(!fiber.dom) {
fiber.dom = createDom(fiber);
}

// if(fiber.parent) {
// fiber.parent.dom.appendChild(fiber.dom);
// }

const elements = fiber.props.children;
reconcileChildren(fiber, elements);

if(fiber.child) {
return fiber.child;
}
let nextFiber = fiber;
while(nextFiber) {
if(nextFiber.sibling) {
return nextFiber.sibling;
}
nextFiber = nextFiber.parent;
}
}

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) {
//更新dom
newFiber = {
type: oldFiber.type,
props: element.props,
dom: oldFiber.dom,
parent: wipFiber,
alternate: oldFiber,
effectTag: 'UPDATE',
}
}
if(element && !sameType) {
//添加dom
newFiber = {
type: element.type,
props: element.props,
dom: null,
parent: wipFiber,
alternate: null,
effectTag: 'PLACEMENT',
}
}
if(oldFiber && !sameType) {
//删除dom
oldFiber.effectTag = 'DELETION';
deletions.push(oldFiber);
}

if(oldFiber) {
oldFiber = oldFiber.sibling;
}

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

prevSibling = newFiber;
index++;
}
}

const XbcbLib = {
createElement,
render
};

/** @jsx XbcbLib.createElement */
const element = (
<div id='xbcb'>
<a href="http://www.xbcb.top">X北辰北</a>
<br />
</div>
);

const container = document.getElementById('root');
XbcbLib.render(element, container);

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();

函数组件

介绍完以上部分之后,我们这部分内容介绍一下函数组件。因为目前我们添加的JSX语法组件都是正常的HTML标记,并不是自定义的组件,所以接下来我们继续优化我们的项目,使其能够支持函数组件。

我们先改写原来编写的element组件代码,让它变成一个函数组件,如下:

1
2
3
4
5
6
7
8
/** @jsx XbcbLib.createElement */
function App(props) {
return <h1>Hi, {props.name}</h1>;
}

const element = <App name="X北辰北" />;
const container = document.getElementById('root');
XbcbLib.render(element, container);

我们这时候直接保存代码的时候,前端页面会报错,因为目前代码中并不支持函数组件渲染。但是我们知道,如果此时将这个函数组件的JSX向JS转换的时候,它应该会做以下的转变:

1
2
3
4
5
6
7
8
9
10
11
12
13
/** @jsx XbcbLib.createElement */
function App(props) {
return XbcbLib.createElement(
'h1',
null,
'Hi',
props.name
)
}

const element = XbcbLib.createElement(App, {
name: 'X北辰北',
});

在开始之前我们要知道两点:

  • 函数组件的fiber没有DOM节点
  • children属性并不是直接来自于props,而是来自于函数的调用

所以我们要对之前的performUnitOfWork()方法做一个优化,在它里面对fiber的类型做一下判断,然后再决定使用不同的更新方法来进行fiber的更新调和操作。如果是函数组件,我们使用函数组件的更新方法,如果不是函数组件,我们使用原来的更新方法,代码如下:

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
function performUnitOfWork(fiber) {

const isFunctionComponent = fiber.type instanceof Function;
if(isFunctionComponent) {
updateFunctionComponent(fiber);
}else {
updateHostComponent(fiber);
}

if(fiber.child) {
return fiber.child;
}
let nextFiber = fiber;
while(nextFiber) {
if(nextFiber.sibling) {
return nextFiber.sibling;
}
nextFiber = nextFiber.parent;
}
}

function updateFunctionComponent(fiber) {

}

function updateHostComponent(fiber) {
if(!fiber.dom) {
fiber.dom = createDom(fiber);
}

const elements = fiber.props.children;
reconcileChildren(fiber, elements);
}

在函数组件的更新方法中,我们主要是去获取children属性。比如在我们的示例代码中,它的fiber.type就是一个App函数,所以我们调用它之后会返回一个h1的dom元素。如果我们拿到了children属性,那接下来的过程就是调和了,调和的过程跟我们之前的代码是没有任何差别的,代码如下:

1
2
3
4
function updateFunctionComponent(fiber) {
const children = [fiber.type(fiber.props)];
reconcileChildren(fiber, children);
}

因为有了没有dom节点的fiber树,所以我们要更改一下commitWork()方法。在这里我们主要改两部分,第一部分就是我们首先要找到dom节点的父节点,我们需要沿着fiber树一直往上找,直到找到带有dom节点的fiber为止,所以要修改的第一部分代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function commitWork(fiber) {
if(!fiber) {
return;
}

let domParentFiber = fiber.parent;
while(!domParentFiber.dom) {
domParentFiber = domParentFiber.parent;
}
const domParent = domParentFiber.dom;

if(fiber.effectTag === 'PLACEMENT' && fiber.dom != null) {
domParent.appendChild(fiber.dom);
}else if(fiber.effectTag === 'DELETION') {
domParent.removeChild(fiber.dom);
}else if(fiber.effectTag === 'UPDATE' && fiber.dom != null) {
updateDom(fiber.dom, fiber.alternate.props, fiber.props);
}

commitWork(fiber.child);
commitWork(fiber.sibling);
}

第二部分就是节点删除部分,我们需要找到具有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
function commitWork(fiber) {
if(!fiber) {
return;
}

let domParentFiber = fiber.parent;
while(!domParentFiber.dom) {
domParentFiber = domParentFiber.parent;
}
const domParent = domParentFiber.dom;

if(fiber.effectTag === 'PLACEMENT' && fiber.dom != null) {
domParent.appendChild(fiber.dom);
}else if(fiber.effectTag === 'DELETION') {
//domParent.removeChild(fiber.dom);
commitDeletion(fiber, domParent);
}else if(fiber.effectTag === 'UPDATE' && fiber.dom != null) {
updateDom(fiber.dom, fiber.alternate.props, fiber.props);
}

commitWork(fiber.child);
commitWork(fiber.sibling);
}

function commitDeletion(fiber, domParent) {
if(fiber.dom) {
domParent.removeChild(fiber.dom);
}else {
commitDeletion(fiber.child, domParent);
}
}

至此为止我们就完成了函数组件的支持,我们定义一个组件,然后将其渲染到页面上,如下:

1
2
3
4
5
6
7
8
/** @jsx XbcbLib.createElement */
function AppFunction(props) {
return <h1>Hi, {props.name}</h1>;
}

const element = <AppFunction name="X北辰北" />;
const container = document.getElementById('root');
XbcbLib.render(element, container);

img

Hooks

我们自己的react目前已经支持函数组件,但是还缺少state的支持,所以接下来我们看看如何添加state的支持。在此处我们使用hooks来维护函数组件中的state。所以我们先改写一下示例代码,就用最经典的计数器例子,每次点击的时候它的次数会增加1,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const XbcbLib = {
createElement,
render,
useState,
};

/** @jsx XbcbLib.createElement */
function AppFunction(props) {
const [state, setState] = XbcbLib.useState(1);
return (
<h1 onClick={() => setState(c => c + 1)}>
H1, {props.name}。你点击的次数为{state}。
</h1>
)
}

const element = <AppFunction name="X北辰北" />;
const container = document.getElementById('root');
XbcbLib.render(element, container);

然后定义useState()方法,并且在定义此方法之前我们还需要定义一些全局变量,以便后续在此方法中使用,各个变量的初始化工作在函数组件更新的方法中完成,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let wipFiber = null;
let hookIndex = null;

function updateFunctionComponent(fiber) {
wipFiber = fiber;
hookIndex = 0;
wipFiber.hooks = [];
const children = [fiber.type(fiber.props)];
reconcileChildren(fiber, children);
}

function useState(initial) {

}

上述代码中我们将work设置为进行中的fiber,同时还向fiber增加了一个hooks数组,以便于支持在同一组件中多次调用useState()。同时我们跟踪当前的hook索引。

当函数组件调用useState()时我们检查它是否有旧的hook。用hook索引去检查fiber的alternate属性。如果有旧的hook,我们将state从旧的hook复制到新的hook,否则我们将初始化state。然后将新的hook添加到fiber,并且将hook索引增加1之后返回state,代码如下:

1
2
3
4
5
6
7
8
9
10
function useState(initial) {
const oldHook = wipFiber.alternate && wipFiber.alternate.hooks && wipFiber.alternate.hooks[hookIndex];
const hook = {
state: oldHook ? oldHook.state : initial,
}

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

此时我们保存代码后可以在前端页面看到预期的效果,但是当我们点击时并没有任何反应,这是因为useState()中还需要返回一个函数去更新state,所以我们要在此方法里面定义一个setState()函数来接收一个操作,我们将这个操作放到一个队列中,然后就执行与渲染过程中类似的操作,将新的进行中的工作单元设置为下一个工作单元,以便可以循环进行新的渲染阶段,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function useState(initial) {
const oldHook = wipFiber.alternate && wipFiber.alternate.hooks && wipFiber.alternate.hooks[hookIndex];
const hook = {
state: oldHook ? oldHook.state : initial,
queue: [],
}

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];
}

但是目前我们还不能运行上述代码中的action操作,我们是在下一次渲染组件时运行这些的,首先是从旧的hook队列中拿到所有的action,然后将它们逐一应用到新的hook中的state上,所以我们会在它更新完成后返回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
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];
}

至此为止,我们就完成了自己的react,点击效果如下:

img

结尾

这篇文章仅仅是帮助我们了解react的工作流程,同时也是为我们后期阅读react源码做了铺垫,所以在我们的代码里使用了和react中同样名称的变量和方法。但是在我们的代码中没有包括很多React的功能和优化。例如,我们可以看看react中有些操作它是怎么做的:

  • 在XbcbLib中,我们在渲染阶段遍历整棵树。相反,React遵循一些提示和试探法,以跳过没有任何更改的整个子树。
  • 我们还在提交阶段遍历整棵树。React仅保留有影响的fiber并仅访问那些fiber的链表。
  • 每次我们建立一个新的进行中的工作树时,都会为每个fiber创建新的对象。React回收了先前树中的fiber。
  • 当XbcbLib在渲染阶段收到新的更新时,它将丢弃进行中的工作树,然后从根开始重新进行。React使用过期时间戳标记每个更新,并使用它来决定哪个更新具有更高的优先级。
  • 类似的还有很多…

你自己也可以添加如下的功能:

  • 使用对象作为样式属性
  • 展平子数组
  • useEffect hook
  • 密钥对帐