Skip to content

Commit 78d5e1d

Browse files
MerriRendez
authored andcommitted
fix(perf+size): Performance and code size improvements
* chore(rename): use a more specific variable name * chore(Container): remove clear and count methods * chore(Container): replace class with regular functions * chore(rename): Container is now a plain JS file * chore(createObserver): remove callback param, move callback fn * fix(warning): hide from production code * fix(reobserve): reobserve always only once; reduce code size * chore(syntax): use similar style for all non-React methods * chore(performance): use shorter and faster code for unmatched * chore(syntax): use shorter code to detect undefined/null * chore(syntax): swap findObserverElement arguments around * chore(performance): use faster for loop; use consistent naming * chore(syntax): use shorter code to achieve same result * chore(performance): use shorter syntax and mutate options object * chore(rename): use more generic name
1 parent bed868d commit 78d5e1d

File tree

8 files changed

+202
-228
lines changed

8 files changed

+202
-228
lines changed

src/IntersectionObserver.js

Lines changed: 35 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -3,27 +3,11 @@ import { findDOMNode } from 'react-dom';
33
import PropTypes from 'prop-types';
44
import invariant from 'invariant';
55
import warning from 'warning';
6-
import IntersectionObserverContainer from './IntersectionObserverContainer';
7-
import { isDOMTypeElement, shallowCompareOptions } from './utils';
8-
9-
/**
10-
* The Intersection Observer API callback that is called whenever one element,
11-
* called the target, intersects either the device viewport or a specified element.
12-
* Also will get caled whenever the visibility of the target element changes and
13-
* crosses desired amounts of intersection with the root.
14-
* @param {array} changes
15-
* @param {IntersectionObserver} observer
16-
*/
17-
export function callback(changes, observer) {
18-
changes.forEach(entry => {
19-
const instance = IntersectionObserverContainer.findElement(entry, observer);
20-
if (instance) {
21-
instance.handleChange(entry);
22-
}
23-
});
24-
}
6+
import { createObserver, observeElement, unobserveElement } from './observer';
7+
import { isDOMTypeElement, shallowCompare } from './utils';
258

269
const observerOptions = ['root', 'rootMargin', 'threshold'];
10+
const observerProps = ['disabled'].concat(observerOptions);
2711
const objectProto = Object.prototype;
2812

