React 事件初探

作者:朱灵子

React 是一个 Facebook 和 Instagram 用来创建用户界面的 JavaScript 库。

创造 React 是为了解决一个问题:构建随着时间数据不断变化的大规模应用程序。

本文初探react的顶层事件代理机制~

顶级事件代理机制

React采用的是顶层的事件代理机制,能够保持事件冒泡的一致性,可以跨浏览器执行,甚至可以在IE8中使用HTML5的事件。

React 实现了一个“合成事件”层,这个事件层消除了 IE 与 W3C 标准实现之间的兼容问题。首先区分原生事件与合成事件,我们在 componentDidMount 方法里面通过 addEventListener 绑定的事件就是浏览器原生事件,使用原生事件的时候注意在 componentWillUnmount 解除绑定 removeEventListener,所有通过 JSX 这种方式绑定的事件都是绑定到“合成事件”。

“合成事件”会以事件委托(event delegation)的方式绑定到组件最上层,并且在组件卸载(unmount)的时候自动销毁绑定的事件。

事件代理

在 DOM 节点上绑定事件比较消耗内存, React 则实现了一遍符合 W3C 规范的事件系统。接下来介绍该事件系统的实现原理, 事件 监听器被绑定到整个文档的根节点上。当事件被触发, 浏览器会给出一个触发目标事件的 DOM 节点。为了在 DOM 的层级传播事件, React 不会迭代 virtual DOM 的层级,而是依靠每个 React component 各自独立的 id 来编码这个层级。我们能通过简单的字符串操作来获取所有父级 component 的父级内容,再把事件监听存储在hashmap当中。下面的例子展示了事件广播到整个virtual DOM时的传播流程。

clickCaptureListeners['a'](event);
clickCaptureListeners['a.b'](event);
clickCaptureListeners['a.b.c'](event);
clickBubbleListeners['a.b.c'](event);
clickBubbleListeners['a.b'](event);
clickBubbleListeners['a'](event);

浏览器为每个事件和每个listener创建一个新的事件对象,我们可以从这个事件对象获取到事件的引用,但是这些事件对象也意味着高额的内存分配。为了减轻垃圾回收的负担,React 在启动时就为那些对象分配了一个内存池,当我们需要用到某一个事件对象时就可以从这个内存池进行复用。

React事件系统框图

 * +------------+    .
 * |    DOM     |    .
 * +------------+    .
 *       |           .
 *       v           .
 * +------------+    .
 * | ReactEvent |    .
 * |  Listener  |    .
 * +------------+    .                         +-----------+
 *       |           .               +--------+|SimpleEvent|
 *       |           .               |         |Plugin     |
 * +-----|------+    .               v         +-----------+
 * |     |      |    .    +--------------+                    +------------+
 * |     +-----------.--->|EventPluginHub|                    |    Event   |
 * |            |    .    |              |     +-----------+  | Propagators|
 * | ReactEvent |    .    |              |     |TapEvent   |  |------------|
 * |  Emitter   |    .    |              |<---+|Plugin     |  |other plugin|
 * |            |    .    |              |     +-----------+  |  utilities |
 * |     +-----------.--->|              |                    +------------+
 * |     |      |    .    +--------------+
 * +-----|------+    .                ^        +-----------+
 *       |           .                |        |Enter/Leave|
 *       +           .                +-------+|Plugin     |
 * +-------------+   .                         +-----------+
 * | application |   .
 * |-------------|   .
 * |             |   .
 * |             |   .
 * +-------------+   .                  
 

框图中的ReactBrowserEventEmitter主要用于连接顶层事件侦听器,例如:

  EventPluginHub.putListener(‘myID’, ‘onClick’, myFunction);

接下来是对react事件系统原理框图的理解:

  • Top-level delegation用于捕获最原始的浏览器事件,它主要由ReactEventListener负责,ReactEventListener被注入后可以支持插件化的事件源,这一过程发生在主线程。
  • 我们对各种事件进行去重复性处理以兼容不同的浏览器,这一过程是由工作线程来完成的。
  • 最后我们转发所有的本地事件到EventPluginHub(这些本地事件由相关顶级类型来捕获),EventPluginHub会注解每个事件,然后分派事件。

React组件状态更新

React中的props代表父级分发下来的属性,state代表组件内部可以自行管理的状态,并且整个React没有数据向上回溯的能力,也就是说数据只能单向向下分发,或者自行内部消化。子组件改变父组件state的办法只能是通过onClick等事件触发父组件声明好的回调,也就是父组件提前声明好函数或方法作为契约描述自己的state将如何变化,再将它同样作为属性交给子组件使用。

这样数据总是单向从顶层向下分发的,只有子组件回调在概念上可以回到state顶层影响数据,这样state一定程度上是响应式的。为了面临所有可能的扩展问题,最容易想到的办法就是把所有state集中放到所有组件顶层,然后分发给所有组件。

React跨浏览器执行的实现原理

React基于VirtualDom构建,可以更快、更有效地完成Dom操作。React实现了一套完整的事件合成机制,能够保持事件冒泡的一致性,同时可以实现跨浏览器执行,甚至可以在IE8中使用HTML5的事件。《Secrets of the JavaScript Ninja》中讲解了如何模拟 submit/focus/blur 等事件的冒泡,还讲述了mouseenter 与 mouseleave 等事件的模拟。除Firefox浏览器外都可使用支持冒泡的 focusin/focusout 来代替 focus/blur 事件,Firefox会在捕获阶段监听 focus/blur 事件。

