我希望创建一个可拖动(即可通过鼠标重新定位)的React组件,它似乎必然涉及全局状态和分散的事件处理程序。我可以在JS文件中使用全局变量,甚至可以将它包装在一个很好的闭包接口中,但我想知道是否有一种方法可以更好地与React配合。
此外,由于我以前从未在原始JavaScript中这样做过,所以我想看看专家是如何做到这一点的,以确保我已经处理了所有的角落案例,特别是当它们与React相关时。
谢谢。
发布于 2014-01-05 07:13:25
我可能应该把这篇文章写成博客文章,但这里有一个非常可靠的例子。
评论应该能很好地解释事情,但如果你有问题请告诉我。
下面是可以使用的小提琴:http://jsfiddle.net/Af9Jt/2/
var Draggable = React.createClass({
getDefaultProps: function () {
return {
// allow the initial position to be passed in as a prop
initialPos: {x: 0, y: 0}
}
},
getInitialState: function () {
return {
pos: this.props.initialPos,
dragging: false,
rel: null // position relative to the cursor
}
},
// we could get away with not having this (and just having the listeners on
// our div), but then the experience would be possibly be janky. If there's
// anything w/ a higher z-index that gets in the way, then you're toast,
// etc.
componentDidUpdate: function (props, state) {
if (this.state.dragging && !state.dragging) {
document.addEventListener('mousemove', this.onMouseMove)
document.addEventListener('mouseup', this.onMouseUp)
} else if (!this.state.dragging && state.dragging) {
document.removeEventListener('mousemove', this.onMouseMove)
document.removeEventListener('mouseup', this.onMouseUp)
}
},
// calculate relative position to the mouse and set dragging=true
onMouseDown: function (e) {
// only left mouse button
if (e.button !== 0) return
var pos = $(this.getDOMNode()).offset()
this.setState({
dragging: true,
rel: {
x: e.pageX - pos.left,
y: e.pageY - pos.top
}
})
e.stopPropagation()
e.preventDefault()
},
onMouseUp: function (e) {
this.setState({dragging: false})
e.stopPropagation()
e.preventDefault()
},
onMouseMove: function (e) {
if (!this.state.dragging) return
this.setState({
pos: {
x: e.pageX - this.state.rel.x,
y: e.pageY - this.state.rel.y
}
})
e.stopPropagation()
e.preventDefault()
},
render: function () {
// transferPropsTo will merge style & other props passed into our
// component to also be on the child DIV.
return this.transferPropsTo(React.DOM.div({
onMouseDown: this.onMouseDown,
style: {
left: this.state.pos.x + 'px',
top: this.state.pos.y + 'px'
}
}, this.props.children))
}
})
关于国有制等问题的思考。
“谁应该拥有什么样的国家”从一开始就是一个需要回答的重要问题。在“可拖动”组件的情况下,我可以看到几个不同的场景。
场景1
父级应拥有可拖动对象的当前位置。在这种情况下,可拖动对象仍然拥有"am I dragging“状态,但只要发生mousemove事件,就会调用this.props.onChange(x, y)
。
场景2
父对象只需要拥有“固定位置”,因此可拖动对象将拥有它的“拖动位置”,但在鼠标向上时,它将调用this.props.onChange(x, y)
,并将最终决策推迟到父对象。如果父对象不喜欢可拖动对象的结束位置,它就不会更新它的状态,并且可拖动对象将在拖动之前“快速返回”到它的初始位置。
混入还是组件?
@ssorallen指出,因为"draggable“更多的是一个属性,而不是它本身的一个东西,所以它可能更适合作为一个mixin。我使用混入的经验有限,所以我还没有看到它们如何在复杂的情况下起到帮助或阻碍作用。这可能是最好的选择。
发布于 2016-08-28 23:44:23
Jared Forsyth的回答大错特错,而且已经过时了。它遵循一整套反模式,例如usage of stopPropagation
、initializing state from props、jQuery的用法、状态中的嵌套对象以及一些奇怪的dragging
状态字段。如果被重写,解决方案将如下所示,但它仍然会在每次鼠标移动时强制进行虚拟DOM协调,并且性能不是很好。
UPD。我的回答大错特错,而且过时了。现在,代码通过使用本地事件处理程序和样式更新缓解了React组件生命周期缓慢的问题,使用了transform
,因为它不会导致回流,并通过requestAnimationFrame
限制DOM更改。现在,我尝试过的每种浏览器的速度都是60帧/秒。
const throttle = (f) => {
let token = null, lastArgs = null;
const invoke = () => {
f(...lastArgs);
token = null;
};
const result = (...args) => {
lastArgs = args;
if (!token) {
token = requestAnimationFrame(invoke);
}
};
result.cancel = () => token && cancelAnimationFrame(token);
return result;
};
class Draggable extends React.PureComponent {
_relX = 0;
_relY = 0;
_ref = React.createRef();
_onMouseDown = (event) => {
if (event.button !== 0) {
return;
}
const {scrollLeft, scrollTop, clientLeft, clientTop} = document.body;
// Try to avoid calling `getBoundingClientRect` if you know the size
// of the moving element from the beginning. It forces reflow and is
// the laggiest part of the code right now. Luckily it's called only
// once per click.
const {left, top} = this._ref.current.getBoundingClientRect();
this._relX = event.pageX - (left + scrollLeft - clientLeft);
this._relY = event.pageY - (top + scrollTop - clientTop);
document.addEventListener('mousemove', this._onMouseMove);
document.addEventListener('mouseup', this._onMouseUp);
event.preventDefault();
};
_onMouseUp = (event) => {
document.removeEventListener('mousemove', this._onMouseMove);
document.removeEventListener('mouseup', this._onMouseUp);
event.preventDefault();
};
_onMouseMove = (event) => {
this.props.onMove(
event.pageX - this._relX,
event.pageY - this._relY,
);
event.preventDefault();
};
_update = throttle(() => {
const {x, y} = this.props;
this._ref.current.style.transform = `translate(${x}px, ${y}px)`;
});
componentDidMount() {
this._ref.current.addEventListener('mousedown', this._onMouseDown);
this._update();
}
componentDidUpdate() {
this._update();
}
componentWillUnmount() {
this._ref.current.removeEventListener('mousedown', this._onMouseDown);
this._update.cancel();
}
render() {
return (
<div className="draggable" ref={this._ref}>
{this.props.children}
</div>
);
}
}
class Test extends React.PureComponent {
state = {
x: 100,
y: 200,
};
_move = (x, y) => this.setState({x, y});
// you can implement grid snapping logic or whatever here
/*
_move = (x, y) => this.setState({
x: ~~((x - 5) / 10) * 10 + 5,
y: ~~((y - 5) / 10) * 10 + 5,
});
*/
render() {
const {x, y} = this.state;
return (
<Draggable x={x} y={y} onMove={this._move}>
Drag me
</Draggable>
);
}
}
ReactDOM.render(
<Test />,
document.getElementById('container'),
);
和一些CSS
.draggable {
/* just to size it to content */
display: inline-block;
/* opaque background is important for performance */
background: white;
/* avoid selecting text while dragging */
user-select: none;
}
发布于 2020-05-08 04:51:00
这里有一个简单的现代方法,在ES6中使用useState
、useEffect
和useRef
。
import React, { useRef, useState, useEffect } from 'react'
const quickAndDirtyStyle = {
width: "200px",
height: "200px",
background: "#FF9900",
color: "#FFFFFF",
display: "flex",
justifyContent: "center",
alignItems: "center"
}
const DraggableComponent = () => {
const [pressed, setPressed] = useState(false)
const [position, setPosition] = useState({x: 0, y: 0})
const ref = useRef()
// Monitor changes to position state and update DOM
useEffect(() => {
if (ref.current) {
ref.current.style.transform = `translate(${position.x}px, ${position.y}px)`
}
}, [position])
// Update the current position if mouse is down
const onMouseMove = (event) => {
if (pressed) {
setPosition({
x: position.x + event.movementX,
y: position.y + event.movementY
})
}
}
return (
<div
ref={ ref }
style={ quickAndDirtyStyle }
onMouseMove={ onMouseMove }
onMouseDown={ () => setPressed(true) }
onMouseUp={ () => setPressed(false) }>
<p>{ pressed ? "Dragging..." : "Press to drag" }</p>
</div>
)
}
export default DraggableComponent
https://stackoverflow.com/questions/20926551
复制相似问题