2913
export default class IntersectionObserver extends React.Component {
@@ -86,18 +70,12 @@ export default class IntersectionObserver extends React.Component {
8670
};
8771

8872
get options() {
89-
return observerOptions.reduce((prev, key) => {
73+
return observerOptions.reduce((options, key) => {
9074
if (objectProto.hasOwnProperty.call(this.props, key)) {
91-
let value = this.props[key];
92-
if (key === 'root' && objectProto.toString.call(this.props[key]) === '[object String]') {
93-
value = document.querySelector(value);
94-
}
95-
return {
96-
...prev,
97-
[key]: value,
98-
};
75+
const useQuery = key === 'root' && objectProto.toString.call(this.props[key]) === '[object String]';
76+
options[key] = useQuery ? document.querySelector(this.props[key]) : this.props[key];
9977
}
100-
return prev;
78+
return options;
10179
}, {});
10280
}
10381

@@ -116,50 +94,42 @@ export default class IntersectionObserver extends React.Component {
11694
this.unobserve();
11795
}
11896
}
119-
warning(
120-
!this.props.hasOwnProperty('onlyOnce'),
121-
'ReactIntersectionObserver: [deprecation] Use the second argument of onChange to unobserve a target instead of onlyOnce. This prop will be removed in the next major version.',
122-
);
97+
// eslint-disable-next-line no-undef
98+
if (process.env.NODE_ENV !== 'production') {
99+
warning(
100+
!this.props.hasOwnProperty('onlyOnce'),
101+
'ReactIntersectionObserver: [deprecation] Use the second argument of onChange to unobserve a target instead of onlyOnce. This prop will be removed in the next major version.',
102+
);
103+
}
123104
};
124105

125106
handleNode = target => {
126107
if (typeof this.props.children.ref === 'function') {
127108
this.props.children.ref(target);
128109
}
129-
if (this.renderedTarget && target && this.renderedTarget !== target) {
110+
/**
111+
* This is a bit ugly: would like to use getSnapshotBeforeUpdate(), but we do not want to depend on
112+
* react-lifecycles-compat to support React versions prior to 16.3 as this extra boolean gets the job done.
113+
*/
114+
this.targetChanged = (this.renderedTarget && target) != null && this.renderedTarget !== target;
115+
if (this.targetChanged) {
130116
this.unobserve();
131-
this.targetChanged = true;
132-
} else {
133-
this.targetChanged = false;
134117
}
135118
this.target = target;
136119
};
137120

138-
compareObserverProps(prevProps) {
139-
return observerOptions
140-
.concat(['disabled'])
141-
.some(option => shallowCompareOptions(this.props[option], prevProps[option]));
142-
}
143-
144-
observe() {
121+
observe = () => {
145122
this.target = isDOMTypeElement(this.target) ? this.target : findDOMNode(this.target);
146-
this.observer = IntersectionObserverContainer.create(callback, this.options);
147-
IntersectionObserverContainer.observe(this);
148-
}
123+
this.observer = createObserver(this.options);
124+
observeElement(this);
125+
};
149126

150127
unobserve = () => {
151128
if (this.target != null) {
152-
IntersectionObserverContainer.unobserve(this);
129+
unobserveElement(this);
153130
}
154131
};
155132

156-
reobserve() {
157-
this.unobserve();
158-
if (!this.props.disabled) {
159-
this.observe();
160-
}
161-
}
162-
163133
componentDidMount() {
164134
// eslint-disable-next-line no-undef
165135
if (process.env.NODE_ENV !== 'production' && parseInt(React.version, 10) < 16) {
@@ -174,8 +144,16 @@ export default class IntersectionObserver extends React.Component {
174144
}
175145

176146
componentDidUpdate(prevProps) {
177-
if (this.targetChanged || this.compareObserverProps(prevProps)) {
178-
this.reobserve();
147+
const propsChanged = observerProps.some(prop => shallowCompare(this.props[prop], prevProps[prop]));
148+
149+
if (propsChanged) {
150+
this.unobserve();
151+
}
152+
153+
if (this.targetChanged || propsChanged) {
154+
if (!this.props.disabled) {
155+
this.observe();
156+
}
179157
}
180158
}
181159

src/IntersectionObserverContainer.js

Lines changed: 0 additions & 83 deletions
This file was deleted.

src/__tests__/IntersectionObserver.spec.js

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
import 'intersection-observer';
33
import React from 'react';
44
import renderer from 'react-test-renderer';
5-
import IntersectionObserver, { callback } from '../IntersectionObserver';
6-
import IntersectionObserverContainer from '../IntersectionObserverContainer';
5+
import IntersectionObserver from '../IntersectionObserver';
6+
import { callback, findObserverElement, observerElementsMap } from '../observer';
77

88
function mockUtilsFunctions() {
99
const utils = require.requireActual('../utils');
@@ -30,7 +30,7 @@ afterAll(() => {
3030
});
3131

3232
afterEach(() => {
33-
IntersectionObserverContainer.clear();
33+
observerElementsMap.clear();
3434
});
3535

3636
test('throws when the property children is not an only child', () => {
@@ -93,7 +93,7 @@ test("should save target in the observer targets' list on mount", () => {
9393
);
9494
const tree = renderer.create(component, { createNodeMock: () => target });
9595
const observer = tree.getInstance().observer;
96-
const retrieved = IntersectionObserverContainer.findElement({ target }, observer);
96+
const retrieved = findObserverElement(observer, { target });
9797

9898
expect(retrieved).toEqual(tree.getInstance());
9999
});
@@ -108,7 +108,7 @@ test("should remove target from the observer targets' list on umount", () => {
108108
const instance = tree.getInstance();
109109
const observer = instance.observer;
110110
tree.unmount();
111-
const retrieved = IntersectionObserverContainer.findElement({ target }, observer);
111+
const retrieved = findObserverElement(observer, { target });
112112

113113
expect(retrieved).toBeNull();
114114
});
@@ -123,14 +123,16 @@ describe('update', () => {
123123
const tree = renderer.create(component, { createNodeMock: () => target });
124124
const instance = tree.getInstance();
125125

126-
const spy = jest.spyOn(instance, 'reobserve');
126+
const spy1 = jest.spyOn(instance, 'observe');
127+
const spy2 = jest.spyOn(instance, 'unobserve');
127128

128129
tree.update(
129130
<IntersectionObserver onChange={noop} rootMargin="20% 10%">
130131
<span />
131132
</IntersectionObserver>,
132133
);
133-
expect(spy).toBeCalled();
134+
expect(spy1).toHaveBeenCalledTimes(1);
135+
expect(spy2).toHaveBeenCalledTimes(1);
134136
});
135137

136138
test('should cleanup when tree reconciliation has led to a full rebuild', () => {
@@ -165,7 +167,7 @@ describe('update', () => {
165167
</IntersectionObserver>,
166168
);
167169

168-
expect(spy1).toHaveBeenCalledTimes(2);
170+
expect(spy1).toHaveBeenCalledTimes(1);
169171
expect(spy2).toHaveBeenCalledTimes(1);
170172
expect(instance.target).toBe(target);
171173
});
@@ -249,7 +251,7 @@ describe('update', () => {
249251
});
250252

251253
test('should be defensive against unobserving nullified nodes', () => {
252-
const spy = jest.spyOn(IntersectionObserverContainer, 'unobserve');
254+
const sizeAfterObserving = observerElementsMap.size + 1;
253255
const component = (
254256
<IntersectionObserver onChange={noop}>
255257
<span />
@@ -261,7 +263,7 @@ describe('update', () => {
261263
tree.getInstance().target = null;
262264
tree.getInstance().unobserve();
263265

264-
expect(spy).not.toBeCalled();
266+
expect(observerElementsMap.size).toBe(sizeAfterObserving);
265267
});
266268

267269
test('should not reobserve on a second render after root changed the first time', () => {
@@ -281,7 +283,8 @@ describe('update', () => {
281283
},
282284
});
283285
const instance = tree.getInstance();
284-
const spy = jest.spyOn(instance, 'reobserve');
286+
const spy1 = jest.spyOn(instance, 'observe');
287+
const spy2 = jest.spyOn(instance, 'unobserve');
285288

286289
tree.update(
287290
<IntersectionObserver onChange={noop}>
@@ -295,7 +298,8 @@ describe('update', () => {
295298
</IntersectionObserver>,
296299
);
297300

298-
expect(spy).toHaveBeenCalledTimes(1);
301+
expect(spy1).toHaveBeenCalledTimes(1);
302+
expect(spy2).toHaveBeenCalledTimes(1);
299303
});
300304
});
301305

@@ -312,7 +316,7 @@ describe('callback', () => {
312316
const instance = renderer.create(component, { createNodeMock: () => target1 }).getInstance();
313317
renderer.create(React.cloneElement(component), { createNodeMock: () => target2 });
314318

315-
expect(IntersectionObserverContainer.count()).toBe(1);
319+
expect(observerElementsMap.size).toBe(1);
316320

317321
const boundingClientRect = {};
318322
const intersectionRect = {};
@@ -431,10 +435,10 @@ describe('handleChange', () => {
431435
<span />
432436
</IntersectionObserver>
433437
);
434-
const spy = jest.spyOn(IntersectionObserverContainer, 'observe');
438+
const sizeBefore = observerElementsMap.size;
435439
renderer.create(component, { createNodeMock: () => target });
436440

437-
expect(spy).not.toBeCalled();
441+
expect(observerElementsMap.size).toBe(sizeBefore);
438442
});
439443

440444
test('should observe if not disabled', () => {
@@ -443,10 +447,10 @@ describe('handleChange', () => {
443447
<span />
444448
</IntersectionObserver>
445449
);
446-
const spy = jest.spyOn(IntersectionObserverContainer, 'observe');
450+
const sizeAfterObserving = observerElementsMap.size + 1;
447451
renderer.create(component, { createNodeMock: () => target }).getInstance();
448452

449-
expect(spy).toBeCalled();
453+
expect(observerElementsMap.size).toBe(sizeAfterObserving);
450454
});
451455

452456
test('should observe if no longer disabled', () => {
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// Jest Snapshot v1, https://goo.gl/fbAQLP
22

3-
exports[`#findElement given an entry without target property throws 1`] = `"Cannot read property 'observe' of undefined"`;
3+
exports[`#findObserverElement given an entry without target property throws 1`] = `"Cannot read property 'observe' of undefined"`;
44

55
exports[`#getPooled throws if rootMargin cannot be parsed 1`] = `"rootMargin must be a string literal containing pixels and/or percent values"`;

0 commit comments

Comments
 (0)