submit/reset 事件会在鼠标点击或者按回车键时触发,所以可以监听冒泡的 click 和 keypress 事件,并判断触发事件的元素是否为一个 form 元素的后代节点,然后手动触发 submit/reset 事件。在Firefox v8.0浏览器下,如果作为top-level listener之一的onmousemove事件不是挂载在document元素上,那么当鼠标在不是该节点或者该节点所对应的子节点元素上移动时,onmousemove事件就不会被触发。根据不同的浏览器对onmouseover事件、onscroll事件以及focusin、focusout事件的支持情况的不同,react进行了有针对性的处理,以下为react事件系统跨浏览器执行的部分代码实现:

    listenTo: function (registrationName, contentDocumentHandle) {
    var mountAt = contentDocumentHandle;
    var isListening = getListeningForDocument(mountAt);
    var dependencies = EventPluginRegistry.registrationNameDependencies[registrationName];

    var topLevelTypes = EventConstants.topLevelTypes;
    for (var i = 0; i < dependencies.length; i++) {
      var dependency = dependencies[i];
      if (!(isListening.hasOwnProperty(dependency) && isListening[dependency])) {
        if (dependency === topLevelTypes.topWheel) {
          if (isEventSupported('wheel')) {
            ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(topLevelTypes.topWheel, 'wheel', mountAt);
          } else if (isEventSupported('mousewheel')) {
            ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(topLevelTypes.topWheel, 'mousewheel', mountAt);
          } else {
            // Firefox浏览器捕获鼠标滚动事件处理
            ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(topLevelTypes.topWheel, 'DOMMouseScroll', mountAt);
          }
        } else if (dependency === topLevelTypes.topScroll) {

          if (isEventSupported('scroll', true)) {
            ReactBrowserEventEmitter.ReactEventListener.trapCapturedEvent(topLevelTypes.topScroll, 'scroll', mountAt);
          } else {
            ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(topLevelTypes.topScroll, 'scroll', ReactBrowserEventEmitter.ReactEventListener.WINDOW_HANDLE);
          }
        } else if (dependency === topLevelTypes.topFocus || dependency === topLevelTypes.topBlur) {

          if (isEventSupported('focus', true)) {
            ReactBrowserEventEmitter.ReactEventListener.trapCapturedEvent(topLevelTypes.topFocus, 'focus', mountAt);
            ReactBrowserEventEmitter.ReactEventListener.trapCapturedEvent(topLevelTypes.topBlur, 'blur', mountAt);
          } else if (isEventSupported('focusin')) {
            // IE 浏览器支持的focusin和focusout事件
            ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(topLevelTypes.topFocus, 'focusin', mountAt);
            ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(topLevelTypes.topBlur, 'focusout', mountAt);
          }
          // 保证blur和focus事件只监听一次
          isListening[topLevelTypes.topBlur] = true;
          isListening[topLevelTypes.topFocus] = true;
        } else if (topEventMapping.hasOwnProperty(dependency)) {
          ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(dependency, topEventMapping[dependency], mountAt);
        }
        isListening[dependency] = true;
      }
    }
  }

原文链接:http://ivweb.io/topic/58227d0a0fea59e31b98bb5f

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏一“技”之长

Bootstrap响应式前端框架笔记十六——模态框交互

    模态框也可以称为弹出窗,其作用是当用户点击某个功能按钮后,在网页上弹出一个内容窗口。在Bootstrap中,创建模态框十分简单。首先模态框组件通过mod...

781
来自专栏深度学习自然语言处理

matplotlib--python的数据可视化入门

在平时做实验的时候,往往为了让数据更直接的表达一个趋势或为了描绘一种关系,经常会用到画图,再加上我们经常用的语言是pyhon居多,所以,作为python中的画图...

29110
来自专栏极客生活

tableau绘制好看的折线图

951
来自专栏Java后端技术

Web上的支持的图片格式以及它们之间的区别

751
来自专栏哲学驱动设计

任务记录:OEA 框架中的多类型树控件

    11年11月我主要对 OEA 框架中 WPF 自动界面生成模块中多类型树型表格控件进行重构,并同时支持更多的功能。这样,整个 OEA 就不再使用 Dat...

2017
来自专栏我的博客

关于制作手机网页笔记

1、px和rem换算 em 的计算是基于父级元素的,在实际使用中给我们的计算带来了很大的不便 rem 的出现就可以避免这类计算的问题,再也不用担心父级元素的...

3478
来自专栏SnailTyan

matplotlib的基本用法(四)——设置legend图例

本文主要是关于matplotlib的一些基本用法。 Demo import matplotlib.pyplot as plt import numpy as n...

2047
来自专栏james大数据架构

TableLayout(表格布局)

表格布局模型以行列的形式管理子控件,每一行为一个TableRow的对象,当然也可以是一个View的对象。TableRow可以添加子控件,每添加一个为一列。 Ta...

1908
来自专栏简书专栏

基于Excel2013的合并计算

工作组、求和、利用函数randbetween、合并计算 比如在excel表格中填入=randbetween(1,10),输入时如果需要补全要用鼠标点击,不能按...

621
来自专栏Python小屋

Python+pyplot绘制带文本标注的柱状图

import numpy as np import matplotlib.pyplot as plt # 生成测试数据 x = np.linspace(0, 1...

2995

扫码关注云+社区