Skip to content

Commit 0972381

Browse files
author
Vadim Demedes
authored
Refactor diffing (#50)
1 parent 78e8653 commit 0972381

14 files changed

+297
-303
lines changed

.babelrc

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"plugins": [
3+
[
4+
"transform-react-jsx",
5+
{
6+
"pragma": "h"
7+
}
8+
]
9+
]
10+
}

example.js

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
'use strict';
2+
const {h, render, Component, Text} = require('.');
3+
4+
class Counter extends Component {
5+
constructor() {
6+
super();
7+
8+
this.state = {
9+
i: 0
10+
};
11+
}
12+
13+
render() {
14+
return (
15+
<Text green>
16+
{this.state.i} tests passed
17+
</Text>
18+
);
19+
}
20+
21+
componentDidMount() {
22+
this.timer = setInterval(() => {
23+
this.setState({
24+
i: this.state.i + 1
25+
});
26+
}, 100);
27+
}
28+
29+
componentWillUnmount() {
30+
clearInterval(this.timer);
31+
}
32+
}
33+
34+
render(<Counter/>);

index.js

+4-18
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ const logUpdate = require('log-update');
55
const StringComponent = require('./lib/string-component');
66
const Component = require('./lib/component');
77
const renderToString = require('./lib/render-to-string');
8-
const callTree = require('./lib/call-tree');
98
const diff = require('./lib/diff');
109
const h = require('./lib/h');
1110
const Indent = require('./lib/components/indent');
@@ -19,19 +18,8 @@ exports.Text = Text;
1918

2019
const noop = () => {};
2120

