forked from reduxjs/redux
/
precompute_react_renderer.js
163 lines (142 loc) · 5.82 KB
/
precompute_react_renderer.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
import React from 'react';
import _ from 'lodash';
import ReactInterpreter from '../react_interpreter'
import Timer from '../timer';
// This batches with RAF, but also precomputes values for `compute`
// before calling `render` in the next animation frame.
// This way we can split the computation step from rendering, and more computation
// that render will need will have been memoized before synchronously
// calling render.
//
// The idea is we write the computation we want performed in components, then
// that can be precomputed before render, so that the hot path in render can stay
// synchronous and fast.
//
// This doesn't work when render has branching we can't look inside, but that
// should be isolated to that part of the component tree, and other
// compute optimizations should help there.
export default class PrecomputeReactRenderer {
constructor(reactClass, el, loggit, options = {}) {
this.reactClass = reactClass;
this.el = el;
this.loggit = loggit;
this.options = options;
this.optimizer = options.optimizer;
this._loop = this._loop.bind(this);
this._lastCompute = {};
this._rootComponent = null;
this.timer = new Timer('PrecomputeReactRenderer.render', {
logFn: this.logMsg.bind(this)
});
}
logMsg(...params) {
// console.log(...params);
}
start() {
this._wasDestroyed = false;
this._isDirty = true;
this._loop();
}
notify() {
this._isDirty = true;
}
destroy() {
// this._rootComponent = null;
this._wasDestroyed = true;
return undefined;
}
_loop() {
if (this._wasDestroyed) return;
if (this._isDirty) {
this.timer.time(() => {
this._render();
});
this._isDirty = false;
}
window.requestAnimationFrame(this._loop);
}
// additional API for optimizer/renderer cooperation
// we're trying to improve over naive top-down rendering
notifyAboutCompute(component, computations, computedValues) {
const reactNodeId = ReactInterpreter.nodeId(component);
this.logMsg('PrecomputeReactRenderer#notifyAboutCompute', reactNodeId, computedValues);
this._lastCompute[reactNodeId] = {computedValues}
}
// instead of top-down rendering, we'll walk the tree ourselves, calling
// render as we need to when a copmutedValue has changed, only in order to
// discover lower branches of the tree.
_render() {
this.logMsg('PrecomputeReactRenderer#render');
if (!this._rootComponent) {
return this._initialRender();
}
// discover what parts of the tree need to be rendered.
const dirtyComponents = this._findDirtyComponents(this._rootComponent);
// console.info('PrecomputeReactRenderer:dirtyComponents:', dirtyComponents);
// do something about it to update components.
// could use heuristics here to batch further.
// we could also optimize this if child components under a 'dirty' component
// end up computing the same data for their component. that's an additional
// step, where we essentially want to short-circuit like `shouldComponentUpdate`.
// could work by setState into each component, might be straightforward.
dirtyComponents.forEach((dirtyComponent) => {
this.logMsg('PrecomputeReactRenderer:forceUpdate', dirtyComponent);
dirtyComponent.forceUpdate();
});
}
// This is important since it also collects the
// information about compute for optimizing in
// subsequent passes.
_initialRender() {
this._rootComponent = React.render(
<this.reactClass loggit={this.loggit} />,
this.el
);
}
// Computes and checks whether value changed and we should render.
_shouldRender(component) {
const reactNodeId = ReactInterpreter.nodeId(component);
const lastCompute = this._lastCompute[reactNodeId];
const computations = ReactInterpreter.computations(component);
this.logMsg('computations:', computations);
if (_.isEmpty(computations)) {
return false;
}
// immutability would help here
const prevComputedValues = lastCompute.computedValues || {};
const nextComputedValues = this.optimizer.compute(computations);
this.logMsg('prev:', prevComputedValues, 'next:', nextComputedValues);
const shouldRender = !_.isEqual(prevComputedValues, nextComputedValues);
this.logMsg('shouldRender:', shouldRender);
return shouldRender;
}
// this is a little slipperty, using the loggit seam to make calls
// to the optimizer that aren't tracked.
_findDirtyComponents(component) {
const reactNodeId = ReactInterpreter.nodeId(component);
this.logMsg('PrecomputeReactRenderer#_findDirtyComponents', reactNodeId);
// don't have to render that component if none of its data
// has changed. but we do have to keep walking the tree since
// child components might compute something different (ie, it's not
// really top-down at all anymore).
const shouldRender = this._shouldRender(component);
if (shouldRender) {
// we need to render the rest of the tree under this component, since
// its data has changed and we can't know the effects on the react component
// tree. this is where the strength of writing plain code in `render` hurts,
// since we can inspect further inside that function to do any optimizations.
//
// return this node and everythign under it as dirty.
this.logMsg('PrecomputeReactRenderer.shouldRender', reactNodeId);
return [component];
}
// we don't need to render this node, it's all set, so the next step
// is to descend into its children. this reaches into react internals,
// so those bits are factored out into ReactInterpreter.
// this recurs through the tree.
const childComponents = ReactInterpreter.childComponents(component);
return _.flatten(childComponents.map((childComponent) => {
return this._findDirtyComponents(childComponent);
}));
}
}