React-平凡之路

react事件系统原理

2021-05-19  本文已影响0人  skoll
46124c3589a1468aac72590d16f4787a_tplv-k3u1fbpfcp-watermark.jpg

1 .我们编写的click事件,最终会被转换城fiber对象
2 .fiber对象上的memoizedProps和pendingProps保存了我们的事件
3 .当前元素绑定的事件是noop()函数和document上的事件监听器,click事件其实是绑定在document上面.

4 .真实DOM上的click事件,会被单独处理,被react替换成空函数
5 .onChange事件,在document上面,有好几个事件跟他对应:blur,change,input,keydown等
6 .react并不是一开始就把所有事件绑定在document上面,而是采用了一种按需绑定,比如发现了click事件,才会绑定document click事件
7 .我们使用的click事件,在react并不是原生事件,而是原生事件合成的React事件.
8 .click事件合称为onClick事件,blur,change,keyDown,keyUp等,合成了onChange事件

事件合成

1 .统一绑定在document上面,防止很多直接绑定在原生dom上面,造成一些不可靠的情况
2 .统一事件,抹平浏览器的不一致性的事件系统
3 .react是如何合成事件

const namesToPlugins = {
    SimpleEventPlugin,
//SimpleEventPlugin等是处理各个事件函数的插件,比如一次点击事件,就会找到SimpleEventPlugin对应的处理函数
    EnterLeaveEventPlugin,

    ChangeEventPlugin,

    SelectEventPlugin,

    BeforeInputEventPlugin,
}

//namesToPlugins:事件名-事件模块插件的映射

plugins:对上面注册的所有插件列表,初始化为空
const  plugins = [LegacySimpleEventPlugin, LegacyEnterLeaveEventPlugin, ...];

//registrationNameModules

{
    onBlur: SimpleEventPlugin,
    onClick: SimpleEventPlugin,
    onClickCapture: SimpleEventPlugin,
    onChange: ChangeEventPlugin,
    onChangeCapture: ChangeEventPlugin,
    onMouseEnter: EnterLeaveEventPlugin,
    onMouseLeave: EnterLeaveEventPlugin,
    ...
}
//registrationNameModules
合成事件-对应的事件插件的关系,在处理props中事件的时候,根据不同的事件名称,找到对应的事件插件,统一绑定在document上,没有出现过就不会绑定

事件插件
const SimpleEventPlugin = {
    eventTypes:{ 
        'click':{ /* 处理点击事件  */
            phasedRegistrationNames:{
                bubbled: 'onClick',       // 对应的事件冒泡 - onClick 
                captured:'onClickCapture' //对应事件捕获阶段 - onClickCapture
            },
            dependencies: ['click'], //事件依赖
            //eventTypes是一个对象,对象保存了原生事件名和对应的配置项dispatchConfig的映射关系。由于v16React的事件是统一绑定在document上的,React用独特的事件名称比如onClick和onClickCapture,来说明我们给绑定的函数到底是在冒泡事件阶段,还是捕获事件阶段执行。
        },
        'blur':{ /* 处理失去焦点事件 */ },
        ...
    }
    extractEvents:function(topLevelType,targetInst,){ /* eventTypes 里面的事件对应的统一事件处理函数,接下来会重点讲到 */ }
}


registrationNameDependencies::记录合成事件和原生事件之间的关系
{
    onBlur: ['blur'],
    onClick: ['click'],
    onClickCapture: ['click'],
    onChange: ['blur', 'change', 'click', 'focus', 'input', 'keydown', 'keyup', 'selectionchange'],
    onMouseEnter: ['mouseout', 'mouseover'],
    onMouseLeave: ['mouseout', 'mouseover'],
    ...
}

1 .注册事件

injectEventPluginsByName({
    SimpleEventPlugin: SimpleEventPlugin,
    EnterLeaveEventPlugin: EnterLeaveEventPlugin,
    ChangeEventPlugin: ChangeEventPlugin,
    SelectEventPlugin: SelectEventPlugin,
    BeforeInputEventPlugin: BeforeInputEventPlugin,
})
injectEventPluginsByName做的事情很简单,形成上述的namesToPlugins,然后执行recomputePluginOrdering,
ecomputePluginOrdering,作用很明确了,形成上面说的那个plugins,数组,然后publishEventForPlugin,publishEventForPlugin 作用形成上述的 registrationNameModules 和 registrationNameDependencies 对象中的映射关系

