React事件机制 – 合成事件

React事件机制是React重要的基础知识,也是面试过程中经常被提及的问题。本文将介绍React事件机制 - 合成事件的概念以及作用等知识点,本文将重点以React V17版本为准。

本文所用代码仓库

一、 概念介绍

React基于浏览器的事件机制自身实现了一套事件机制,包括事件注册、事件的合成、事件冒泡、事件派发等,在React中这套事件机制被称之为合成事件。

与原生事件直接在元素上注册的方式不同的是,React的合成事件不会直接绑定到目标DOM节点上,用事件委托机制,以队列的方式,从触发事件的组件向父组件回溯,直到Root节点。因此,React组件上声明的事件最终绑定到了Root对象(React17之前是Document)上。在Root节点,用一个统一的监听器去监听,这个监听器上保存着目标节点与事件对象的映射。当组件挂载或卸载时,只需在这个统一的事件监听器上插入或删除对应对象;当事件发生时(即Root上的事件处理函数被执行),在映射里按照冒泡或捕获的路径去组件中收集真正的事件处理函数,然后,由这个统一的事件监听器对所收集的事件逐一执行。

这样做的好处:

• 对事件进行归类,可以在事件产生的任务上包含不同的优先级

• 提供合成事件对象,抹平浏览器的兼容性差异

• 减少内存消耗,提升性能,不需要注册那么多的事件了,一种事件类型只在 Root上注册一次

• 对开发者友好

二、 原生事件回顾

JavaScript原生事件的执行需要经过三个阶段,事件捕获-事件目标-事件冒泡。

1. 事件捕获

当某个元素触发某个事件(如 onclick ),顶层对象 Document 就会发出一个事件流,随着 DOM 树的节点向目标元素节点流去,直到到达事件真正发生的目标元素。

2. 事件目标

当到达目标元素之后,执行目标元素该事件相应的处理函数。如果没有绑定监听函数,那就不执行。

3. 事件冒泡

从目标元素开始,往顶层元素传播。途中如果有节点绑定了相应的事件处理函数,这些函数都会被触发一次。

4. 事件委托/事件代理

简单理解就是将一个响应事件委托到另一个元素。

当子节点被点击时,click 事件向上冒泡,父节点捕获到事件后,我们判断是否为所需的节点,然后进行处理。

三、 合成事件与原生事件区别

React合成事件与原生事件很相似,但不完全相同。这里列举几个常见区别:

1. 事件名称命名方式不同

原生事件命名为纯小写(如:onclick),而 React 事件命名采用小驼峰式,如 onClick 等:


// 原生事件命名方式

<button onclick="handleClick()">原生事件命名方式</button>

//React合成事件命名方式

<button onClick={handleClick}>合成事件命名方式 </button>


2. 事件处理函数写法不同

原生事件中事件处理函数为字符串,在 React JSX 语法中,传入一个函数作为事件处理函数。


// 原生事件处理函数写法

<button onclick="handleClick()">原生事件处理函数写法</button>

//React合成事件处理函数写法

<button onClick={handleClick}>React合成事件处理函数写法</button>


3. 阻止默认行为方式不同

在原生事件中,可以通过返回 false 方式来阻止默认行为,但是在 React 中,需要显式使用e.preventDefault() 方法来阻止。

这里以阻止 标签默认打开新页面为例,介绍两种事件区别:


// 原生事件阻止默认行为方式

<a href="https://www.baidu.com"

  onclick="console.log('原生事件阻止默认行为'); return false"

>

  原生事件阻止默认行为

</a>

// React合成事件阻止默认行为方式

const handleClick = e => {

  e.preventDefault();

  console.log('合成事件阻止默认行为');

}

const clickElement = <a href="https://www.baidu.com" onClick={handleClick}>

  合成事件阻止默认行为

</a>


四、 React合成事件与原生事件执行顺序

在 React 中,合成事件会以事件委托方式绑定在Root对象上。这里我们手写一个简单示例来观察 React 事件和原生事件的执行顺序:


class App extends React.Component {

  parentRef;

  childRef;

  constructor(props) {

    super(props);

    this.parentRef = React.createRef();

    this.childRef = React.createRef();

  }

  componentDidMount() {

    document.addEventListener(

      "click",

      (e) => {

        console.log("原生事件:document DOM 事件捕获---");

      },

      true

    );

    this.parentRef.current?.addEventListener(

      "click",

      () => {

        console.log("原生事件:父元素 DOM 事件捕获---");

      },

      true

    );

    this.childRef.current?.addEventListener(

      "click",

      () => {

        console.log("原生事件:子元素 DOM 事件捕获---");

      },

      true

    );

    document.addEventListener(

      "click",

      (e) => {

        console.log("原生事件:document DOM 事件冒泡!");

      },

      false

    );

    this.parentRef.current?.addEventListener(

      "click",

      () => {

        console.log("原生事件:父元素 DOM 事件冒泡!");

      },

      false

    );

    this.childRef.current?.addEventListener(

      "click",

      () => {

        console.log("原生事件:子元素 DOM 事件冒泡!");

      },

      false

    );

  }

