From 7d3445b306540aa64d0eac738dcf8b9bc44a01ee Mon Sep 17 00:00:00 2001 From: rhysd Date: Sat, 20 Aug 2016 23:54:30 +0900 Subject: [PATCH 1/3] use passive event to avoid blocking scroll --- react-list.es6 | 4 ++-- react-list.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/react-list.es6 b/react-list.es6 index 72c972e..66f17a8 100644 --- a/react-list.es6 +++ b/react-list.es6 @@ -70,7 +70,7 @@ export default class extends Component { componentDidMount() { this.updateFrame = this.updateFrame.bind(this); - window.addEventListener('resize', this.updateFrame); + window.addEventListener('resize', this.updateFrame, {passive: true}); this.updateFrame(this.scrollTo.bind(this, this.props.initialIndex)); } @@ -219,7 +219,7 @@ export default class extends Component { prev.removeEventListener('scroll', this.updateFrame); prev.removeEventListener('mousewheel', NOOP); } - this.scrollParent.addEventListener('scroll', this.updateFrame); + this.scrollParent.addEventListener('scroll', this.updateFrame, {passive: true}); this.scrollParent.addEventListener('mousewheel', NOOP); } diff --git a/react-list.js b/react-list.js index a2d60b1..0eb1209 100644 --- a/react-list.js +++ b/react-list.js @@ -137,7 +137,7 @@ key: 'componentDidMount', value: function componentDidMount() { this.updateFrame = this.updateFrame.bind(this); - window.addEventListener('resize', this.updateFrame); + window.addEventListener('resize', this.updateFrame, { passive: true }); this.updateFrame(this.scrollTo.bind(this, this.props.initialIndex)); } }, { @@ -314,7 +314,7 @@ prev.removeEventListener('scroll', this.updateFrame); prev.removeEventListener('mousewheel', NOOP); } - this.scrollParent.addEventListener('scroll', this.updateFrame); + this.scrollParent.addEventListener('scroll', this.updateFrame, { passive: true }); this.scrollParent.addEventListener('mousewheel', NOOP); } }, { From 36c11d3f38fb606f761ec63442006dce2d821f3c Mon Sep 17 00:00:00 2001 From: rhysd Date: Sun, 21 Aug 2016 00:31:13 +0900 Subject: [PATCH 2/3] use shallow compare for shouldComponentUpdate() --- package.json | 3 ++- react-list.es6 | 10 ++-------- react-list.js | 22 +++++++--------------- 3 files changed, 11 insertions(+), 24 deletions(-) diff --git a/package.json b/package.json index a3411a3..c6a9948 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,8 @@ }, "peerDependencies": { "react": "0.14 || >= 15.0.0-rc.1 < 16", - "react-dom": "0.14 || >= 15.0.0-rc.1 < 16" + "react-dom": "0.14 || >= 15.0.0-rc.1 < 16", + "react-addons-shallow-compare": "0.14 || >= 15.0.0-rc.1 < 16" }, "devDependencies": { "cogs": "1.0.7", diff --git a/react-list.es6 b/react-list.es6 index 66f17a8..212a875 100644 --- a/react-list.es6 +++ b/react-list.es6 @@ -1,15 +1,9 @@ import React, {Component, PropTypes} from 'react'; import ReactDOM from 'react-dom'; +import shallowCompare from 'react-addons-shallow-compare'; const {findDOMNode} = ReactDOM; -const isEqualSubset = (a, b) => { - for (let key in a) if (a[key] !== b[key]) return false; - return true; -}; - -const isEqual = (a, b) => isEqualSubset(a, b) && isEqualSubset(b, a); - const CLIENT_SIZE_KEYS = {x: 'clientWidth', y: 'clientHeight'}; const CLIENT_START_KEYS = {x: 'clientTop', y: 'clientLeft'}; const INNER_SIZE_KEYS = {x: 'innerWidth', y: 'innerHeight'}; @@ -75,7 +69,7 @@ export default class extends Component { } shouldComponentUpdate(props, state) { - return !isEqual(props, this.props) || !isEqual(state, this.state); + return shallowCompare(this, props, state); } componentDidUpdate() { diff --git a/react-list.js b/react-list.js index 0eb1209..c47c877 100644 --- a/react-list.js +++ b/react-list.js @@ -1,16 +1,16 @@ (function (global, factory) { if (typeof define === 'function' && define.amd) { - define(['exports', 'module', 'react', 'react-dom'], factory); + define(['exports', 'module', 'react', 'react-dom', 'react-addons-shallow-compare'], factory); } else if (typeof exports !== 'undefined' && typeof module !== 'undefined') { - factory(exports, module, require('react'), require('react-dom')); + factory(exports, module, require('react'), require('react-dom'), require('react-addons-shallow-compare')); } else { var mod = { exports: {} }; - factory(mod.exports, mod, global.React, global.ReactDOM); + factory(mod.exports, mod, global.React, global.ReactDOM, global.shallowCompare); global.ReactList = mod.exports; } -})(this, function (exports, module, _react, _reactDom) { +})(this, function (exports, module, _react, _reactDom, _reactAddonsShallowCompare) { 'use strict'; var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); @@ -27,17 +27,9 @@ var _ReactDOM = _interopRequireDefault(_reactDom); - var findDOMNode = _ReactDOM['default'].findDOMNode; - - var isEqualSubset = function isEqualSubset(a, b) { - for (var key in a) { - if (a[key] !== b[key]) return false; - }return true; - }; + var _shallowCompare = _interopRequireDefault(_reactAddonsShallowCompare); - var isEqual = function isEqual(a, b) { - return isEqualSubset(a, b) && isEqualSubset(b, a); - }; + var findDOMNode = _ReactDOM['default'].findDOMNode; var CLIENT_SIZE_KEYS = { x: 'clientWidth', y: 'clientHeight' }; var CLIENT_START_KEYS = { x: 'clientTop', y: 'clientLeft' }; @@ -143,7 +135,7 @@ }, { key: 'shouldComponentUpdate', value: function shouldComponentUpdate(props, state) { - return !isEqual(props, this.props) || !isEqual(state, this.state); + return (0, _shallowCompare['default'])(this, props, state); } }, { key: 'componentDidUpdate', From 6d8bf551a9a8ba45c3b5b48a6875e2d8ee154e3d Mon Sep 17 00:00:00 2001 From: rhysd Date: Sun, 21 Aug 2016 01:18:05 +0900 Subject: [PATCH 3/3] use requestAnimationFrame not to prevent user input by component update --- react-list.es6 | 32 +++++++++++++++++++++++++++++--- react-list.js | 35 ++++++++++++++++++++++++++++++----- 2 files changed, 59 insertions(+), 8 deletions(-) diff --git a/react-list.es6 b/react-list.es6 index 212a875..cbd17ff 100644 --- a/react-list.es6 +++ b/react-list.es6 @@ -15,6 +15,12 @@ const SCROLL_START_KEYS = {x: 'scrollLeft', y: 'scrollTop'}; const SIZE_KEYS = {x: 'width', y: 'height'}; const NOOP = () => {}; +const requestAnimationFrame = + window.requestAnimationFrame || + window.mozRequestAnimationFrame || + window.webkitRequestAnimationFrame || + window.msRequestAnimationFrame; +const cancelAnimationFrame = window.cancelAnimationFrame || window.mozCancelAnimationFrame; export default class extends Component { static displayName = 'ReactList'; @@ -55,6 +61,7 @@ export default class extends Component { this.constrain(initialIndex, pageSize, itemsPerRow, this.props); this.state = {from, size, itemsPerRow}; this.cache = {}; + this.rafId = null; } componentWillReceiveProps(next) { @@ -80,6 +87,9 @@ export default class extends Component { window.removeEventListener('resize', this.updateFrame); this.scrollParent.removeEventListener('scroll', this.updateFrame); this.scrollParent.removeEventListener('mousewheel', NOOP); + if (this.rafId !== null) { + cancelAnimationFrame(this.rafId); + } } getOffset(el) { @@ -217,6 +227,22 @@ export default class extends Component { this.scrollParent.addEventListener('mousewheel', NOOP); } + setNextState(state, cb) { + if (!requestAnimationFrame) { + this.setState(state, cb); + return; + } + + if (this.rafId !== null) { + return; + } + + this.rafId = requestAnimationFrame(() => { + this.setState(state, cb); + this.rafId = null; + }); + } + updateSimpleFrame(cb) { const {end} = this.getStartAndEnd(); const itemEls = findDOMNode(this.items).children; @@ -233,7 +259,7 @@ export default class extends Component { if (elEnd > end) return cb(); const {pageSize, length} = this.props; - this.setState({size: Math.min(this.state.size + pageSize, length)}, cb); + this.setNextState({size: Math.min(this.state.size + pageSize, length)}, cb); } updateVariableFrame(cb) { @@ -265,7 +291,7 @@ export default class extends Component { ++size; } - this.setState({from, size}, cb); + this.setNextState({from, size}, cb); } updateUniformFrame(cb) { @@ -282,7 +308,7 @@ export default class extends Component { this.props ); - return this.setState({itemsPerRow, from, itemSize, size}, cb); + return this.setNextState({itemsPerRow, from, itemSize, size}, cb); } getSpaceBefore(index, cache = {}) { diff --git a/react-list.js b/react-list.js index c47c877..2b35cb9 100644 --- a/react-list.js +++ b/react-list.js @@ -42,6 +42,8 @@ var SIZE_KEYS = { x: 'width', y: 'height' }; var NOOP = function NOOP() {}; + var requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame; + var cancelAnimationFrame = window.cancelAnimationFrame || window.mozCancelAnimationFrame; var _default = (function (_Component) { _inherits(_default, _Component); @@ -113,6 +115,7 @@ this.state = { from: from, size: size, itemsPerRow: itemsPerRow }; this.cache = {}; + this.rafId = null; } _createClass(_default, [{ @@ -148,6 +151,9 @@ window.removeEventListener('resize', this.updateFrame); this.scrollParent.removeEventListener('scroll', this.updateFrame); this.scrollParent.removeEventListener('mousewheel', NOOP); + if (this.rafId !== null) { + cancelAnimationFrame(this.rafId); + } } }, { key: 'getOffset', @@ -309,6 +315,25 @@ this.scrollParent.addEventListener('scroll', this.updateFrame, { passive: true }); this.scrollParent.addEventListener('mousewheel', NOOP); } + }, { + key: 'setNextState', + value: function setNextState(state, cb) { + var _this = this; + + if (!requestAnimationFrame) { + this.setState(state, cb); + return; + } + + if (this.rafId !== null) { + return; + } + + this.rafId = requestAnimationFrame(function () { + _this.setState(state, cb); + _this.rafId = null; + }); + } }, { key: 'updateSimpleFrame', value: function updateSimpleFrame(cb) { @@ -333,7 +358,7 @@ var pageSize = _props5.pageSize; var length = _props5.length; - this.setState({ size: Math.min(this.state.size + pageSize, length) }, cb); + this.setNextState({ size: Math.min(this.state.size + pageSize, length) }, cb); } }, { key: 'updateVariableFrame', @@ -372,7 +397,7 @@ ++size; } - this.setState({ from: from, size: size }, cb); + this.setNextState({ from: from, size: size }, cb); } }, { key: 'updateUniformFrame', @@ -394,7 +419,7 @@ var from = _constrain2.from; var size = _constrain2.size; - return this.setState({ itemsPerRow: itemsPerRow, from: from, itemSize: itemSize, size: size }, cb); + return this.setNextState({ itemsPerRow: itemsPerRow, from: from, itemSize: itemSize, size: size }, cb); } }, { key: 'getSpaceBefore', @@ -534,7 +559,7 @@ }, { key: 'renderItems', value: function renderItems() { - var _this = this; + var _this2 = this; var _props8 = this.props; var itemRenderer = _props8.itemRenderer; @@ -547,7 +572,7 @@ for (var i = 0; i < size; ++i) { items.push(itemRenderer(from + i, i)); }return itemsRenderer(items, function (c) { - return _this.items = c; + return _this2.items = c; }); } }, {