react如何绑定事件

1 .事件绑定流程

<div>
  <button onClick={ this.handerClick }  className="button" >点击</button>
</div>

2 .编译的时候,绑定给hostCompoonent种类的fiber,本例中的button元素,在button对应的fiber上面,memoizedProps,pendingProps形成保存

button 对应 fiber
memoizedProps = {
   onClick:function handerClick(){},
   className:'button'
}
dec68c8a3d6d47d18aaecd565861cb97_tplv-k3u1fbpfcp-watermark.jpg

3 .React在调合子节点的时候,会进入diff阶段,如果判断是HostComponent类型的fiber,会用diff props函数diffPropsties单独处理

4 .diffProperties函数在 diff props 如果发现是合成事件(onClick) 就会调用legacyListenToEvent函数。注册事件监听器.肯定是合成事件吧

//  registrationName -> onClick 事件
//  mountAt -> document or container
function legacyListenToEvent(registrationName,mountAt){
   const dependencies = registrationNameDependencies[registrationName]; // 根据 onClick 获取  onClick 依赖的事件数组 [ 'click' ]。
    for (let i = 0; i < dependencies.length; i++) {
    const dependency = dependencies[i];
    //这个经过多个函数简化,如果是 click 基础事件,会走 legacyTrapBubbledEvent ,而且都是按照冒泡处理
     legacyTrapBubbledEvent(dependency, mountAt);
  }
}

5 .legacyTrapBubbledEvent 就是执行将绑定真正的dom事件的函数 legacyTrapBubbledEvent(冒泡处理)。

1 .先找到React合成事件对应的原生事件集合onClick-click,onCheng-[blur,change,input,keydown,keyup],然后遍历依赖项的数组,绑定事件

2 .大部分事件都是用的冒泡,特殊的事件用的是捕获比如scroll事件
case TOP_SCROLL: {                                // scroll 事件
    legacyTrapCapturedEvent(TOP_SCROLL, mountAt); // legacyTrapCapturedEvent 事件捕获处理。
    break;
}
case TOP_FOCUS: // focus 事件
case TOP_BLUR:  // blur 事件
legacyTrapCapturedEvent(TOP_FOCUS, mountAt);
legacyTrapCapturedEvent(TOP_BLUR, mountAt);
break;

6 .绑定dispatchEvent ,进行事件监听

/*
  targetContainer -> document
  topLevelType ->  click
  capture = false
*/
function addTrappedEventListener(targetContainer,topLevelType,eventSystemFlags,capture){
   const listener = dispatchEvent.bind(null,topLevelType,eventSystemFlags,targetContainer) 
   if(capture){
       // 事件捕获阶段处理函数。
   }else{
       /* TODO: 重要, 这里进行真正的事件绑定。*/
      targetContainer.addEventListener(topLevelType,listener,false) // document.addEventListener('click',listener,false)
   }
}
这个函数内容虽然不多,但是却非常重要,首先绑定我们的事件统一处理函数 dispatchEvent,绑定几个默认参数,事件类型 topLevelType demo中的click ,还有绑定的容器doucment。然后真正的事件绑定,添加事件监听器addEventListener。 事件绑定阶段完毕

7 .总结

① 在React,diff DOM元素类型的fiber的props的时候, 如果发现是React合成事件,比如onClick,会按照事件系统逻辑单独处理。
② 根据React合成事件类型,找到对应的原生事件的类型,然后调用判断原生事件类型,大部分事件都按照冒泡逻辑处理,少数事件会按照捕获逻辑处理(比如scroll事件)。
③ 调用 addTrappedEventListener 进行真正的事件绑定,绑定在document上,dispatchEvent 为统一的事件处理函数。
④ 有一点值得注意: 只有上述那几个特殊事件比如 scorll,focus,blur等是在事件捕获阶段发生的,其他的都是在事件冒泡阶段发生的,无论是onClick还是onClickCapture都是发生在冒泡阶段

