200行代码react(译)

babel

在runtime时, babel转译器将对每一个节点都执行在编译注释(Pragma)中声明的函数。
例如:

before babel: (the code you write)

1
2
/** @jsx h */
let foo = <div id="foo">Hello!</div>;

after babel: (the code you run)

1
var foo = h('div', {id:"foo"}, 'Hello!');

也可以在浏览器里打印看一下结果:

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
<script type="text/jsx" data-presets="es2016,react">
/** @jsx h */
let foo = <div id="foo">Hello!</div>;

const h = (type, attributes, ...args) => {
let children = args.length ? [].concat(...args) : null;
return { nodeName, attributes, children };
}

console.log(foo);
// foo: {
// attributes: {id: "foo"}
// children: ["Hello!"]
// nodeName: "div"
// }
</script>
</body>
</html>

createElement

将jsx中的element转换成 plain object

1
2
3
4
5
6
7
8
9
10
11
12
13
function createElement(type, config, ...args) {
const props = Object.assign({}, config);
const hasChildren = args.length > 0;
const rawChildren = hasChildren ? [].concat(...args) : [];
props.children = rawChildren
.filter(c => c != null && c !== false)
.map(c => c instanceof Object ? c : createTextElement(c));
return { type, props };
}

function createTextElement(value) {
return createElement(TEXT_ELEMENT, { nodeValue: value });
}

这里对text node特殊处理了一下,使其不那么特殊, 如此后面处理时省略了许多判断

instance

每个element(jsx转换而来的)对应一个instance,instance包含了dom, element, childInstances。
如果是custom component 则还有一个publicInstance,是组件的实例.

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
function instantiate(element) {
const { type, props } = element;
const isDomElement = typeof type === "string";

// 这里对custom component与built-in component分别做处理
if (isDomElement) {
const isTextElement = type === TEXT_ELEMENT;
const dom = isTextElement
? document.createTextNode("")
: document.createElement(type);

// updateDomProperties 作用是更新dom上的属性,比如class、id、或者删除新增事件监听函数等
updateDomProperties(dom, [], props);

// 处理子组件
const childElements = props.children || [];
const childInstances = childElements.map(instantiate);
const childDoms = childInstances.map(childInstance => childInstance.dom);

// 将子组件的实例插入到dom上
childDoms.forEach(childDom => dom.appendChild(childDom));

const instance = { dom, element, childInstances };
return instance;
} else {
const instance = {};
// 或者custom-component 实例
const publicInstance = createPublicInstance(element, instance);

// 调用render方法,获得子组件,再获取child instance.
// 注意这里是childInstance而不是childInstances,因为custom component只能一个子组件,不能是数组
const childElement = publicInstance.render();
const childInstance = instantiate(childElement);
const dom = childInstance.dom;

Object.assign(instance, { dom, element, childInstance, publicInstance });
return instance;
}
}

createPublicInstance

1
2
3
4
5
6
7
function createPublicInstance(element, internalInstance) {
const { type, props } = element;
const publicInstance = new type(props);

publicInstance.__internalInstance = internalInstance;
return publicInstance;
}

这里的internalInstance其实就是 instance,之所以需要将其绑定到组件实例上是因为
在自定义组件中update需要用到,这里可以先看一下Component的setState实现

1
2
3
4
5
6
7
8
9
10
setState(partialState) {
this.state = Object.assign({}, this.state, partialState);
updateInstance(this.__internalInstance);
}

function updateInstance(internalInstance) {
const parentDom = internalInstance.dom.parentNode;
const element = internalInstance.element;
reconcile(parentDom, internalInstance, element);
}

reconciliation 对组件进行 diff

下面instance是当前的状态,而element是新的状态,将这两者进行diff,就可以得出如何更新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
function reconcile(parentDom, instance, element) {
if (instance == null) {
// 不存在instance则创建一个
const newInstance = instantiate(element);
parentDom.appendChild(newInstance.dom);
return newInstance;
} else if (element == null) {
// 不存在element说明该element已经被删除
parentDom.removeChild(instance.dom);
return null;
} else if (instance.element.type !== element.type) {
// 如果新旧element类型不同则替换
const newInstance = instantiate(element);
parentDom.replaceChild(newInstance.dom, instance.dom);
return newInstance;
} else if (typeof element.type === "string") {
// build-in component 更新属性
updateDomProperties(instance.dom, instance.element.props, element.props);
instance.childInstances = reconcileChildren(instance, element);
instance.element = element;
return instance;
} else {
// 更新custom component的属性
instance.publicInstance.props = element.props;
const childElement = instance.publicInstance.render();
const oldChildInstance = instance.childInstance;
const childInstance = reconcile(
parentDom,
oldChildInstance,
childElement
);
instance.dom = childInstance.dom;
instance.childInstance = childInstance;
instance.element = element;
return instance;
}
}

function reconcileChildren(instance, element) {
const dom = instance.dom;
const childInstances = instance.childInstances;
const nextChildElements = element.props.children || [];
const newChildInstances = [];
const count = Math.max(childInstances.length, nextChildElements.length);
for (let i = 0; i < count; i++) {
const childInstance = childInstances[i];
const childElement = nextChildElements[i];
const newChildInstance = reconcile(dom, childInstance, childElement);
newChildInstances.push(newChildInstance);
}
// 过滤掉已经被删除的element
return newChildInstances.filter(instance => instance != null);
}

Component

由于之前已经讲过了setState,其他部分就很简单了

1
2
3
4
5
6
7
8
9
10
11
class Component {
constructor(props) {
this.props = props;
this.state = this.state || {};
}

setState(partialState) {
this.state = Object.assign({}, this.state, partialState);
updateInstance(this.__internalInstance);
}
}

