react事件源码步步调

本文通过一个简短的实例&控制台调试,了解react事件处理的全过程。下面是测试用代码,使用控制台可以清晰看到函数执行过程中参数变化以及方法所属模块&调用栈,所以本文图片较多。


class RemoveBtn extends Component {
    clickHandler = () => {
        this.props.handleClick();
    }
    render(){
        return(
            <button onClick={this.clickHandler}>togglage测试组件</button>
        )
    }
}

class Root extends Component {
    
    clickHandler = () => {
        alert('hanlder is1 perform')
    }
    render(){
        return (
            <div className="first">
                <RemoveBtn handleClick = {this.clickHandler}/>
            </div>
        )
    } 
}

ReactDOM.render(<Root />, document.getElementById('root'));

1 事件绑定

1.1 绑定的结果

事件绑定结果

说明: 这里的backend.js是react调试工具的脚本不用考虑。

图中可见只有在document上绑定了名为dispatchEvent的来自于 ReactEventListener.js模块的事件处理函数。

1.2 事件绑定的过程

ReactDOM.render(<Root />, document.getElementById('root'));

一切开始于ReactDOM.render调用的ReactMount.jsrender方法。忽略掉实例化组建的过程,详细调用可以查看截图右侧的调用栈。

ReactDom.render 将react组件渲染到指定的容器上

_renderSubtreeIntoContainer -> mountComponentIntoNode -> mountComponent[reactReconciler.js] -> _updateDOMProperties

判断绑定事件还是删除事件-w1564

_updateDOMProperties函数在mountComponentunmountComponentupdateComponent阶段都有调用,它是检查属性变化,调优性能的重要方法。下图节选处理事件绑定部分代码,方法中有指向上次属性值得lastProp, nextProp是当前属性值,这里nextProp是我们绑定给组件的onclick事件处理函数。nextProp 不为空调用enqueuePutListener绑定事件为空则注销事件绑定。

queuePutListener

enqueuePutListener 这个方法只在浏览器环境下执行,传给listenTo参数分别是事件名称'onclick'和代理事件的绑定dom。如果是fragement 就是根节点(在reactDom.render指定的),不是的话就是documentlistenTo 用于绑定事件到 document ,下面交由事务处理的是回调函数的存储,便于调用。ReactBrowserEventEmitter 文件中的 listenTo 看做事件处理的源头。

listenTo
  listenTo: function (registrationName, contentDocumentHandle) {
    var mountAt = contentDocumentHandle;
    var isListening = getListeningForDocument(mountAt);
    // 获取 registrationName(注册事件名称)的topLevelEvent(顶级事件类型)
    var dependencies = EventPluginRegistry.registrationNameDependencies[registrationName];

    for (var i = 0; i < dependencies.length; i++) {
      var dependency = dependencies[i];
      if (!(isListening.hasOwnProperty(dependency) && isListening[dependency])) {
        if (dependency === 'topWheel') {
           ...         
        } else if (dependency === 'topScroll') {
               ...
        } else if (dependency === 'topFocus' || dependency === 'topBlur') {
                ...
        } else if (topEventMapping.hasOwnProperty(dependency)) {
        // 获取 topLevelEvent 对应的浏览器原生事件
          ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(dependency, topEventMapping[dependency], mountAt);
        }
        isListening[dependency] = true;
      }
    }
  },

对于同一个事件,例如click有两个事件 onClick(在冒泡阶段触发) onClickCapture(在捕获阶段触发)两个事件名,这个冒泡和捕获都是react事件模拟出来的。绑定到 document上面的事件基本上都是在冒泡阶段(对 whell, focus, scroll 有额外处理),如下图 click 事件绑定执行的如下。

listenTo

topEventMappingtopLevlelEvent 浏览器事件对照关系,mountAt 是绑定对象是函数接收第二个参数,也就是上文的doc(document)。

ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent 对所传的target做了非空判断后调用 EventListener.listen 传参数分别是:事件对象, 浏览器原生事件名称, 指定了顶级事件类型的事件处理函数(bind函数)ReactEventListener.dispatchEvent.bind(null, topLevelType)

EventListener.listen

EventListener.listen 将事件绑定到target上。
回到上文利用事务存储事件部分,这里调用的putListener方法

调用 EventPluginHub.putListener 第一个参数是组件事例,第二个是‘onClick’,第三个是我们写的事件处理函数