8 .EventPlugin, 事件插件可以认为是 React 将不同的合成事件处理函数封装成了一个模块,每个模块只处理自己对应的合成事件,这样不同类型的事件种类就可以在代码上解耦,例如针对onChange事件有一个单独的LegacyChangeEventPlugin插件来处理,针对onMouseEnter, onMouseLeave 使用 LegacyEnterLeaveEventPlugin 插件来处理

9 .react执行diff操作,标记出哪些DOM类型的节点需要添加或者更新
10 .当检测到需要创建一个节点或者更新一个节点的时候,使用registernationModule查看一个prop是不是一个事件类型,如果是就执行下一步 .onClick,onMouseDown
11 .通过registrationNameDependencied检查这个React事件依赖了哪些原生事件类型.找到原生的事件属性注册到顶层,onClick-click
12 .检查一个或者多个原生事件类型有没有被注册过,如果注册过,就忽略.也就是说,永远都只有一个click事件注册到document上面
13 .如果这个原生事件类型没有被注册过,就注册这个原生事件到document上,回调为React提供dispatchEvent函数
14 .所有原生事件的listener都是dispatchEvent函数
15 .同一个类型的事件react只会绑定一次,无论写了多少个onClick,最终反映到DOM事件上只会有一个listener
16 .业务逻辑的listener和实际的DOM事件压根就没有关系,react只是会确保原生事件能够被触发,监听到.后续会由React来派发我们的事件回调.

react如何触发事件

1 .事件触发处理函数 dispatchEvent,所有类型种类的事件都是绑定为React的dispatchEvent函数

export function dispatchEventForLegacyPluginEventSystem(
  topLevelType: DOMTopLevelEventType,
  eventSystemFlags: EventSystemFlags,
  nativeEvent: AnyNativeEvent,
  targetInst: null | Fiber,
): void {
  const bookKeeping = getTopLevelCallbackBookKeeping(
    topLevelType,
    nativeEvent,
    targetInst,
    eventSystemFlags
  );

  try {
    // Event queue being processed in the same cycle allows
    // `preventDefault`.
    batchedEventUpdates(handleTopLevel, bookKeeping);
  } finally {
    releaseTopLevelCallbackBookKeeping(bookKeeping);
  }
}
//bookkeeping 为事件执行时组件的层级关系存储,如果在事件执行过程中发生组件结构变更,并不会影响事件的触发流程

2 .React注册事件的时候,统一的监听器是dispatchEvent,当我们点击按钮之后,首先执行的是dispatchEvent,而真正的源对象是event,被默认绑定为第四个参数

function dispatchEvent(topLevelType,eventSystemFlags,targetContainer,nativeEvent){
    /* 尝试调度事件 */
    const blockedOn = attemptToDispatchEvent( topLevelType,eventSystemFlags, targetContainer, nativeEvent);
}

3 .handleTopLevel:依次执行plugins里所有的事件插件,如果一个插件检测到自己需要处理的事件类型时,处理改事件

/*
topLevelType -> click
eventSystemFlags -> 1
targetContainer -> document
nativeEvent -> 原生事件的 event 对象
*/
function attemptToDispatchEvent(topLevelType,eventSystemFlags,targetContainer,nativeEvent){
    /* 获取原生事件 e.target */
    const nativeEventTarget = getEventTarget(nativeEvent)
    /* 获取当前事件,最近的dom类型fiber ,我们 demo中 button 按钮对应的 fiber */
    let targetInst = getClosestInstanceFromNode(nativeEventTarget); 
    /* 重要:进入legacy模式的事件处理系统 */
    dispatchEventForLegacyPluginEventSystem(topLevelType,eventSystemFlags,nativeEvent,targetInst,);
    return null;
}

3.1 .根据真实的事件源对象,找到e.target真实的dom元素
3.2 .根据dom元素,找到与他对应的fiber对象targetInst,比如找到button对应的fiber
3.3 然后进去legacy模式的事件处理系统,在这个模式下,批量更新处理事件

4 .原生dom如何找到对应的fiber

1 .为什么这里要找原生呢,编译模板的时候虚拟dom直接对应这个事件不久好了吗?
2 .点击原生dom的时候不会会dispatch一个事件,那个事件不是会告诉当前触发的taget,编译的时候target和事件对一个映射不就这里不用找了吗?
3 .React 在初始化真实 dom 的时候,用一个随机的 key internalInstanceKey 指针指向了当前dom对应的fiber对象,fiber对象用stateNode指向了当前的dom元素