22-
const unmount = tree => callTree(tree, 'unmount');
23-
const didMount = tree => callTree(tree, 'didMount');
24-
const didUpdate = tree => callTree(tree, 'didUpdate');
25-
26-
const build = (nextTree, prevTree, onUpdate = noop, context = {}, autoLifecycle = true) => {
27-
const reconciledTree = diff(prevTree, nextTree, onUpdate, context);
28-
29-
if (autoLifecycle) {
30-
didMount(reconciledTree);
31-
didUpdate(reconciledTree);
32-
}
33-
34-
return reconciledTree;
21+
const build = (nextTree, prevTree, onUpdate = noop, context = {}) => {
22+
return diff(prevTree, nextTree, onUpdate, context);
3523
};
3624

3725
exports.build = build;
@@ -64,10 +52,8 @@ exports.render = (tree, options) => {
6452
}
6553

6654
const update = () => {
67-
const nextTree = build(tree, currentTree, onUpdate, context, false); // eslint-disable-line no-use-before-define
55+
const nextTree = build(tree, currentTree, onUpdate, context); // eslint-disable-line no-use-before-define
6856
log(renderToString(nextTree));
69-
didMount(nextTree);
70-
didUpdate(nextTree);
7157

7258
currentTree = nextTree;
7359
};
@@ -123,7 +109,7 @@ exports.render = (tree, options) => {
123109
}
124110

125111
isUnmounted = true;
126-
unmount(currentTree);
112+
build(null, currentTree, onUpdate, context); // eslint-disable-line no-use-before-define
127113
log.done();
128114

129115
consoleMethods.forEach(method => console[method].restore());

lib/call-tree.js

-18
This file was deleted.

lib/component.js

+13
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,19 @@ class Component {
5454
return this.render(this.props, this.state, this.context);
5555
}
5656

57+
_componentWillUpdate(nextProps, nextState) {
58+
this.componentWillUpdate(nextProps, nextState);
59+
this.props = nextProps;
60+
this.state = nextState;
61+
this._pendingState = null;
62+
}
63+
64+
_componentDidUpdate() {
65+
this.componentDidUpdate();
66+
this._stateUpdateCallbacks.forEach(cb => cb());
67+
this._stateUpdateCallbacks = [];
68+
}
69+
5770
_enqueueUpdate() {
5871
enqueueUpdate(this._onUpdate);
5972
}

lib/diff.js

+135-25
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,165 @@
11
'use strict';
22

3-
const isEqual = require('lodash.isequal');
4-
const {getInstance, getOrCreateInstance} = require('./instance');
3+
const PropTypes = require('prop-types');
4+
const isEqualShallow = require('is-equal-shallow');
55
const VNode = require('./vnode');
66

7-
const diff = (prevNode, nextNode, onUpdate, context) => {
8-
const prevInstance = getInstance(prevNode);
7+
const isClassComponent = component => component.isComponent;
8+
9+
const checkPropTypes = (component, props) => {
10+
if (process.env.NODE_ENV !== 'production' && typeof component.propTypes === 'object') {
11+
const name = component.displayName || component.name;
12+
13+
PropTypes.checkPropTypes(component.propTypes, props, 'prop', name);
14+
}
15+
};
16+
17+
const getProps = vnode => Object.assign({}, vnode.component.defaultProps, vnode.props);
18+
19+
const getNextState = vnode => {
20+
if (isClassComponent(vnode.component)) {
21+
return vnode.instance._pendingState || vnode.instance.state;
22+
}
23+
24+
return null;
25+
};
26+
27+
const mount = (vnode, context, onUpdate) => {
28+
const props = getProps(vnode);
29+
checkPropTypes(vnode.component, props);
30+
31+
if (isClassComponent(vnode.component)) {
32+
vnode.createInstance(props);
33+
vnode.instance._onUpdate = onUpdate;
34+
35+
const childContext = vnode.instance.getChildContext();
36+
vnode.instance.context = Object.assign({}, context, childContext);
37+
Object.assign(context, childContext);
38+
39+
vnode.instance.componentWillMount();
40+
41+
vnode.children = vnode.instance._render();
42+
} else {
43+
vnode.children = vnode.component(props, context);
44+
}
45+
};
46+
47+
const componentDidMount = vnode => {
48+
if (isClassComponent(vnode.component)) {
49+
vnode.instance.componentDidMount();
50+
51+
if (vnode.ref) {
52+
vnode.ref(vnode.instance);
53+
}
54+
}
55+
};
56+
57+
const shouldComponentUpdate = (vnode, nextProps, nextState) => {
58+
if (isClassComponent(vnode.component)) {
59+
return vnode.instance.shouldComponentUpdate(nextProps, nextState);
60+
}
61+
62+
return !isEqualShallow(vnode.props, nextProps);
63+
};
64+
65+
const componentWillReceiveProps = (vnode, nextProps) => {
66+
vnode.props = nextProps;
967

68+
if (isClassComponent(vnode.component)) {
69+
vnode.instance.componentWillReceiveProps(nextProps);
70+
}
71+
};
72+
73+
const rerender = vnode => {
74+
const nextProps = getProps(vnode);
75+
checkPropTypes(vnode.component, nextProps);
76+
77+
if (isClassComponent(vnode.component)) {
78+
vnode.instance._componentWillUpdate(nextProps, getNextState(vnode));
79+
vnode.children = vnode.instance._render();
80+
return;
81+
}
82+
83+
vnode.children = vnode.component(nextProps, {});
84+
};
85+
86+
const componentDidUpdate = vnode => {
87+
if (isClassComponent(vnode.component)) {
88+
vnode.instance._componentDidUpdate();
89+
}
90+
};
91+
92+
const componentWillUnmount = vnode => {
93+
if (isClassComponent(vnode.component)) {
94+
vnode.instance.componentWillUnmount();
95+
}
96+
};
97+
98+
const unmount = vnode => {
99+
if (isClassComponent(vnode.component)) {
100+
componentWillUnmount(vnode);
101+
vnode.instance = null;
102+
}
103+
104+
vnode.children.forEach(childVNode => {
105+
diff(childVNode, null); // eslint-disable-line no-use-before-define
106+
});
107+
108+
if (isClassComponent(vnode.component) && vnode.ref) {
109+
vnode.ref(null);
110+
}
111+
};
112+
113+
const diff = (prevNode, nextNode, onUpdate, context) => {
10114
if (typeof nextNode === 'number') {
11115
if (prevNode instanceof VNode) {
12-
prevInstance.unmount();
116+
unmount(prevNode);
13117
}
14118

15119
return String(nextNode);
16120
}
17121

18122
if (!nextNode || typeof nextNode === 'boolean') {
19123
if (prevNode instanceof VNode) {
20-
prevInstance.unmount();
124+
unmount(prevNode);
21125
}
22126

23127
return null;
24128
}
25129

26130
if (typeof nextNode === 'string') {
27131
if (prevNode instanceof VNode) {
28-
prevInstance.unmount();
132+
unmount(prevNode);
29133
}
30134

31135
return nextNode;
32136
}
33137

34-
const nextInstance = getOrCreateInstance(nextNode);
35-
36-
nextInstance.context = context;
37-
nextInstance.onUpdate = onUpdate;
138+
let isUpdate = true;
38139

39140
if (!(prevNode instanceof VNode)) {
40-
nextInstance.mount();
41-
return nextNode;
141+
mount(nextNode, context, onUpdate);
142+
isUpdate = false;
42143
}
43144

44-
if (prevNode.component !== nextNode.component) {
45-
prevInstance.unmount();
46-
nextInstance.mount();
47-
return nextNode;
145+
if (isUpdate && prevNode.component !== nextNode.component) {
146+
unmount(prevNode);
147+
mount(nextNode, context, onUpdate);
148+
isUpdate = false;
48149
}
49150

50-
const shouldUpdate = prevInstance.shouldComponentUpdate(nextNode.props);
51-
const prevChildren = [].slice.call(prevNode.children);
151+
const shouldUpdate = isUpdate && shouldComponentUpdate(prevNode, getProps(nextNode), getNextState(prevNode));
152+
const prevChildren = isUpdate ? [].slice.call(prevNode.children) : [];
52153

53-
if (!isEqual(prevNode.props, nextNode.props)) {
54-
prevInstance.props = nextNode.props;
154+
if (isUpdate && !isEqualShallow(getProps(prevNode), getProps(nextNode))) {
155+
componentWillReceiveProps(prevNode, getProps(nextNode));
55156
}
56157

57158
if (shouldUpdate) {
58-
prevInstance.rerender();
159+
rerender(prevNode);
59160
}
60161

61-
const nextChildren = prevNode.children;
162+
const nextChildren = isUpdate ? prevNode.children : nextNode.children;
62163

63164
const length = Math.max(prevChildren.length, nextChildren.length);
64165
const reconciledChildren = [];
@@ -68,9 +169,18 @@ const diff = (prevNode, nextNode, onUpdate, context) => {
68169
reconciledChildren.push(childNode);
69170
}
70171

71-
prevInstance.children = reconciledChildren;
172+
if (isUpdate) {
173+
prevNode.children = reconciledChildren;
174+
175+
if (shouldUpdate) {
176+
componentDidUpdate(prevNode);
177+
}
178+
} else {
179+
nextNode.children = reconciledChildren;
180+
componentDidMount(nextNode);
181+
}
72182

73-
return prevNode;
183+
return isUpdate ? prevNode : nextNode;
74184
};
75185

76186
module.exports = diff;

0 commit comments

Comments
 (0)