说说react的事件机制
react的事件是合成事件((Synethic event),不是原生事件
合成事件与原生事件的区别
1.写法不同,合适事件是驼峰写法,而原生事件是全部小写
2.执行时机不同,合适事件全部委托到document上,而原生事件绑定到DOM元素本身
3.合成事件中可以是任何类型,比如this.handleClick这个函数,而原生事件中只能是字符串react事件执行大致流程如下图片:
我们来看个例子深入了解下,如果我们需要实现一个组件,这个组件点击按钮会显示一个二维码,点击二维码之外的区域可以隐藏二维码,但是点击二维码本身却不会关闭,代码如下:
//代码来源于《深入React技术栈》2.1.4节 class QrCode extends Component { constructor(props) { super(props); this.handleClick = this.handleClick.bind(this); this.handleClickQr = this.handleClickQr.bind(this); this.state = { active: false, }; } componentDidMount() { document.body.addEventListener('click', e => { this.setState({ active: false, }); }); } componentWillUnmount() { document.body.removeEventListener('click'); } handleClick() { this.setState({ active: !this.state.active, }); } handleClickQr(e) { e.stopPropagation(); } render() { return ( <div className="qr-wrapper"> <button className="qr" onClick={this.handleClick}>二维码</button> <div className="code" style={{ display: this.state.active ? 'block' : 'none' }} onClick={this.handleClickQr} > <img src="qr.jpg" alt="qr" /> </div> </div> ); } }
上面代码从感官上感觉确实可以实现要求的组件,但事实上我们运行上述代码可以发现,点击二维码本身也会导致二维码的隐藏,现在就有意思了,我们来仔细分析一下。
其实React事件并没有原生的绑定在真实的DOM上,而是使用了行为委托方式实现事件机制。
如上图所示,在JavaScript中,事件的触发实质上是要经过三个阶段:事件捕获、目标对象本身的事件处理和事件冒泡,假设在div中触发了click事件,实际上首先经历捕获阶段会由父级元素将事件一直传递到事件发生的元素,执行完目标事件本身的处理事件后,然后经历冒泡阶段,将事件从子元素向父元素冒泡。正因为事件在DOM的传递经历这样一个过程,从而为行为委托提供了可能。通俗地讲,行为委托的实质就是将子元素事件的处理委托给父级元素处理。React会将所有的事件都绑定在最外层(document),使用统一的事件监听,并在冒泡阶段处理事件,当挂载或者卸载组件时,只需要在通过的在统一的事件监听位置增加或者删除对象,因此可以提高效率。
并且React并没有使用原生的浏览器事件,而是在基于Virtual DOM的基础上实现了合成事件(SyntheticEvent),事件处理程序接收到的是SyntheticEvent的实例。SyntheticEvent完全符合W3C的标准,因此在事件层次上具有浏览器兼容性,与原生的浏览器事件一样拥有同样的接口,可以通过stopPropagation()和preventDefault()相应的中断。如果需要访问当原生的事件对象,可以通过引用nativeEvent获得。
上图为大致的React事件机制的流程图,React中的事件机制分为两个阶段:事件注册和事件触发:
事件注册
React在组件加载(mount)和更新(update)时,其中的ReactDOMComponent会对传入的事件属性进行处理,对相关事件进行注册和存储。document中注册的事件不处理具体的事件,仅对事件进行分发。ReactBrowserEventEmitter作为事件注册入口,担负着事件注册和事件触发。注册事件的回调函数由EventPluginHub来统一管理,根据事件的类型(type)和组件标识(_rootNodeID)为key唯一标识事件并进行存储。
事件执行
事件执行时,document上绑定事件ReactEventListener.dispatchEvent会对事件进行分发,根据之前存储的类型(type)和组件标识(_rootNodeID)找到触发事件的组件。ReactEventEmitter利用EventPluginHub中注入(inject)的plugins(例如:SimpleEventPlugin、EnterLeaveEventPlugin)会将原生的DOM事件转化成合成的事件,然后批量执行存储的回调函,回调函数的执行分为两步,第一步是将所有的合成事件放到事件队列里面,第二步是逐个执行。需要注意的是,浏览器原生会为每个事件的每个listener创建一个事件对象,可以从这个事件对象获取到事件的引用。这会造成高额的内存分配,React在启动时就会为每种对象分配内存池,用到某一个事件对象时就可以从这个内存池进行复用,节省内存。
再回到我们刚开始的问题,现在看起来就很没有很费解了,之所以会出现上面的问题是因为我们混用了React的事件机制和DOM原生的事件机制,认为通过:
handleClickQr(e) { e.stopPropagation(); }
就能阻止原生的事件传播,其实在事件委托的情形下是不能实现这一点的。当然解决的办法也不复杂,不要将React事件和DOM原生事件混用。
componentDidMount() { document.body.addEventListener('click', e => { this.setState({ active: false, }); }); document.querySelector('.code').addEventListener('click', e => { e.stopPropagation(); }) } componentWillUnmount() { document.body.removeEventListener('click'); document.querySelector('.qr').removeEventListener('click'); }
或者通过事件原件对象中的target进行判断:
componentDidMount() { document.body.addEventListener('click', e => { if (e.target && e.target.matches('div.code')) { return; } this.setState({ active: false, }); }); } 都可以解决异常关闭的问题。
阻止事件冒泡
事实上阻止合成事件的方法有很多,但是有个原则:阻止合成事件的冒泡不会阻止原生事件的冒泡,但是阻止原生事件的冒泡会阻止合成事件的冒泡。
-----------------------以上是不是没明白,我们继续看下面解释---------------------------
React不会将事件处理函数直接绑定到真实的节点上,而是把所有的事件绑定到结构的最外层,使用一个统一的事件监听器。
这个监听器维持了一个映射,保存所有组件内部的事件监听和处理函数。当事件发生时,首先被这个统一的事件监听器处理,
然后在映射里找到真正的事件处理函数并调用。
合成事件分为以下三个主要过程:
一 事件注册
所有事件都会注册到document上,拥有统一的回调函数dispatchEvent来执行事件分发
二 事件合成
从原生的nativeEvent对象生成合成事件对象,同一种事件类型只能生成一个合成事件Event,如onclick这个类型的事件,dom上所有带有通过jsx绑定的onClick的回调函数都会按顺序(冒泡或者捕获)会放到Event._dispatchListeners 这个数组里,后面依次执行它
三 事件派发
每次触发事件都会执行根节点上 addEventListener 注册的回调,也就是 ReactEventListener.dispatchEvent 方法,事件分发入口函数。该函数的主要业务逻辑如下:
1.找到事件触发的 DOM 和 React Component
2.从该 React Component,调用 findParent 方法,遍历得到所有父组件,存在数组中。
3.从该组件直到最后一个父组件,根据之前事件存储,用 React 事件名 + 组件 key,找到对应绑定回调方法,执行,详细过程为:
4.根据 DOM 事件构造 React 合成事件。
5.将合成事件放入队列。
6.批处理队列中的事件(包含之前未处理完的,先入先处理)
React合成事件的冒泡并不是真的冒泡,而是节点的遍历。
----------------------------来对比下与原生事件区别----------------------------------
React事件处理的特性
React的事件系统和浏览器事件系统相比,主要增加了两个特性:事件代理和事件自动绑定
1、事件代理
1.区别于浏览器事件处理方式,React并未将事件处理函数与对应的DOM节点直接关联,而是在顶层使用了一个全局事件监听器监听所有的事件;
2.React会在内部维护一个映射表记录事件与组件事件处理函数的对应关系;
3.当某个事件触发时,React根据这个内部映射表将事件分派给指定的事件处理函数;
4.当映射表中没有事件处理函数时,React不做任何操作;
5.当一个组件安装或者卸载时,相应的事件处理函数会自动被添加到事件监听器的内部映射表中或从表中删除。
2、事件自动绑定
1.在JavaScript中创建回调函数时,一般要将方法绑定到特定的实例,以保证this的正确性;
2.在React中,每个事件处理回调函数都会自动绑定到组件实例(使用ES6语法创建的例外);
注意:事件的回调函数被绑定在React组件上,而不是原始的元素上,即事件回调函数中的
this所指的是组件实例而不是DOM元素;
3、合成事件
1.与浏览器事件处理稍微有不同的是,React中的事件处理程序所接收的事件参数是被称为“合成事件(SyntheticEvent)”的实例。
合成事件是对浏览器原生事件跨浏览器的封装,并与浏览器原生事件有着同样的接口,如stopPropagation(),preventDefault()等,并且
这些接口是跨浏览器兼容的。
2.如果需要使用浏览器原生事件,可以通过合成事件的nativeEvent属性获取
-------------------说了这么多,来总结下react合成事件优点------------------------
React在事件处理的优点
1.几乎所有的事件代理(delegate)到 document ,达到性能优化的目的
2.对于每种类型的事件,拥有统一的分发函数 dispatchEvent
3.事件对象(event)是合成对象(SyntheticEvent),不是原生的,其具有跨浏览器兼容的特性
4.react内部事件系统实现可以分为两个阶段: 事件注册、事件分发,几乎所有的事件均委托到document上,而document上事件的回调函数只有一个:ReactEventListener.dispatchEvent,然后进行相关的分发
5.对于冒泡事件,是在 document 对象的冒泡阶段触发。对于非冒泡事件,例如focus,blur,是在 document 对象的捕获阶段触发,最后在 dispatchEvent 中决定真正回调函数的执行