// 声明随机key
var internalInstanceKey = '__reactInternalInstance$' + randomKey;

// 使用随机key 
function getClosestInstanceFromNode(targetNode){
  // targetNode -dom  targetInst -> 与之对应的fiber对象

  var targetInst = targetNode[internalInstanceKey];
}

LegacySimpleEventPlugin插件

1 .通过原生事件类型决定使用哪个合成事件类型,
2 .如果对象池里面有这个类型的实例,就取出,覆盖他的属性,作为本次派发的事件对象,没有就新建一个


10221d4f89b847c29cd87c4aa6ccb3f7_tplv-k3u1fbpfcp-zoom-1.png

3 .从点击的原生事件中找到对应的DOM节点,从DOM节点中找到一个最近的React组件实例,从而找到了一条由这个实例父节点不断向上组成的链条.这个链就是我们要触发合成事件的链
4 .方向触发这条链,模拟捕获阶段,触发所有props中含有onClickCapture的实例.正向触发这条链,子-父,模拟冒泡阶段,触发所有props中含有的onClick的实例
5 .React的冒泡和捕获并不是真正DOM级别的冒泡和捕获
6 .react会在一个原生事件里触发所有相关节点的onClick事件,在执行onClick之前React会打开批量渲染开关,这个开关会将所有的setState变成异步函数
7 .事件只针对原生组件生效,自定义组件不会触发 onClick。--这个不懂啊
8 .我们收到的event对象为React合成事件,event对象在事件之外不可以使用

function onClick(event) {
    setTimeout(() => {
        console.log(event.target.value);
    }, 100);
}

9 .React会在派发事件的时候打开批量更新,此时所有的setState都变成异步

function onClick(event) {
    setState({a: 1}); // 1
    setState({a: 2}); // 2
//这里两次只会触发一次render,
    setTimeout(() => {
        setState({a: 3}); // 3
        setState({a: 4}); // 4
//这里会触发两次render
    }, 0);
}

10 .所有的事件注册到顶层事件上,所以多个ReactDOM.render会存在冲突,所以多版本react在事件上存在冲突
11 .

fb9df1e3d518405aaac807e9ba2ade89_tplv-k3u1fbpfcp-watermark.jpg

legacy 事件处理系统与批量更新

1 .batchedEventUpdates为批量更新的主要函数

/* topLevelType - click事件 | eventSystemFlags = 1 | nativeEvent = 事件源对象  | targetInst = 元素对应的fiber对象  */
function dispatchEventForLegacyPluginEventSystem(topLevelType,eventSystemFlags,nativeEvent,targetInst){
    /* 从React 事件池中取出一个,将 topLevelType ,targetInst 等属性赋予给事件  */
    const bookKeeping = getTopLevelCallbackBookKeeping(topLevelType,nativeEvent,targetInst,eventSystemFlags);
    try { /* 执行批量更新 handleTopLevel 为事件处理的主要函数 */
    batchedEventUpdates(handleTopLevel, bookKeeping);
  } finally {
    /* 释放事件池 */  
    releaseTopLevelCallbackBookKeeping(bookKeeping);
  }
}

2 .react通过开关isBatchingEventUpdate来控制是否启用批量更新

export function batchedEventUpdates(fn,a){
    isBatchingEventUpdates = true;
    try{
       fn(a)
       // handleTopLevel(bookKeeping)
//这里里面触发setState,都是在一个同步内,所以setState会被merge为一次操作,生效,批量更新.
    }finally{
        isBatchingEventUpdates = false
    }
}

3 .handleTopLevel

// 流程简化后
// topLevelType - click  
// targetInst - button Fiber
// nativeEvent
function handleTopLevel(bookKeeping){
    const { topLevelType,targetInst,nativeEvent,eventTarget, eventSystemFlags} = bookKeeping
    for(let i=0; i < plugins.length;i++ ){
        const possiblePlugin = plugins[i];
        /* 找到对应的事件插件,形成对应的合成event,形成事件执行队列  */
        const  extractedEvents = possiblePlugin.extractEvents(topLevelType,targetInst,nativeEvent,eventTarget,eventSystemFlags)  
    }
    if (extractedEvents) {
        events = accumulateInto(events, extractedEvents);
    }
    /* 执行事件处理函数 */
    runEventsInBatch(events);
}