listenerBank存储的listener

putListener 将事件处理函数存储到listenerBank[registrationName][key]上其中registrationName是事件名称,.${_rootNodeID}``作为key值,处理函数作为value存储。下面调用的方法有对与safraiclick`事件的兼容处理。
至此事件绑定告一段落了。

2 事件处理

event pooling事件池
合成事件是 pooled(循环使用的),这意味着合成事件对象会被重复使用,所有的属性在被调用以后会被值为null,该机制用于性能优化,因此你不可以异步访问事件。除非调用 event.persist(),该方法不会不会把事件放入事件池中,保持event对象不被重置允许代码的引用到。

事件触发后执行dispatchEvent方法,该方法第一个参数是绑定时bind的 topLevelEvent这里是 topClick,此处调用TopLevelCallbackBookKeeping.getPooled函数先去事件池中取可以复用的,没有的话初始化新的。

这个bookKeeping初始化很简单,就是把顶级事件类型,原生事件对象,空的父组件列表放在一个对象上。

获取bookKeeping-w1062

reactUpdate.batchedUpdates是用事务封装了handleTopLevelImpl(bookKeeping)

getEventTarget 返回的是对应的Dom节点
ReactDOMComponentTree.getClosestInstanceFromNode 返回对应的 reactDomComponent

执行事件回调前,先由当前组件向上遍历它的所有父组件。保存到bookKeeping.ancestors这个数组中。因为事件回调中可能会改变DOM结构,所以要先遍历好组件层级,防止与已缓存ReactMount's node相矛盾。之后就是依次掉调用 ReactEventListener._handleTopLevel

最后一个参数通过getEventTarget函数兼容svg以及safraitextNode 这里最终返回的是触发事件的DOM节点。

handleTopLevel函数经由EventPluginHub处理 top level Event,在EventPluginHub处理过程中不同的plugin可以创建派发相应的事件。第一行是构造出合成事件,第二行就是交由事务处理事件。

2.1 构建react事件

extractEvent 让已注册的plugin处理相应的的topLevelType。下图看到在运行过程中已注册的plugin只有五个分别是

 ReactInjection.EventPluginHub.injectEventPluginsByName({
    SimpleEventPlugin: SimpleEventPlugin,
    EnterLeaveEventPlugin: EnterLeaveEventPlugin,
    ChangeEventPlugin: ChangeEventPlugin,
    SelectEventPlugin: SelectEventPlugin,
    BeforeInputEventPlugin: BeforeInputEventPlugin
  });

extractEvent会依次调用每个pluginextractEvents方法,第一个处理的是SimpleEventPlugin,该plugin处理了绝大部分的事件,本例 onClick 就是其中之一。

SimpleEventPlugin.extractEvent

经由一个switch(topLevelType)确定该react事件的构造函数为SyntheticMouseEvent

SimpleEventPlugin.extractEvent 根据 topLevelEvent 处理事件

上文看到 topClick 使用 syntheticMouseEvent 作为事件构造函数。

这里调用的EventConstructor.getPooled就是开篇提到的事件池,先看有没有可以复用的事件对象没有的话在重新实例一个。

-w944

这里SyntheticMouseEvent调用 SyntheticUIEvent, SyntheticUIEvent调用 SyntheticEventSyntheticEvent构造函数这部分代码相对较长,函数注释中说道,该方法应该尽量减少调用的频率,使用pooling(回收再利用|池)机制。在构建时候会通过判断isPersistent属性来判断调用后是否放入池中。使用者可以通过调用 persist方法来改变这个值。
而后执行的是 EventPropagators.accumulateTwoPhaseDispatches(event)
这个方法经历层层跳转,详情可见调用栈,最后到traverseTwoPhase这个函数。inst 为 触发事件的reactDomComponent,fnaccumulateDirectionDispatches, arg 为合成事件。

function traverseTwoPhase(inst, fn, arg) {
  var path = [];
  while (inst) {
    path.push(inst);
    inst = inst._hostParent;
  }
  var i;
  for (i = path.length; i-- > 0;) {
    fn(path[i], 'captured', arg);
  }
  for (i = 0; i < path.length; i++) {
    fn(path[i], 'bubbled', arg);
  }
}

path 为收集的以target起始到根节点为止的组件,本例中两个。用于后续模拟事件的捕获和冒泡。

traverseTwoPhase

之后按照从外到内捕获从里到外冒泡的顺序调用 accumulateDirectionDispatches(path[i], 'captured', arg)该方法将合成事件与处理函数联系起来。

这里 listenerAtPhase -> getListener[EventPluginHub.js] 获取事件处理函数。
在事件绑定中最后把所有的事件处理放在一个对象上listenerBank

-w594

通过注册类型获取到对应类型的所有处理函数,使用.${reactDomComponent._rootNodeID} 找到对应虚拟Dom上的事件处理函数。

/**
  * @param {object} inst reactDOMComponent 实例 (虚拟DOM)
  * @param {string} registrationName 注册事件名
  * * /
 getListener: function (inst, registrationName) {
    // TODO: shouldPreventMouseEvent is DOM-specific and definitely should not
    // live here; needs to be moved to a better place soon
    // 获取同类型的所有处理函数
    var bankForRegistrationName = listenerBank[registrationName];
    if (shouldPreventMouseEvent(registrationName, inst._currentElement.type, inst._currentElement.props)) {
      return null;
    }
    // 获取 .${reactDomComponent._rootNodeID}`
    var key = getDictionaryKey(inst);
    // 返回指定虚拟DOM上的事件处理函数
    return bankForRegistrationName && bankForRegistrationName[key];
  }


