addeventlistener事件第三个参数_通过几个例子来理解 React 的事件系统

news/2024/7/8 2:07:27

22a9777e8e9239fae00c8862a1d73af4.png

来源:Aaaaaaaaaaayou

https://juejin.im/post/6863083643427979271

说明:本文结论均基于 React 16.13.1 得出,若有出入请参考对应版本源码

几个题目

我们先来看几个题目,如果你都能很确定的说出结果,那么这篇文章就不用看了。

点击 BUTTON 打印的结果是:

题目一:

export default class App extends React.Component {
  innerClick = () => {
    console.log('A: react inner click.')
  }

  outerClick = () => {
    console.log('B: react outer click.')
  }

  componentDidMount() {
    document.getElementById('outer').addEventListener('click', () => {
      console.log('C: native outer click')
    })
    document.getElementById('inner').addEventListener('click', () => {
      console.log('D: native inner click')
    })
  }

  render() {
    return (
      <div id='outer' onClick={this.outerClick}><button id='inner' onClick={this.innerClick}>
          BUTTONbutton>div>
    )
  }
}

答案:D C A B

题目二:

export default class App extends React.Component {
  innerClick = (e) => {
    console.log('A: react inner click.')
    e.stopPropagation()
  }

  outerClick = () => {
    console.log('B: react outer click.')
  }

  componentDidMount() {
    document.getElementById('outer').addEventListener('click', () => {
      console.log('C: native outer click')
    })
    document.getElementById('inner').addEventListener('click', () => {
      console.log('D: native inner click')
    })
  }

  render() {
    return (
      <div id='outer' onClick={this.outerClick}><button id='inner' onClick={this.innerClick}>
          BUTTONbutton>div>
    )
  }
}

答案:D C A

题目三:

export default class extends React.Component {
  constructor(props) {
    super(props)
    document.addEventListener('click', () => {
      console.log('C: native document click')
    })
  }

  innerClick = () => {
    console.log('A: react inner click.')
  }

  outerClick = () => {
    console.log('B: react outer click.')
  }

  render() {
    return (
      <div id='outer' onClick={this.outerClick}><button id='inner' onClick={this.innerClick}>
          BUTTONbutton>div>
    )
  }
}

答案:C A B

题目四:

export default class extends React.Component {
  constructor(props) {
    super(props)
    document.addEventListener('click', () => {
      console.log('C: native document click')
    })
  }

  innerClick = (e) => {
    console.log('A: react inner click.')
    e.nativeEvent.stopImmediatePropagation()
  }

  outerClick = () => {
    console.log('B: react outer click.')
  }

  componentDidMount() {
    document.addEventListener('click', () => {
      console.log('D: native document click')
    })
  }

  render() {
    return (
      <div id='outer' onClick={this.outerClick}><button id='inner' onClick={this.innerClick}>
          BUTTONbutton>div>
    )
  }
}

答案:C A B

你全都答对了吗?

DOM 事件

首先,我们先简单地复习下 DOM 事件的相关知识点:

  1. 事件委托。React 利用了事件委托,将事件都绑定在 document 之上。
  2. DOM 事件模型。分成捕获、目标、冒泡阶段。

事件委托

如下所示,我们想监听 li 标签上的点击事件,但是我们不把事件绑定在 li 上,而是绑定在它的父元素上,通过 e.target 来获取当前点击的目标元素,这种做法就是事件委托。通过事件委托我们可以减少页面中的事件监听函数,提升性能。

<ul>
  <li>1li>
  <li>2li>
  <li>3li>
ul>
<script>const $ul = document.querySelector('ul')
  $ul.addEventListener('click', (e) => {console.log(e.target.innerText)
  })script>

DOM 事件模型

我们知道 DOM 事件分为三个阶段:捕获、目标、冒泡。我们通过几个例子来说明其工作流程:

例一:

<div id="id">
  <button id="btn">Buttonbutton>
div>
<script>const $div = document.querySelector('#id')const $btn = document.querySelector('#btn')document.addEventListener('click', () => {console.log('document click')
  })
  $div.addEventListener('click', (e) => {console.log('div click 1')
  })
  $div.addEventListener('click', (e) => {console.log('div click 2')
  })
  $div.addEventListener('click', (e) => {console.log('div click 3')
  })
  $btn.addEventListener('click', () => {console.log('button click')
  })script>

我们知道, addEventListener 第三个参数是指定是否在捕获阶段触发事件相应函数,默认 false,所以上面的事件均在冒泡阶段触发。事件触发的顺序是从下至上,同一个元素上的事件按照绑定的顺序执行,如下图:

0db1388016f63a25fe9717deee50d5b4.png