4 .extractEvents 形成事件对象event 和 事件处理函数队列

const  SimpleEventPlugin = {
    extractEvents:function(topLevelType,targetInst,nativeEvent,nativeEventTarget){
        const dispatchConfig = topLevelEventsToDispatchConfig.get(topLevelType);
        if (!dispatchConfig) {
            return null;
        }
        switch(topLevelType){
            default:
            EventConstructor = SyntheticEvent;
            break;
        }
        /* 产生事件源对象 */
        const event = EventConstructor.getPooled(dispatchConfig,targetInst,nativeEvent,nativeEventTarget)
        const phasedRegistrationNames = event.dispatchConfig.phasedRegistrationNames;
        const dispatchListeners = [];
        const {bubbled, captured} = phasedRegistrationNames; /* onClick / onClickCapture */
        const dispatchInstances = [];
        /* 从事件源开始逐渐向上,查找dom元素类型HostComponent对应的fiber ,收集上面的React合成事件,onClick / onClickCapture  */
         while (instance !== null) {
              const {stateNode, tag} = instance;
              if (tag === HostComponent && stateNode !== null) { /* DOM 元素 */
                   const currentTarget = stateNode;
                   if (captured !== null) { /* 事件捕获 */
                        /* 在事件捕获阶段,真正的事件处理函数 */
                        const captureListener = getListener(instance, captured);
                        if (captureListener != null) {
                        /* 对应发生在事件捕获阶段的处理函数,逻辑是将执行函数unshift添加到队列的最前面 */
                            dispatchListeners.unshift(captureListener);
                            dispatchInstances.unshift(instance);
                            dispatchCurrentTargets.unshift(currentTarget);
                        }
                    }
                    if (bubbled !== null) { /* 事件冒泡 */
                        /* 事件冒泡阶段,真正的事件处理函数,逻辑是将执行函数push到执行队列的最后面 */
                        const bubbleListener = getListener(instance, bubbled);
                        if (bubbleListener != null) {
                            dispatchListeners.push(bubbleListener);
                            dispatchInstances.push(instance);
                            dispatchCurrentTargets.push(currentTarget);
                        }
                    }
              }
              instance = instance.return;
         }
          if (dispatchListeners.length > 0) {
              /* 将函数执行队列,挂到事件对象event上 */
            event._dispatchListeners = dispatchListeners;
            event._dispatchInstances = dispatchInstances;
            event._dispatchCurrentTargets = dispatchCurrentTargets;
         }
        return event
    }
}

// 形成React事件独有的合成事件源对象,这个对象,保存了整个事件的信息.将作为参数传递给真正的事件处理函数

//然后声明事件可执行队列,按照冒泡和捕获逻辑,从事件源开始逐渐向上,查找dom元素类型hostComponent对应的fiber,收集上面的react合成 事件,例如Onclick,onClickCapture,对于冒泡阶段的事件,push到执行队列后面,捕获阶段的事件onClickCapture,unshift到队列的前面

//最后将事件执行队列,保存到React事件源对象上,等待执行

5 .React事件源对象

function SyntheticEvent( dispatchConfig,targetInst,nativeEvent,nativeEventTarget){
  this.dispatchConfig = dispatchConfig;
  this._targetInst = targetInst;
  this.nativeEvent = nativeEvent;
  this._dispatchListeners = null;
  this._dispatchInstances = null;
  this._dispatchCurrentTargets = null;
  this.isPropagationStopped = () => false; /* 初始化,返回为false  */

}
SyntheticEvent.prototype={
    stopPropagation(){ this.isPropagationStopped = () => true;  }, /* React单独处理,阻止事件冒泡函数 */
    preventDefault(){ },  /* React单独处理,阻止事件捕获函数  */
    ...
}

runEventInbatch 事件触发

