Test rxjs instead of reconciler, add ability to draw layers, test performance

This commit is contained in:
cybice 2016-06-13 17:51:37 +03:00
parent c4471ae1fa
commit 100c219e1e
16 changed files with 313 additions and 27 deletions

View File

@ -15,7 +15,7 @@ export const gMap = ({
style, hoverDistance, options,
mapParams: { center, zoom },
onChange, onChildMouseEnter, onChildMouseLeave,
markers, hoveredMarkerId,
markers, // hoveredMarkerId,
}) => (
<GoogleMapReact
style={style}
@ -29,13 +29,6 @@ export const gMap = ({
>
{
markers
.map(({ ...markerProps, id }) => (
<SimpleMarker
key={id}
hovered={id === hoveredMarkerId}
{...markerProps}
/>
))
}
</GoogleMapReact>
);
@ -66,7 +59,7 @@ export const gMapHOC = compose(
)
),
withState('hoveredMarkerId', 'setHoveredMarkerId', -1),
withState('mapParams', 'setMapParams', { center: susolvkaCoords, zoom: 10 }),
withState('mapParams', 'setMapParams', { center: susolvkaCoords, zoom: 6 }),
// describe events
withHandlers({
onChange: ({ setMapParams }) => ({ center, zoom, bounds }) => {
@ -86,6 +79,19 @@ export const gMapHOC = compose(
? markers.filter(m => ptInBounds(bounds, m))
: [],
})
),
withPropsOnChange(
['markers'],
({ markers }) => ({
markers: markers
.map(({ ...markerProps, id }) => (
<SimpleMarker
key={id}
id={id}
{...markerProps}
/>
)),
})
)
);

107
develop/GMapOptim.js Normal file
View File

@ -0,0 +1,107 @@
// Example to test the React Reconciler, and even this
// is 100x faster in development mode, I see no difference
// with NODE_ENV==='production'
// so the simple example with same params at ./GMap.js works with same speed
// works very well
import React from 'react';
import compose from 'recompose/compose';
import defaultProps from 'recompose/defaultProps';
import withStateSelector from './utils/withStateSelector';
import withHandlers from 'recompose/withHandlers';
import withState from 'recompose/withState';
import withPropsOnChange from 'recompose/withPropsOnChange';
import ptInBounds from './utils/ptInBounds';
import GoogleMapReact from '../src';
// import SimpleMarker from './markers/SimpleMarker';
import ReactiveMarker from './markers/ReactiveMarker';
import { createSelector } from 'reselect';
import { susolvkaCoords, generateMarkers } from './data/fakeData';
import props2Stream from './utils/props2Stream';
export const gMap = ({
style, hoverDistance, options,
mapParams: { center, zoom },
onChange, onChildMouseEnter, onChildMouseLeave,
markers,
}) => (
<GoogleMapReact
style={style}
options={options}
hoverDistance={hoverDistance}
center={center}
zoom={zoom}
onChange={onChange}
onChildMouseEnter={onChildMouseEnter}
onChildMouseLeave={onChildMouseLeave}
experimental
>
{markers}
</GoogleMapReact>
);
export const gMapHOC = compose(
defaultProps({
clusterRadius: 60,
hoverDistance: 30,
options: {
minZoom: 3,
maxZoom: 15,
},
style: {
position: 'relative',
margin: 0,
padding: 0,
flex: 1,
},
}),
// withState so you could change markers if you want
withStateSelector(
'markers',
'setMarkers',
() => createSelector(
({ route: { markersCount = 20 } }) => markersCount,
(markersCount) => generateMarkers(markersCount)
)
),
withState('hoveredMarkerId', 'setHoveredMarkerId', -1),
withState('mapParams', 'setMapParams', { center: susolvkaCoords, zoom: 6 }),
// describe events
withHandlers({
onChange: ({ setMapParams }) => ({ center, zoom, bounds }) => {
setMapParams({ center, zoom, bounds });
},
onChildMouseEnter: ({ setHoveredMarkerId }) => (hoverKey, { id }) => {
setHoveredMarkerId(id);
},
onChildMouseLeave: ({ setHoveredMarkerId }) => () => {
setHoveredMarkerId(-1);
},
}),
withPropsOnChange(
['markers', 'mapParams'],
({ markers, mapParams: { bounds } }) => ({
markers: bounds
? markers.filter(m => ptInBounds(bounds, m))
: [],
})
),
props2Stream('hoveredMarkerId'),
withPropsOnChange(
['markers', 'hoveredMarkerId$'],
({ markers, hoveredMarkerId$ }) => ({
markers: markers
.map(({ ...markerProps, id }) => (
<ReactiveMarker
key={id}
id={id}
hoveredMarkerId$={hoveredMarkerId$}
{...markerProps}
/>
)),
})
)
);
export default gMapHOC(gMap);

View File

@ -12,6 +12,7 @@ export class Layout extends Component { // eslint-disable-line
<header className={header}>
<div className={links}>
<Link to="/">Multi Markers</Link>
<Link to="/hoverunoptim">Hover unoptim</Link>
<Link to="/hoveroptim">Hover optim</Link>
</div>
<div>

View File

@ -4,6 +4,7 @@ import { render } from 'react-dom';
import { Router, Route, IndexRoute, browserHistory } from 'react-router';
import Layout from './Layout';
import GMap from './GMap';
import GMapOptim from './GMapOptim';
import 'normalize.css/normalize.css';
import './Main.sass';
@ -13,7 +14,8 @@ const mountNode = document.getElementById('app');
render(
<Router history={browserHistory}>
<Route path="/" component={Layout}>
<Route markersCount={200} path="hoveroptim" component={GMap} />
<Route markersCount={500} path="hoverunoptim" component={GMap} />
<Route markersCount={500} path="hoveroptim" component={GMapOptim} />
<IndexRoute markersCount={20} component={GMap} />
</Route>
</Router>

View File

@ -1,11 +1,20 @@
import path from 'path'; // eslint-disable-line no-var
import autoprefixer from 'autoprefixer'; // eslint-disable-line no-var
import webpack from 'webpack';
export default {
devtool: 'cheap-module-eval-source-map',
postcss: [
autoprefixer({ browsers: ['last 2 versions'] }),
],
plugins: [
new webpack.DefinePlugin(process.env.NODE_ENV
? {
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
}
: {}
),
],
module: {
loaders: [
{

View File

@ -7,7 +7,7 @@ import { Motion, spring } from 'react-motion';
import clusterMarkerStyles from './ClusterMarker.sass';
export const clusterMarker = ({
styles, text, hovered,
styles, text, hovered, $hover,
defaultMotionStyle, motionStyle,
}) => (
<Motion
@ -20,7 +20,7 @@ export const clusterMarker = ({
className={styles.marker}
style={{
transform: `translate3D(0,0,0) scale(${scale}, ${scale})`,
zIndex: hovered ? 1 : 0,
zIndex: (hovered || $hover) ? 1 : 0,
}}
>
<div
@ -57,15 +57,16 @@ export const clusterMarkerHOC = compose(
})
),
withPropsOnChange(
['hovered'],
['hovered', '$hover'],
({
hovered, hoveredScale, defaultScale,
hovered, $hover, hoveredScale, defaultScale,
stiffness, damping, precision,
}) => ({
$hover,
hovered,
motionStyle: {
scale: spring(
hovered ? hoveredScale : defaultScale,
(hovered || $hover) ? hoveredScale : defaultScale,
{ stiffness, damping, precision }
),
},

View File

@ -0,0 +1,26 @@
import React from 'react';
import compose from 'recompose/compose';
import SimpleMarker from './SimpleMarker';
import stream2Props from '../utils/stream2Props';
import 'rxjs/add/operator/filter';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/do';
import 'rxjs/add/operator/scan';
import 'rxjs/add/operator/distinctUntilChanged';
// import defaultProps from 'recompose/defaultProps';
// import reactiveMarkerStyles from './reactiveMarker.scss';
export const reactiveMarker = (props) => (
<SimpleMarker {...props} />
);
export const reactiveMarkerHOC = compose(
stream2Props(({ id, hoveredMarkerId$ }) => (
hoveredMarkerId$
.map(hoveredMarkerId => hoveredMarkerId === id)
.distinctUntilChanged()
.map(v => ({ hovered: v }))
))
);
export default reactiveMarkerHOC(reactiveMarker);

View File

@ -7,7 +7,7 @@ import { clusterMarkerHOC } from './ClusterMarker.js';
import simpleMarkerStyles from './SimpleMarker.sass';
export const simpleMarker = ({
styles, hovered,
styles, hovered, $hover,
defaultMotionStyle, motionStyle,
}) => (
<Motion
@ -20,7 +20,7 @@ export const simpleMarker = ({
className={styles.marker}
style={{
transform: `translate3D(0,0,0) scale(${scale}, ${scale})`,
zIndex: hovered ? 1 : 0,
zIndex: (hovered || $hover) ? 1 : 0,
}}
>
</div>
@ -32,7 +32,7 @@ export const simpleMarker = ({
export const simpleMarkerHOC = compose(
defaultProps({
styles: simpleMarkerStyles,
initialScale: 0.3,
initialScale: 0.6,
defaultScale: 0.6,
hoveredScale: 0.7,
}),

View File

@ -0,0 +1,18 @@
/* eslint-disable max-len */
// `npm bin`/mocha --compilers js:babel-register --require babel-polyfill --reporter min --watch './develop/**/*.spec.js'
/* eslint-enable max-len */
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import 'rxjs/add/operator/distinctUntilChanged';
describe('Playground', () => {
it('Play', async () => {
const comparator = (a, b) => a === b;
const props$ = (new BehaviorSubject(1))
.distinctUntilChanged(comparator);
props$.subscribe(v => console.log(v));
props$.next(1);
props$.next(2);
props$.next(1);
});
});

13
develop/utils/omit.js Normal file
View File

@ -0,0 +1,13 @@
// https://github.com/acdlite/recompose/blob/master/src/packages/recompose/utils/omit.js
const omit = (obj, keys) => {
const { ...rest } = obj;
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
if (rest.hasOwnProperty(key)) {
delete rest[key];
}
}
return rest;
};
export default omit;

14
develop/utils/pick.js Normal file
View File

@ -0,0 +1,14 @@
// https://github.com/acdlite/recompose/blob/master/src/packages/recompose/utils/pick.js
const pick = (obj, keys) => {
const result = {};
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
if (obj.hasOwnProperty(key)) {
result[key] = obj[key];
}
}
return result;
};
export default pick;

View File

@ -0,0 +1,28 @@
import { Component } from 'react';
import createEagerFactory from './createEagerFactory';
import createHelper from 'recompose/createHelper';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import 'rxjs/add/operator/distinctUntilChanged';
import omit from './omit';
const prop2Stream = (propName, comparator = (a, b) => a === b) =>
BaseComponent => {
const factory = createEagerFactory(BaseComponent);
return class extends Component {
props$ = (new BehaviorSubject(this.props[propName]))
.distinctUntilChanged(comparator);
componentWillReceiveProps(nextProps) {
this.props$.next(nextProps[propName]);
}
render() {
return factory({
...omit(this.props, [propName]),
[`${propName}$`]: this.props$,
});
}
};
};
export default createHelper(prop2Stream, 'prop2Stream');

View File

@ -0,0 +1,31 @@
import { Component } from 'react';
import createEagerFactory from './createEagerFactory';
import createHelper from 'recompose/createHelper';
// if stream prop will change this will fail,
// this is expected behavior
const stream2Props = (props2Stream) =>
BaseComponent => {
const factory = createEagerFactory(BaseComponent);
return class extends Component {
state = {};
componentWillMount() {
this.subscription = props2Stream(this.props)
.subscribe(value => this.setState({ value }));
}
componentWillUnmount() {
this.subscription.unsubscribe();
}
render() {
return factory({
...this.props,
...this.state.value,
});
}
};
};
export default createHelper(stream2Props, 'stream2Props');

View File

@ -69,7 +69,7 @@
"file-loader": "^0.8.5",
"jsdom": "^6.5.1",
"kotatsu": "^0.14.0",
"lodash": "^4.6.1",
"lodash": "^4.13.1",
"mocha": "^2.3.3",
"node-sass": "^3.7.0",
"normalize.css": "^4.1.1",
@ -82,6 +82,7 @@
"recompose": "^0.19.0",
"reselect": "^2.5.1",
"rimraf": "^2.4.3",
"rxjs": "^5.0.0-beta.8",
"sass-loader": "^3.2.0",
"style-loader": "^0.13.1",
"url-loader": "^0.5.7",

View File

@ -435,6 +435,7 @@ export default class GoogleMap extends Component {
ReactDOM.render(
(
<GoogleMapMarkers
experimental={this_.props.experimental}
onChildClick={this_._onChildClick}
onChildMouseDown={this_._onChildMouseDown}
onChildMouseEnter={this_._onChildMouseEnter}
@ -873,6 +874,7 @@ export default class GoogleMap extends Component {
const mapMarkerPrerender = !this.state.overlayCreated
? (
<GoogleMapMarkersPrerender
experimental={this.props.experimental}
onChildClick={this._onChildClick}
onChildMouseDown={this._onChildMouseDown}
onChildMouseEnter={this._onChildMouseEnter}

View File

@ -1,5 +1,6 @@
import React, { PropTypes, Component } from 'react';
import shallowEqual from 'fbjs/lib/shallowEqual';
import omit from './utils/omit';
const mainStyle = {
width: '100%',
@ -56,6 +57,14 @@ export default class GoogleMapMarkers extends Component {
}
shouldComponentUpdate(nextProps, nextState) {
if (this.props.experimental === true) {
return !shallowEqual(this.props, nextProps) ||
!shallowEqual(
omit(this.state, ['hoverKey']),
omit(nextState, ['hoverKey'])
);
}
return !shallowEqual(this.props, nextProps) || !shallowEqual(this.state, nextState);
}
@ -79,7 +88,6 @@ export default class GoogleMapMarkers extends Component {
}
const prevChildCount = (this.state.children || []).length;
const state = this._getState();
this.setState(
@ -174,6 +182,13 @@ export default class GoogleMapMarkers extends Component {
const hoverDistance = this.props.getHoverDistance();
React.Children.forEach(this.state.children, (child, childIndex) => {
// layers
if (child.props.latLng === undefined &&
child.props.lat === undefined &&
child.props.lng === undefined) {
return;
}
const childKey = child.key !== undefined && child.key !== null ? child.key : childIndex;
const dist = this.props.distanceToMouse(this.dimesionsCache_[childKey], mp, child.props);
if (dist < hoverDistance) {
@ -211,14 +226,26 @@ export default class GoogleMapMarkers extends Component {
render() {
const mainElementStyle = this.props.style || mainStyle;
this.dimesionsCache_ = {};
const markers = React.Children.map(this.state.children, (child, childIndex) => {
const pt = this.props.geoService.project({
lat: child.props.lat,
lng: child.props.lng,
}, this.props.projectFromLeftTop);
if (child.props.latLng === undefined &&
child.props.lat === undefined &&
child.props.lng === undefined) {
return (
React.cloneElement(child, {
$geoService: this.props.geoService,
$onMouseAllow: this._onMouseAllow,
$prerender: this.props.prerender,
})
);
}
const latLng = child.props.latLng !== undefined
? child.props.latLng
: { lat: child.props.lat, lng: child.props.lng };
const pt = this.props.geoService.project(latLng, this.props.projectFromLeftTop);
const stylePtPos = {
left: pt.x,
@ -238,11 +265,11 @@ export default class GoogleMapMarkers extends Component {
// to prevent rerender on child element i need to pass
// const params $getDimensions and $dimensionKey instead of dimension object
const childKey = child.key !== undefined && child.key !== null ? child.key : childIndex;
this.dimesionsCache_[childKey] = {
x: pt.x + dx,
y: pt.y + dy,
lat: child.props.lat,
lng: child.props.lng,
...latLng,
};
return (