React17源码解读—— 事件系统

  读完本篇文章你将明白为什么是React的合成事件SyntheticEvent, 以及React如何模拟浏览器的捕获和冒泡。

  在学习React的合成事件之前,我们先复习下浏览器的事件系统,以及代理委托。这对我理解React事件系统源码非常重要。

  W3C 标准约定了一个事件的传播过程要经过以下 3 个阶段:

  1. 事件捕获阶段
  2. 目标阶段
  3. 冒泡阶段

理解这个过程最好的方式就是读图了,下图是一棵 DOM 树的结构简图,图中的箭头就代表着事件的“穿梭”路径。

  

当我们点击了一个事件, 首先做的的第一件事就是从外层的元素,直接穿梭到我们的目标元素。这个阶段会执行所有捕获阶段的函数, ok, 然后事件流切换到目标阶段,执行自身的事件函数,这时候事件流在沿着相反的方向一直向上执行所有函数。 OK 我们在dom节点 绑定过多的监听事件,必定造成内存浪费。 所以就有一种优化: 就是事件委托

像这样利用事件的冒泡特性,把多个子元素的同一类型的监听逻辑,合并到父元素上通过一个监听函数来管理的行为,就是事件委托。

合成事件——SyntheticEvent

为什么React要自己实现一套事件系统?

答: React 作为一个框架, ok框架的必须要想的多, ok自然想到了可恶的IE, 和通用的浏览器根本不太一样。