  parentClickFun = () => {

    console.log("React 事件:父元素事件冒泡!");

  };

  parentCapture = () => {

    console.log("React 事件:父元素事件捕获-----");

  };

  childClickFun = () => {

    console.log("React 事件:子元素事件冒泡!");

  };

  childCapture = () => {

    console.log("React 事件:子元素事件捕获-----");

  };

  render() {

    return (

      <div

        ref={this.parentRef}

        onClick={this.parentClickFun}

        onClickCapture={this.parentCapture}

      >

        <p

          ref={this.childRef}

          onClick={this.childClickFun}

          onClickCapture={this.childCapture}

        >

          分析事件执行顺序

        </p>

      </div>

    );

  }

}

export default App;


触发事件后,可以看到控制台输出:



通过上面流程,我们可以总结得出:

• React 所有事件都委托在root 对象上;

• 当真实 DOM 元素触发事件,会冒泡到 root 对象后,再处理 React 事件;

• 在捕获阶段,先注册的先执行,且React合成事件先于原生事件执行;冒泡阶段,先注册的后执行,且原生事件先于React事件执行

五、 阻止冒泡

原生事件阻止冒泡:可以使用 e.stopPropagation() 或者 e.cancelBubble=true(IE)来阻止事件的冒泡传播。

React阻止冒泡:通常开发者都希望使用 e.stopPropagation() 方法来阻止React的事件冒泡,但事实上,由于所有的合成事件本身都是在 Root上执行的,所以,e.stopPropagation() 只能阻止合成事件间冒泡,即下层的合成事件,不会冒泡到上层的合成事件。

因此,在react中,阻止冒泡的方式有三种:

1. 阻止合成事件与非合成事件(除了document)之间的冒泡,以上两种方式都不适用,需要用到e.target 判断。

2. 阻止合成事件与最外层document上的事件间的冒泡,用e.nativeEvent.stopImmediatePropagation();

3. 阻止合成事件间的冒泡,用e.stopPropagation();

存在代码如下


class StopBubbling extends React.Component {

  componentDidMount() {

    document.body.addEventListener("click", this.handleBodyClick, false);

    document.addEventListener("click", this.handleDocumentClick, false);

  }

handleDocumentClick = (e) => {

    console.log("handleDocumentClick!");

  };

  handleBodyClick = (e) => {

    console.log("handleBodyClick! ");

  };

  handleClickTestBox = (e) => {

    console.warn("handleClickTestBox!");

  };

  handleClickTestBox2 = (e) => {

    console.warn("handleClickTestBox2!");

  };

  handleClickTestBox3 = (e) => {

    console.warn("handleClickTestBox3!");

  };

  render() {

    return (

      <div

        className="test-box"

        onClick={this.handleClickTestBox}

        style={{ background: "red" }}

      >

        Box

        <div

          onClick={this.handleClickTestBox2}

          style={{ background: "green", margin: "10px" }}

        >

          Box2

          <div

            id="inner"

            onClick={this.handleClickTestBox3}

            style={{ background: "yellow", margin: "10px" }}

          >

            Box3

          </div>

        </div>

      </div>

    );

  }

}

export default StopBubbling;


当没有任何阻止冒泡时,点击Box3输出:

阻止合成事件与非合成事件(除了document)之间的冒泡


handleBodyClick = (e) => {

  //阻止合成事件与非合成事件(除了document)之间的冒泡

  if (e.target && e.target === document.querySelector("#inner")) {

    return;

  }

  console.log("handleBodyClick!");

};


此时,点击Box3输出:

阻止合成事件与最外层document上的事件间的冒泡


handleClickTestBox3 = (e) => {

  // 阻止与原生事件的冒泡

  e.nativeEvent.stopImmediatePropagation();

  console.warn("handleClickTestBox3!");

};


此时,点击Box3输出:

阻止合成事件间的冒泡


handleClickTestBox3 = (e) => {

  // 阻止合成事件的冒泡

  e.stopPropagation();

  console.warn("handleClickTestBox3!");

};


此时,点击Box3输出:

注意,合成事件和原生事件最好不要混用。

因为,React中的执行e.stopPropagation()可以阻止React合成事件冒泡,但是不能阻止原生事件冒泡;反之,在原生事件中执行e.stopPropagation()阻止冒泡行为,会导致所有元素的事件将无法冒泡到Root上,即所有的 React 事件都将无法被注册。

推荐学习:

【React深入】React事件机制:

实现React事件机制(1、2)

参考文献:

探索 React 合成事件

react中阻止事件冒泡

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

推荐阅读更多精彩内容