今天遇到这么一个问题:
我们在实现列表拖拽排序时使用了react-sortable-hoc这个库,确实很好用,利用ES装饰器新特性,几个@符号就解决了。但是因为我们在每一个列表item上还有菜单按钮,点击菜单按钮会弹出菜单,这样一来就会发现菜单的click事件和react-sortable-hoc
的mousedown
事件冲突了,特别是在你把pressDelay设置低于300ms时。
如果你使用过这个组件,你会发现pressDelay超过100ms,拖住体验就不再有丝滑般的感觉了。怎么办?我们需要完美啊,既要丝滑般的感觉又不要事件冲突。
假设在jQuery一把鎍的时代,这个都不是问题,因为菜单按键和列表item明显是父子元素的关系,我只需要把菜单上的mouse事件不向父元素listitem冒泡就解决了。
事实上时代并没有变化,react时代也是这么解决的。只是解决的时候会发现一个细节需要注意,那就是--react事件机制和原生dom事件机制之男女有别。
我们先来看react-sortable-hoc
组件源码里是怎么绑定事件的
_utils.events.move.forEach(function (eventName) {
// 原生事件addEventListener
return _this.listenerNode.addEventListener(eventName,
_this.handleSortMove, false);
});
_utils.events.end.forEach(function (eventName) {
return _this.listenerNode.addEventListener(eventName, _this.handleSortEnd, false);
});
使用的是dom的addEventListener函数绑定事件,这走的是原生事件机制。
然后看我们在react项目中怎么去stop冒泡的
// 简单期间,只贴render函数足以
render() {
const { showMenu } = this.state;
const { children, className } = this.props;
const cls = classNames(styles['dropdown'], className);
return (
<div
className={cls}
ref={(el) => {
this.dropDown = el;
}}
{ // 这里直接绑定onMouseDown,走的是react的SyntheticEvent机制 }
onMouseDown={ (e) => e.stopPropagation()}
>
{ this.renderDropIcon() }
<Animate transitionName='fade' component='span'>
{ showMenu && this.renderDropDownMenu()}
</Animate>
</div>
);
}
在jsx模板里直接绑定onMouseDown/onMouseMove/onMouseUp事件,并调用e.stopPropagation()
企图阻止冒泡。
然后你就发现完全没卵用啊,没道理啊,怎么会没有用呢?抓耳中...
这时候如果你在两个地方均加上log,你会发现竟然是react-sortable-hoc
包裹的列表item的mousedown
事件先触发,作为子元素的菜单后触发,更郁闷了有么有?
其实你通过本文的题目应该就猜到原因了。根本原因就是react的事件机制自成一套,具体可以看这篇文章React事件机制
](https://segmentfault.com/a/1190000008782645)
主要一个观点就是:
其实React事件并没有原生的绑定在真实的DOM上,而是使用了行为委托方式实现事件机制。
React会将所有的事件都绑定在最外层(document),使用统一的事件监听,并在冒泡阶段处理事件,当挂载或者卸载组件时,只需要在通过的在统一的事件监听位置增加或者删除对象,因此可以提高效率。
明白了吧,我们直接在jsx模板上绑定的事件,都是委托在了document上,那自然要比直接在dom上绑定的事件慢了,等document收到事件后才去e.stopPropagation()
,太晚了
所以解决方案就是通过ref获取到真实的dom,在componentDidMount时通过addEventListener添加事件监听,此时阻止冒泡就好了。注意一点,这种方式需要自己释放事件,即在componentWillUnmount时removeEventListener。
每一次遇到问题都会让自己对一些理论有更深的理解和记忆,还是那句话:问题是最好的教材啊