从源码中随便举个简单的小例子:

      if (event.preventDefault) {        event.preventDefault();        // $FlowFixMe - flow is not aware of `unknown` in IE      } else if (typeof event.returnValue !== 'unknown') {        event.returnValue = false;      }
复制代码

第二个很重要的 自研事件系统使 React 牢牢把握住了事件处理的主动权, 举个大家都比较熟悉的例子,比如说它想在事件系统中处理 Fiber 相关的优先级概念,或者想把多个事件揉成一个事件(比如 onChange 事件) 。

谈到事件,必定有绑定和触发

React事件的绑定:

React17中的事件的绑定其实已经绑定在diy#container上不在之前的document上了, 先给大家看一下整个流程的调用图:

这个过程是发生在初始化过程,此时dom节点还没有挂载。

listenToNativeEvent 这个函数做的事是非常的简单, 就是调用下面两个函数, addTrappedEventListener 和 addEventBubbleListener  ok 我们着重分析下这两个函数,

let listener = createEventListenerWrapperWithPriority(    targetContainer,    domEventName,    eventSystemFlags,  );
复制代码

第一件事就是创建监听函数, 这里React 对我们用的 事件做了优先级分类主要是以下3种

  1. DiscreteEvent   例如:  foucus | blur | click  …

  2. UserBlockingEvent  例如:  mouseMove | mouseOver …

  3. ContinuousEvent  例如:  progress |  load | error

不同优先级对应的对应的listenr不同,listener 和我们常见的事件监听的职责不太一样, 在React中listener 是一个统一地事件分发函数. 

事件类型的有3种,同样listener 也有3种分发事件

  1. dispatchDiscreteEvent

  2. dispatchUserBlockingUpdate

  3. dispatchEvent 

第一种 和第二种的区别主要体现在优先级上,对事件分发动作倒没什么影响。无论是 dispatchDiscreteEvent 还是 dispatchUserBlockingUpdate,它们最后都是通过调用 dispatchEvent 来执行事件分发的。因此可以认为,最后绑定到div#root 上的这个统一的事件分发函数,其实就是 dispatchEvent。

React 如何dispacthEvent的 ?

我还是举一个例子说明 :

function App() {    const [count, setCount] = useState(0);    const handleClick = () => {        console.error('App -----');    }    const handleCapture = () => {        console.error('cauture----');    }    return (        <div className="App" onClick={handleClick} onClickCapture={handleCapture}>            <header className="App-header">                <h1>{count}</h1>                <div onClick={() => {                    setCount(count + 1);                }}>                    点击我          </div>            </header>        </div>    );}
复制代码

当我点击了div 发生了什么事情

  1. 首先,点击的div 会冒泡到最上层的div 也就是 div #root
  2. 执行dispatchEvent
  3. 创建事件对应的合成事件 SyntheticEvent
  4. 收集捕获的回调函数和对应的节点实例
  5. 收集冒泡的回调函数和对应的节点实例
  6. 执行对应的回调函数,同时将SyntheticEvent 作为参数传入

ok 我先给你分析一下SyntheticEvent 这个源码 其实很简单, 就是做了一件事, 合成事件, 做了兼容处理。

function createSyntheticEvent(Interface: EventInterfaceType) {     function SyntheticBaseEvent(    reactName: string | null, // 接受的事件名字    reactEventType: string, // 事件的类型    targetInst: Fiber, // 对应的Fiber 节点     nativeEvent: {[propName: string]: mixed},// 对应的浏览器原生事件    nativeEventTarget: null | EventTarget,// 原生事件的触发者  ) {
   // 实例初始化 一些参数    this._reactName = reactName;    this._targetInst = targetInst;    this.type = reactEventType;    this.nativeEvent = nativeEvent;    this.target = nativeEventTarget;    this.currentTarget = null;    // InterFace 就是当前事件有哪些 参数    for (const propName in Interface) {      if (!Interface.hasOwnProperty(propName)) {        continue;      }      const normalize = Interface[propName];      if (normalize) {        this[propName] = normalize(nativeEvent);      } else {        this[propName] = nativeEvent[propName];      }    }      const defaultPrevented =      nativeEvent.defaultPrevented != null        ? nativeEvent.defaultPrevented        : nativeEvent.returnValue === false;    if (defaultPrevented) {      this.isDefaultPrevented = functionThatReturnsTrue;    } else {      this.isDefaultPrevented = functionThatReturnsFalse;    }    this.isPropagationStopped = functionThatReturnsFalse;    return this;  }  // 在原型链上挂载我们常用的两个函数 
  Object.assign(SyntheticBaseEvent.prototype, {    preventDefault: function() {      this.defaultPrevented = true;  
      // 默认行为是合成事件的preventDefault, 也是直接调用的原生事件的默认行为      const event = this.nativeEvent;      if (!event) {        return;      }      if (event.preventDefault) {        event.preventDefault();      } else if (typeof event.returnValue !== 'unknown') {        event.returnValue = false;      }      this.isDefaultPrevented = functionThatReturnsTrue;    },    stopPropagation: function() {
      // 注意看重点: 当我们调用合成事件的阻止冒泡事件, 其实调用的浏览器原生的阻止事件向上冒泡      const event = this.nativeEvent;      if (!event) {        return;      }            if (event.stopPropagation) {        event.stopPropagation();      } else if (typeof event.cancelBubble !== 'unknown') {        event.cancelBubble = true;      }      this.isPropagationStopped = functionThatReturnsTrue;    },    // react 17.0 已经取消事件池了    persist: function() {      // Modern event system doesn't use pooling.    },         isPersistent: functionThatReturnsTrue,  });  return SyntheticBaseEvent;}
复制代码

敲重点:

  • React 17  合成事件的 阻止冒泡 和阻止默认行为, 其实调用的就是绑定在当前节点的原生浏览器事件 

1. 没有阻止冒泡的 

2. 阻止冒泡了,按道理就不会冒泡到doucument上, 之前大家的阻止冒泡, 都是获取当前节点的ref,然后去阻止冒泡。 现在已经不用了。舒服

看图,document 上的事件已经被阻止了。

OK合成事件看完了,我们继续往下看, dispatchEvent 之后,中间某些复杂的操作

会收集当前所有捕获的合成event 和listener、 然后放到一个dispacthQueue 队列中,我们来看核心代码:

function processDispatchQueueItemsInOrder(  event: ReactSyntheticEvent,  dispatchListeners: Array<DispatchListener>,  inCapturePhase: boolean,): void {  let previousInstance;
   // inCapturePhase 表示是否在捕获阶段。  if (inCapturePhase) {    for (let i = dispatchListeners.length - 1; i >= 0; i--) {      const {instance, currentTarget, listener} = dispatchListeners[i];
      // 上文合成事件, 如果我们在合成事件调用了e.stopProgation(), 会把合成事件上
      // this.isPropagationStopped = functionThatReturnsTrue; 这是个返回true 的函数
      // 调用了阻止冒泡 函数调用就直接结束。      if (instance !== previousInstance && event.isPropagationStopped()) {        return;      }      executeDispatch(event, listener, currentTarget);      previousInstance = instance;    }  } else {    for (let i = 0; i < dispatchListeners.length; i++) {      const {instance, currentTarget, listener} = dispatchListeners[i];      if (instance !== previousInstance && event.isPropagationStopped()) {        return;      }      executeDispatch(event, listener, currentTarget);      previousInstance = instance;    }  }}
复制代码

这里不知道你有没有注意到  一个正序遍历, 一个倒序遍历, 主要是React 在收集listeners, 是从我们点击的元素一层一层往上找, 这个过程其实其实和浏览器的冒泡阶段地行为是相符合的。 那么捕获极端自然就倒叙遍历, React 这个是真的非常秒哇。

还有一个特别重要的我要和大家强调的是,如果我在全局绑定了很多onClick 事件, 由于是事件代理到div#root ,所以呢合成事件只会创建一次,只是有很多dispacthListeners 而已, 而每个listener 包含了当前的事件的currentTarget。 直接上截图:

大家可以结合我这个数据,去分析上面的代码自然就清楚了。

最后 每一个listener调用后会将event.currentTarget = null;  

好了,到这里React  事件系统 源码分析结束, 其实 React 针对不同的时间 有不同的事件插件Plugin, 然后合成事件也是在每个方法去实现。extraEvent 大家感兴趣地话,可以自行阅读源码。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享