获取事件处理函数后,将它和响应的reactDOMComponent分别添加到队列中。accumulateInto用于将内容添加到现有队列中,传入原来队列和要添加到队列中的内容。

accumulateInto

至此事件已经封装准备好了。

2.2 事件分发

事件分发

承接上文封装好的event对象。使用runEventQueueInBatch开始事件分发。

这里第一行用于将事件放入队列processEventQueue中,其内部调用的还是accumulateInto方法。
第二行,processEventQueue派发所有在事件队列processEventQueue中的合成事件。

首先将队列中的内容取出,清空队列,以防处理中队列变化。

simulated:为true表示React测试代码,我们一般都是false
此注解出自参考文章一

这里forEachAccumulate就是对第一个参数执行foreach调用第二个参数。

executeDispatchesAndReleaseTopLevel -> executeDispatchesAndRelease 该函数 -> EventPluginUtils.executeDispatchesInOrder,并将没有调用persist的事件对象回收到事件池。

EventPluginUtils.executeDispatchesInOrder

处理函数是多个,则依次执行。本例中只有一个处理函数 -> executeDispatch。执行后设置 event._dispatchListenerevent._dispatchInstances 为 null。

executeDispatch

通过EventPluginUtils.getNodeFromInstance获取响应的对应的真实DOM节点作为事件的currentTarget
本例执行85行 这里的type 为click,func为事件处理函数, event为合成事件对象。
在生产环境中,会直接调用事件处理函数,开发环境中会模拟浏览器事件。

模拟过程如下。

这里在创建的fakeElement上绑定事件,之后模拟事件触发(执行本例中的事件处理函数),再注销事件绑定。

到此为止这个事件已经处理完,接下来就是把这个事件属性置为null,然后把它放入事件池中了。判断是否强制了调用了persistent,没有的话就释放事件对象。

其实这里可以看到事件池有一个上线就是10,当可用的对象大于10也不会再往里面添加了。
最后看一下事件的 destructor 方法

这里获取所有的属性设置为null,并且再访问该事件对象时会预警提醒。

至此事件处理完成。

3 事件机制总结

这里是源码注释的翻译

  • 顶级代理是用于捕获多数原生浏览器事件,这些只会在主线程发生,并由reactEventLister 负责处理,reactEventLister 是被注入的因此可以支持插件事件资源,这是唯一在主线程执行的。
  • 封装了顶层事件(TopLevelEvent)来应对浏览器异常。这个在工作线程完成。
  • 传递原生事件以及封装的顶层事件名称到 EventPluginHub,他会遍历插件是否要执行某些合成事件。
  • EventPluginHub 获取响应的事件监听器,以及Dom绑定到生成的事件对象上。
  • EventPluginHub 将会派发事件

3.1 各种事件名

主要三个事件:regiestrationName(注册事件名),topLevelType(顶层事件),(原生事件)