所以结果是:

button click
div click 1
div click 2
div click 3
document click

例二:

<div id="id">
  <button id="btn">Buttonbutton>
div>
<script>const $div = document.querySelector('#id')const $btn = document.querySelector('#btn')document.addEventListener('click', () => {console.log('document click')
  })
  $div.addEventListener('click', (e) => {console.log('div click 1')
  })
  $div.addEventListener('click', (e) => {
    e.stopPropagation()console.log('div click 2')
  })
  $div.addEventListener('click', (e) => {console.log('div click 3')
  })
  $btn.addEventListener('click', () => {console.log('button click')
  })script>

这里新加了一句 e.stopPropagation(),其作用是阻止事件扩散,所以 document 上的事件监听函数就不会执行了。

ac3783d40c2fedfe1105fde3393347c3.png

例三:

<div id="id">
  <button id="btn">Buttonbutton>
div>
<script>const $div = document.querySelector('#id')const $btn = document.querySelector('#btn')document.addEventListener('click', () => {console.log('document click')
  })
  $div.addEventListener('click', (e) => {console.log('div click 1')
  })
  $div.addEventListener('click',
    (e) => {console.log('div click 2')
    },true
  )
  $div.addEventListener('click',
    (e) => {console.log('div click 3')
    },true
  )
  $btn.addEventListener('click', () => {console.log('button click')
  })script>

这里把 div 的两个事件监听函数绑定在捕获阶段。当事件触发的时候会先执行捕获阶段的监听函数,执行顺序是从上而下,相同元素上仍然按照绑定顺序执行。

ad0e7a350b9125173851581b99cfacc2.png

所以结果是:

div click 2
div click 3
button click
div click 1
document click

例四:

<div id="id">
  <button id="btn">Buttonbutton>
div>
<script>const $div = document.querySelector('#id')const $btn = document.querySelector('#btn')document.addEventListener('click', () => {console.log('document click')
  })
  $div.addEventListener('click', (e) => {console.log('div click 1')
  })
  $div.addEventListener('click',
    (e) => {
      e.stopImmediatePropagation()console.log('div click 2')
    },true
  )
  $div.addEventListener('click',
    () => {console.log('div click 3')
    },true
  )
  $btn.addEventListener('click', () => {console.log('button click')
  })script>

这里新增了 e.stopImmediatePropagation(),该方法是加强版的 stopPropagation,不仅可以阻止向其他元素扩散,也可以在本元素内部阻止扩散。

6629f5242440888231c1c2e61f80dee5.png

React 事件系统

回顾了下 DOM 事件的知识点后我们进入正题,首先我们看 React 事件绑定是怎么做的。

React 事件绑定

首先,我们知道 React 利用了事件委托机制,将所有事件绑定到了 document 之上(17 版本有变动)。具体到代码,可以查看 react-reconciler/src/ReactFiberCompleteWork.old.js 文件:

...
// 通过 FiberNode 创建真实 DOM
// 这里已经执行过类组件的 constructor 方法,但是还没有执行 componentDidMount
const instance = createInstance(
  type,
  newProps,
  rootContainerInstance,
  currentHostContext,
  workInProgress
)

...

if (
  // 该方法最终会进行事件绑定
  finalizeInitialChildren(
    instance,
    type,
    newProps,
    rootContainerInstance,
    currentHostContext
  )
) {
  ...
}

其中 finalizeInitialChildren 最终会调用 react-dom/src/events/EventListener.js 文件中的 addEventBubbleListener

export function addEventBubbleListener(
  target: EventTarget,
  eventType: string,
  listener: Function): Function {
  target.addEventListener(eventType, listener, false)
  return listener
}

注意, constructor 函数在事件绑定前就执行了,而 componentDidMount 则在事件绑定之后才执行。

事件触发

我们用下面的例子来体会事件触发的流程:

export default class App extends React.Component {
  innerClick = () => {
    console.log('A: react inner click.')
  }

  outerClick = () => {
    console.log('B: react outer click.')
  }

  render() {
    return (
      <div id='outer' onClickCapture={this.outerClick}><button id='inner' onClick={this.innerClick}>
          BUTTONbutton>div>
    )
  }
}

当事件在 document 上触发的时候,我们可以拿到原生事件对象 NativeEvent,通过 target 可以访问到当前点击的 DOM 元素 button,通过其属性 __reactFiber$*****(*****表示随机数)可以获取 button 所对应的 FiberNode