function runEventsInBatch(){
    const dispatchListeners = event._dispatchListeners;
    const dispatchInstances = event._dispatchInstances;
    if (Array.isArray(dispatchListeners)) {
    for (let i = 0; i < dispatchListeners.length; i++) {
      if (event.isPropagationStopped()) { /* 判断是否已经阻止事件冒泡 */
        break;
      }
      
      dispatchListeners[i](event)
    }
  }
  /* 执行完函数,置空两字段 */
  event._dispatchListeners = null;
  event._dispatchInstances = null;

dom上所有带有通过jsx绑定的onClick的回调函数都会按顺序(冒泡或者捕获)会放到Event._dispatchListeners 这个数组里,后面依次执行它
}

1 .dispatchListenersi就是我们的事件处理函数,比如handleClick,所以事件处理函数中,返回false,并不会阻止浏览器默认行为
2 .e.preventDefault() 需要这样才行

事件池

1 .每次用的事件源对象,在事件函数执行完毕之后,可以通过releaseTopLevelbackBookkeeping等方法将事件源对象释放到事件池中,这样的好处就是不必在创建事件源对象,下次使用的时候可以直接从事件池中取出一个事件源对象进行复用.执行完毕之后,再次释放到事件池中
2 .总结

1 .首先通过统一处理函数dispatchEvent,进行批量更新betchUpdate
2 .然后执行事件对应的处理插件中的extractEvents,合成事件源对象,每次React会从事件源开始,从上遍历类型为hostCompoent即DOM类型的fiber,判断props中是否有当前事件,比如onClick,最终形成一个事件执行队列,React就是用这个队列,来模拟事件捕获,事件源,事件冒泡这一过程.
3 .最后通过EventsInBatch执行事件对列,如果发现阻止冒泡,那么break跳出旬换,最后重置事件源,返回事件池中,完成整个流程

react 17事件系统

1 .调整将顶层事件绑定在container上面,而不是document,可以解决多版本共存的问题,可以实现微前端方案
2 .对齐原生浏览器事件,支持原生捕获事件
3 .onFocus,onBlur使用foucsin,blusout合成
4 .onScroll不在进行冒泡,原生的支持冒泡
5 .取消事件复用,优化不明显了,而且容易用错

事件委托

1 .这种如果我们在一个map渲染出来的元素上添加点击事件,他是会自动帮我们委托到一个父级元素上面吗?
2 .先委托到一个父级元素上,Event提供了一个属性叫target.可以返回事件的目标节点.我们成为事件源,也就是说,target对象就可以表示为触发当前事件dom.我们可以根据dom进行判断到底是哪个元素触发了事件,根据不同的元素,执行不同的回调方法
3 .优点

1 .减少事件的注册,能够提升整体性能
2 .半路新怎得dom节点,相应事件的更新,不需要重新绑定

4 .步骤1 .jsx被render函数执行

<div onClick={this.handlerClick}></div>

{
    $$typeof: REACT_ELEMENT_TYPE,
    type:'div',
    key: 'key_name',
    ref: "ref_name",
    props: {
        class: "class_name",
        id: "id_name",
        onClick:fn
    }
     _owner: ReactCurrentOwner.current,
}

5 .React源码流程图

/**
 *
 * Overview of React and the event system:
 *
 * +------------+    .
 * |    DOM     |    .
 * +------------+    .
 *       |           .
 *       v           .
 * +------------+    .
 * | ReactEvent |    .
 * |  Listener  |    .
 * +------------+    .                         +-----------+
 *       |           .               +--------+|SimpleEvent|
 *       |           .               |         |Plugin     |
 * +-----|------+    .               v         +-----------+
 * |     |      |    .    +--------------+                    +------------+
 * |     +-----------.--->|EventPluginHub|                    |    Event   |
 * |            |    .    |              |     +-----------+  | Propagators|
 * | ReactEvent |    .    |              |     |TapEvent   |  |------------|
 * |  Emitter   |    .    |              |<---+|Plugin     |  |other plugin|
 * |            |    .    |              |     +-----------+  |  utilities |
 * |     +-----------.--->|              |                    +------------+
 * |     |      |    .    +--------------+
 * +-----|------+    .                ^        +-----------+
 *       |           .                |        |Enter/Leave|
 *       +           .                +-------+|Plugin     |
 * +-------------+   .                         +-----------+
 * | application |   .
 * |-------------|   .
 * |             |   .
 * |             |   .
 * +-------------+   .
 *                   .
 */
上一篇下一篇

猜你喜欢

热点阅读