render

1
2
3
function render(element, container) {
reconcile(container, null, element);
}

一个完整的例子

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<div id="root"></div>
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
<script type="text/jsx" data-presets="es2016,react">
/** @jsx Didact.createElement */

const Didact = importFromBelow();
class App extends Didact.Component {

constructor(props) {
super(props);
this.state = { count: 0 };
}

add = () => {
this.setState({
count: this.state.count + 1,
})
}

render() {
return (
<div>
<h1>count: {this.state.count}</h1>
<Button onClick={this.add} />
</div>
);
}
}

class Button extends Didact.Component {
render() {
const { onClick } = this.props;

return (
<button onClick={onClick}>add</button>
);
}
}

Didact.render(<App />, document.getElementById("root"));

function importFromBelow() {
let rootInstance = null;
const TEXT_ELEMENT = "TEXT_ELEMENT";

function createElement(type, config, ...args) {
const props = Object.assign({}, config);
const hasChildren = args.length > 0;
const rawChildren = hasChildren ? [].concat(...args) : [];
props.children = rawChildren
.filter(c => c != null && c !== false)
.map(c => c instanceof Object ? c : createTextElement(c));
return { type, props };
}

function createTextElement(value) {
return createElement(TEXT_ELEMENT, { nodeValue: value });
}

function render(element, container) {
const prevInstance = rootInstance;
const nextInstance = reconcile(container, prevInstance, element);
rootInstance = nextInstance;
}

function reconcile(parentDom, instance, element) {
if (instance == null) {
const newInstance = instantiate(element);
parentDom.appendChild(newInstance.dom);
return newInstance;
} else if (element == null) {
parentDom.removeChild(instance.dom);
return null;
} else if (instance.element.type !== element.type) {
const newInstance = instantiate(element);
parentDom.replaceChild(newInstance.dom, instance.dom);
return newInstance;
} else if (typeof element.type === "string") {
updateDomProperties(instance.dom, instance.element.props, element.props);
instance.childInstances = reconcileChildren(instance, element);
instance.element = element;
return instance;
} else {
instance.publicInstance.props = element.props;
const childElement = instance.publicInstance.render();
const oldChildInstance = instance.childInstance;
const childInstance = reconcile(
parentDom,
oldChildInstance,
childElement
);
instance.dom = childInstance.dom;
instance.childInstance = childInstance;
instance.element = element;
return instance;
}
}

function reconcileChildren(instance, element) {
const dom = instance.dom;
const childInstances = instance.childInstances;
const nextChildElements = element.props.children || [];
const newChildInstances = [];
const count = Math.max(childInstances.length, nextChildElements.length);
for (let i = 0; i < count; i++) {
const childInstance = childInstances[i];
const childElement = nextChildElements[i];
const newChildInstance = reconcile(dom, childInstance, childElement);
newChildInstances.push(newChildInstance);
}
return newChildInstances.filter(instance => instance != null);
}

function instantiate(element) {
const { type, props } = element;
const isDomElement = typeof type === "string";

if (isDomElement) {
const isTextElement = type === TEXT_ELEMENT;
const dom = isTextElement
? document.createTextNode("")
: document.createElement(type);

updateDomProperties(dom, [], props);

const childElements = props.children || [];
const childInstances = childElements.map(instantiate);
const childDoms = childInstances.map(childInstance => childInstance.dom);
childDoms.forEach(childDom => dom.appendChild(childDom));

const instance = { dom, element, childInstances };
return instance;
} else {
const instance = {};
const publicInstance = createPublicInstance(element, instance);
const childElement = publicInstance.render();
const childInstance = instantiate(childElement);
const dom = childInstance.dom;

Object.assign(instance, { dom, element, childInstance, publicInstance });
return instance;
}
}

function updateDomProperties(dom, prevProps, nextProps) {
const isEvent = name => name.startsWith("on");
const isAttribute = name => !isEvent(name) && name != "children";

Object.keys(prevProps).filter(isEvent).forEach(name => {
const eventType = name.toLowerCase().substring(2);
dom.removeEventListener(eventType, prevProps[name]);
});

Object.keys(prevProps).filter(isAttribute).forEach(name => {
dom[name] = null;
});

Object.keys(nextProps).filter(isAttribute).forEach(name => {
dom[name] = nextProps[name];
});

Object.keys(nextProps).filter(isEvent).forEach(name => {
const eventType = name.toLowerCase().substring(2);
dom.addEventListener(eventType, nextProps[name]);
});
}
function createPublicInstance(element, internalInstance) {
const { type, props } = element;
const publicInstance = new type(props);
publicInstance.__internalInstance = internalInstance;
return publicInstance;
}

class Component {
constructor(props) {
this.props = props;
this.state = this.state || {};
}

setState(partialState) {
this.state = Object.assign({}, this.state, partialState);
updateInstance(this.__internalInstance);
}
}

function updateInstance(internalInstance) {
const parentDom = internalInstance.dom.parentNode;
const element = internalInstance.element;
reconcile(parentDom, internalInstance, element);
}

return {
createElement,
render,
Component
};
}
</script>
</body>
</html>

总结

本文是对文章Didact: a DIY guide to build your own React 的总结。旨在帮助我在阅读react源码之前先整体把握react的结构组成,帮助我更好的阅读源码。由于react16大量更新了代码,引入了Fiber:Incremental reconciliation,下篇看一下fiber的简单的实现。是同一位作者所写。

参考文章

wtf-is-jsx
DIY React