同时,React 还会利用 NativeEvent 来生成 SyntheticEvent,其中 SyntheticEvent 有几个重要的属性值得关注下:

  1. nativeEvent,指向 NativeEvent
  2. _dispatchListeners,存储要执行的事件监听函数。
  3. _dispatchInstances,存储要执行的事件监听函数所属的 FiberNode 对象。
759ecbe3ef3d166c3121de158b15bd88.png

接下来就会分捕获和冒泡两个阶段来收集要执行的事件监听函数:

40e7376c2288249731bb644be7cb13e0.png
687851d7db8df378d59894ca15491867.png

最后,按照顺序执行 _dispatchListeners 中的方法,并通过 _dispatchInstances 中的 FiberNode 来得到 currentTarget

export function executeDispatch(event, listener, inst) {
  const type = event.type || 'unknown-event'
  event.currentTarget = getNodeFromInstance(inst)
  invokeGuardedCallbackAndCatchFirstError(type, listener, undefined, event)
  event.currentTarget = null
}

/**
 * Standard/simple iteration through an event's collected dispatches.
 */
export function executeDispatchesInOrder(event) {
  const dispatchListeners = event._dispatchListeners
  const dispatchInstances = event._dispatchInstances
  if (__DEV__) {
    validateEventDispatches(event)
  }
  if (Array.isArray(dispatchListeners)) {
    for (let i = 0; i       if (event.isPropagationStopped()) {
        break
      }
      // Listeners and Instances are two parallel arrays that are always in sync.
      executeDispatch(event, dispatchListeners[i], dispatchInstances[i])
    }
  } else if (dispatchListeners) {
    executeDispatch(event, dispatchListeners, dispatchInstances)
  }
  event._dispatchListeners = null
  event._dispatchInstances = null
}

注意到 event.isPropagationStopped(),该方法是检查当前是否要阻止扩散,假设我们在某个事件监听函数中调用 e.stopPropagation(),则会执行下面的代码:

function functionThatReturnsTrue() {
  return true;
}
...
  stopPropagation: function() {
    const event = this.nativeEvent;
    if (!event) {
      return;
    }

    if (event.stopPropagation) {
      event.stopPropagation();
    } else if (typeof event.cancelBubble !== 'unknown') {
      // The ChangeEventPlugin registers a "propertychange" event for
      // IE. This event does not support bubbling or cancelling, and
      // any references to cancelBubble throw "Member not found".  A
      // typeof check of "unknown" circumvents this issue (and is also
      // IE specific).
      event.cancelBubble = true;
    }

    this.isPropagationStopped = functionThatReturnsTrue;
  }
...

这样,_dispatchListeners 数组中后面的函数就都不会执行了,从而实现了阻止事件扩散的功能。

题目解答

最后,让我们来对文章开头的题目做一个解答。

题目一:

export default class App extends React.Component {
  innerClick = () => {
    console.log('A: react inner click.')
  }

  outerClick = (e) => {
    console.log('B: react outer click.')
  }

  componentDidMount() {
    document.getElementById('outer').addEventListener('click', () => {
      console.log('C: native outer click')
    })
    document.getElementById('inner').addEventListener('click', () => {
      console.log('D: native inner click')
    })
  }

  render() {
    return (
      <div id='outer' onClick={this.outerClick}><button id='inner' onClick={this.innerClick}>
          BUTTONbutton>div>
    )
  }
}
ea4c930df0595be38983cd71e00de146.png

事件模型可以简化为上图,其中 A B 在一个框中表示他们属于同一个事件监听函数中的不同子函数。根据事件冒泡机制,答案为:D C A B

题目二:

export default class App extends React.Component {
  innerClick = () => {
    console.log('A: react inner click.')
    e.stopPropagation()
  }

  outerClick = (e) => {
    console.log('B: react outer click.')
  }

  componentDidMount() {
    document.getElementById('outer').addEventListener('click', () => {
      console.log('C: native outer click')
    })
    document.getElementById('inner').addEventListener('click', () => {
      console.log('D: native inner click')
    })
  }

  render() {
    return (
      <div id='outer' onClick={this.outerClick}><button id='inner' onClick={this.innerClick}>
          BUTTONbutton>div>
    )
  }
}
d2a77006ad1c4e073399f7e7b0883c96.png

调用了 stopPropagation,所以 B 不打印,答案为:D C A

题目三:

export default class extends React.Component {
  constructor(props) {
    super(props)
    document.addEventListener('click', () => {
      console.log('C: native document click')
    })
  }

  innerClick = (e) => {
    console.log('A: react inner click.')
  }

  outerClick = () => {
    console.log('B: react outer click.')
  }

  render() {
    return (
      <div id='outer' onClick={this.outerClick}><button id='inner' onClick={this.innerClick}>
          BUTTONbutton>div>
    )
  }
}
f69de2795bf58678d3b0cca675d52b00.png