事件绑定阶段,从组件属性中获取’注册事件名‘,会区分捕获和默认冒泡事件名,这里的注册名为react对外暴露的事件,包含自定义事件。
顶层事件是react封装EventPlugin处理的单位,react对外暴露的事件是由一个多个事件模拟而成的。
原生事件是最终绑定到目标元素上的事件,和顶层事件对应关系为一对一的关系。在绑定给document的是使用bind函数,固定第一个参数——topLevelEvent的函数。因此当事件触发后使用的。

// 本例中
// regiestrationName(注册名)
onClick
onClickCapture

// topLevelType (顶层事件类型)
topClick

// native Event (原生事件) | dependence
click

// regiestrationName => topLevelType
EventPluginRegisterName.registionNameDependencies

// topLevelType => native event
topEventMapping[位于reactBrowserEventEmitter.js]

3.2 事件全局代理(target)

根据不同的topLevelType对应的浏览器事件,绑定到target上(如果是fragement 就是根节点(在reactDom.render指定的),不是的话就是document)ReactEventListener.dispatchEvent.bind(null, topLevelType)。

3.3 事件存储

当组件渲染和更新的时候会调用_updateDomPorperties方法检查属性变化,这里执行reactBrowerEventEmitter模块下的listenTo对不同事件进行了兼容处理后最终调用 EventPluginHub.js模块下的 putListener方法,将事件处理函数,以 .${reactDomComponent._rootNodeID}为key值放在listenerBank[registrationName]对象上。

3.4 阻止事件冒泡

通过事件绑定的分析会发现,无论注册的是onClick 还是 onClickCapture 最后都是调用 ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent 在冒泡阶段触发的事件, 也会发现在没有执行事件处理函数的时候,事件就已经eventQuene中,那是不是就意味着调用e.stopPropagation()就不能阻止事件冒泡了呢。

事件处理函数中获取的事件是合成事件对象,合成事件对象也是有stopPropagation方法的。

合成事件对象的stopPropagation方法

注意这里的最后一行,这里执行的函数为事件isPropagationStopped方法赋值了一个只会返回true的函数。而在一次调用事件处理函数的过程中,每一次都会调用事件对象的该方法。

因此使用e.stopPropagation()不能组织原生事件冒泡,但是模拟到阻止事件冒泡的效果的。

react 文档说明
更多可参考[4]

3.5 事件相关文件

synthetcEvent 封装合成事件基类

原型方法:

  • preventDefault()
  • stopPropergation()
  • persist() 调用后isPersist = true, 此事件对象将不会被销毁复用(进入事件池)
  • isPersist
  • desturctor() 事件触发后(isPersist!==true), 清空事件对象属性。

**静态方法: **

  • arugumentClass
// @prarm interface 需要定义的事件对象属性
// @param Class 子类
SyntheticEvent.augmentClass = function(Class, Interface) {
  var Super = this;
  var E = function() {};
  E.prototype = Super.prototype;
  var prototype = new E();

  Object.assign(prototype, Class.prototype);
  // 子类继承基类原型上的方法
  Class.prototype = prototype;
  Class.prototype.constructor = Class;
    // 合并interface
  Class.Interface = Object.assign({}, Super.Interface, Interface);
  Class.augmentClass = Super.augmentClass;
  // 为子类添加事件池相关属性和方法
  addEventPoolingTo(Class);
};
  • eventPool[]
  • getPooled()
    参数同构造函数传参,判断事件池中是否有可用事件,有的复用,没有新建。
  • release(event)
    判断事件对象是否 isPersist过 没有的话调用对象的 destructor, 之后将其添加入事件池。

参考
React源码分析7 — React合成事件系统
看源码react事件机制
React源码解读系列 – 事件机制
react 合成事件和原生事件的阻止冒泡

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,051评论 25 707
  • 版权声明:本文为博主原创文章,未经博主允许不得转载。 PS:转载请注明出处作者:TigerChain地址:http...
    TigerChain阅读 8,358评论 1 9
  • 原教程内容详见精益 React 学习指南,这只是我在学习过程中的一些阅读笔记,个人觉得该教程讲解深入浅出,比目前大...
    leonaxiong阅读 2,805评论 1 18
  • 成果: Django的简介 Django的基本教程这个是菜鸟教程中的,包含了安装和一些基本的使用,讲的还可以 介绍...
    泠泠七弦客阅读 393评论 0 0
  • 在师父的教导下,我对代码的书写规范也是越来越有强迫症了。我的代码若能有幸被你看见,则不难发现有一些规律。 *.ht...
    依暄阅读 1,915评论 8 8