React17源码解读—— 事件系统

简介: 读完本篇文章你将明白为什么是React的合成事件SyntheticEvent, 以及React如何模拟浏览器的捕获和冒泡。  在学习React的合成事件之前,我们先复习下浏览器的事件系统,以及代理委托。这对我理解React事件系统源码非常重要。  W3C 标准约定了一个事件的传播过程要经过以下 3 个阶段:

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


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


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


  1. 事件捕获阶段


  1. 目标阶段


  1. 冒泡阶段


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


 image.png


当我们点击了一个事件, 首先做的的第一件事就是从外层的元素,直接穿梭到我们的目标元素。这个阶段会执行所有捕获阶段的函数, 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上了, 先给大家看一下整个流程的调用图:


image.png


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


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


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


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

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


  1. UserBlockingEvent  例如:  mouseMove | mouseOver ...


  1. ContinuousEvent  例如:  progress |  load | error


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


image.png


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


  1. dispatchDiscreteEvent


  1. dispatchUserBlockingUpdate


  1. 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----');    }    useEffect(() => {        document.addEventListener('click',()=>{            console.log('我到底有没有被阻止')        })    }, [])    return (        <div className="App" onClick={handleClick} onClickCapture={handleCapture}>            <header className="App-header">                <h1>{count}</h1>                <div onClick={(e) => {                    // e.stopPropagation();                    setCount(count + 1);                }}>                    点击我          </div>            </header>        </div>    );}
复制代码


  1. 执行dispatchEvent


  1. 创建事件对应的合成事件 SyntheticEvent


  1. 收集捕获的回调函数和对应的节点实例


  1. 收集冒泡的回调函数和对应的节点实例


  1. 执行对应的回调函数,同时将SyntheticEvent 作为参数传入


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



function createSyntheticEvent(Interface: EventInterfaceType) {   function SyntheticBaseEvent(    reactName: string | null,    reactEventType: string,    targetInst: Fiber,    nativeEvent: {[propName: string]: mixed},    nativeEventTarget: null | EventTarget,  ) {    this._reactName = reactName;// 事件名字    this._targetInst = targetInst; // Fiber 节点    this.type = reactEventType;     this.nativeEvent = nativeEvent; // 原生事件    this.target = nativeEventTarget;    this.currentTarget = null;    
    // 初始化 做一些赋值操作
    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];      }    }    // 兼容可恶的ie    const defaultPrevented =      nativeEvent.defaultPrevented != null        ? nativeEvent.defaultPrevented        : nativeEvent.returnValue === false;    if (defaultPrevented) {      this.isDefaultPrevented = functionThatReturnsTrue;    } else {      this.isDefaultPrevented = functionThatReturnsFalse;    }
    // 默认是不冒泡的, 但是react如果调用了阻止冒泡会把这个函数设为functionThatReturnsTrue    this.isPropagationStopped = functionThatReturnsFalse;    return this;  }  Object.assign(SyntheticBaseEvent.prototype, {    preventDefault: function() {
      // 掉用的就是原生的浏览器行为      this.defaultPrevented = true;      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() {
      // 原生的浏览器事件 React 17      const event = this.nativeEvent;      if (!event) {        return;      }      if (event.stopPropagation) {        event.stopPropagation();           } else if (typeof event.cancelBubble !== 'unknown') {           event.cancelBubble = true;      }      // 这就是React 自己在冒泡的时候 函数终止的条件      this.isPropagationStopped = functionThatReturnsTrue;    },    // 不在使用事件池了    persist: function() {      // Modern event system doesn't use pooling.    },    isPersistent: functionThatReturnsTrue,  });  return SyntheticBaseEvent;}
复制代码


敲重点:


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


1. 没有阻止冒泡的


image.png


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


image.png


看图,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。 直接上截图:


image.png


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


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


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



相关文章
|
1月前
|
移动开发 前端开发 JavaScript
React 表单与事件
10月更文挑战第10天
38 1
|
21天前
|
前端开发 JavaScript 开发者
React 事件处理机制详解
【10月更文挑战第23天】本文介绍了 React 的事件处理机制,包括事件绑定、事件对象、常见问题及解决方案。通过基础概念和代码示例,详细讲解了如何处理 `this` 绑定、性能优化、阻止默认行为和事件委托等问题,帮助开发者编写高效、可维护的 React 应用程序。
65 4
|
1月前
|
前端开发 JavaScript IDE
React 事件处理
10月更文挑战第8天
21 1
|
1月前
|
前端开发 JavaScript
一文详解React事件中this指向,面试必备
一文详解React事件中this指向,面试必备
46 0
|
2月前
|
前端开发 JavaScript
react学习(19)事件处理
react学习(19)事件处理
|
1月前
|
移动开发 JSON 数据可视化
精选八款包括可视化CMS,jquery可视化表单,vue可视化拖拉,react可视化源码
精选八款包括可视化CMS,jquery可视化表单,vue可视化拖拉,react可视化源码
49 0
|
2月前
|
前端开发 JavaScript
React的事件与原生事件的执行顺序?
React的事件与原生事件的执行顺序?
|
3月前
|
前端开发 JavaScript Java
React 中的合成事件
【8月更文挑战第30天】
48 6
|
3月前
|
前端开发 JavaScript
React 中的事件是什么?
【8月更文挑战第30天】
73 5
|
3月前
|
存储 前端开发 JavaScript
React中的事件处理(八)
【8月更文挑战第15天】React中的事件处理
35 1