constructor 函数先于 React 事件绑定,所以答案为:C A B

题目四:

export default class extends React.Component {
  constructor(props) {
    super(props)
    document.addEventListener('click', () => {
      console.log('C: native document click')
    })
  }

  innerClick = (e) => {
    console.log('A: react inner click.')
    e.nativeEvent.stopImmediatePropagation()
  }

  outerClick = () => {
    console.log('B: react outer click.')
  }

  componentDidMount() {
    document.addEventListener('click', () => {
      console.log('D: native document click')
    })
  }

  render() {
    return (
      <div id='outer' onClick={this.outerClick}><button id='inner' onClick={this.innerClick}>
          BUTTONbutton>div>
    )
  }
}
dc2abb7e92e7cafdfd6e7eaf99be29f3.png

调用原生事件上的 stopImmediatePropagation,会阻止事件在本元素中继续扩散,所以答案为:C A B

f82fe64a7c003c69fd1d72010b457871.png

》》面试官都在用的题库,快来看看《《


http://www.niftyadmin.cn/n/3661282.html

相关文章

为取经而来_唐僧为什么冒着生命危险去为李世民取经,李世民的目的又是什么?...

唐僧去西天取经&#xff0c;是为了李世民&#xff0c;这跟他的追求没有一点关系&#xff0c;作为臣子的盲目崇拜也好&#xff0c;还是作为下属的无知感动也罢&#xff0c;反正去西天取这个经&#xff0c;不是自己本身的意图。那李世民为什么听了观音菩萨的一席话就决定派人去西…

[个人] 确立了新的研究方向

2007年01月31日 13:41:00 昨天晚上&#xff0c;听了wang的javascript和jodo的培训&#xff0c;他讲得更多的是技巧&#xff0c;所以很多人没有听懂&#xff0c;不过我找到了被他忽略了但我很感兴趣的点&#xff1a;javascript的内存模型。感觉好像又回到了若干年前那个意气风发…

html生成器_一本道生成器Python版,笑喷了

今天皮一下&#xff0c;众所周知&#xff0c;一本道是一本正经的胡说八道的简称&#xff0c;想必写过议论文的小伙伴&#xff0c;都知道引经据典是议论文高分必备&#xff0c;套上名人的话更加具有说服力是语文老师必教的知识点。所以呢&#xff0c;今天介绍的这个生成器就走的…

nslookup 包含在那个包中_nslookup命令详解

Nslookup 是一个监测网络中DNS服务器是否能正确实现域名解析的命令行工具。它在 Windows NT/2000/XP 中均可使用,但在Windows 98中却没有集成这一个工具。Nslookup 必须要安装了TCP/IP 协议的网络环境之后才能使用。现在网络中已经架设好了一台 DNS 服务器&#xff0c;主机名称…

[豆趣]成长日记

2007年01月16日 21:36:00 豆跟豆娘对话&#xff1a;"什么叫男儿&#xff1f;"豆娘说&#xff1a;"男儿就是泛指男同志&#xff0c;比方说男儿当自强啊。"&#xff0c;豆一指豆娘&#xff1a;"那&#xff0c;你是女儿啦"&#xff0d;&#xff0d;…

[豆趣]世界上仅存5只恐龙

2007年01月03日 20:26:00 元旦放了天假&#xff0c;火急火燎的赶回家&#xff0c;恰好&#xff0c;给豆在网上淘的《恐龙世纪》也到了&#xff0c;这家伙缠着我要恐龙也有些时日了&#xff0c;本来不想给他买的&#xff0c;可是豆娘说&#xff1a;"总比喜欢奥特曼什么的强…

工业机器人调运角度_FANUC/发那科搬运工业机器人R-2000iC/125L 负载125KG 臂展3100m...

FANUC/发那科搬运工业机器人M-900iA/350 负载350KG 臂展2650mm产品介绍FANUC 发那科重载搬运工业机器人M-900iA/350 负载350kg 工作半径2650mm发那科搬运工业机器人 发那科上下料工业机器人 发那科码垛工业机器人工业 ...FANUC/发那科搬运工业机器人R-2000iB/165F 负载165KG 臂…

[个人]分享ubuntu

2006年12月30日 16:58:00 今天&#xff0c;拿到了从荷兰寄来的ubuntu 6.06 LTS版本&#xff0c;LTS是long time support的缩写&#xff0c;据说&#xff0c;6.06之后又推出了6.10版本&#xff0c;不过不再免费送CD了。当然&#xff0c;ubuntu的老大发话了&#xff0c;2007年要推…