​React太劝退,通过anu学合成事件

时间:2022-07-26
本文章向大家介绍​React太劝退,通过anu学合成事件,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

相信React开发者或多或少听说过React合成事件(SyntheticEvent)这一概念。

合成事件这块源码代码量多、耦合了很多其他逻辑,读起来很劝退。

最近刚好在改一个anubug,发现anu合成事件实现的简单易懂。为什么不通过anu来学合成事件呢?

anu是什么

anu是司徒正美老师开发的类React框架,他的特点是:

  • 支持React16的各种新功能
  • 跑通官方近800多个单元测试
  • 支持React全家桶
  • 支持99%的antd组件

以上是面向开发者的特点。

源码层面,anu的架构和Reactv17是很像的,体积却只有React的1/3,通过他来学习React源码的一些流程再合适不过了。

让我们开始吧。

合成事件是什么、有什么用

合成事件是React在浏览器原有捕获->目标->冒泡事件运行机制的基础上重新实现的一套事件运行机制。

为什么要在浏览器事件运行机制之上再重新造轮子呢?最主要的原因是:

浏览器原生实现中,event触发后会在DOM树中依次完成捕获->目标->冒泡

在此过程中经过的DOM如果注册了event handler,则handler会被调用。

而在React内部,并不直接操作DOM,而是操作一棵与DOM树有映射关系的虚拟DOM树(fiber树)。

比如,对于如下应用:

function App() {
  return (
    <div>
      <p onClick={() => console.log('click')}>click~</p>
    </div>
  )
}

ReactDOM.render(<App/>, root);

DOM树与fiber树分别为:

DOM树:     fiber树:

 html     FiberRootNode
  |            |
 body       rootFiber
  |            |
 div          App fiber
  |            |
  p           div fiber
               |
               p fiber

可见,DOM树与fiber树并不是一一对应的。

onClick handler作为props保存在p对应的fiber上,而不是p DOM上。

所以React需要模拟DOM树中事件的传递机制,实现一套类似机制在fiber树中传递事件。

当重新实现整套事件机制后,要在其上再增加一些特性就再容易不过了,比如:

  1. 抹平不同浏览器事件机制的不同(IE说的就是你)
  2. 对事件的定制化需求。

比如在React中,表单组件的change事件的触发时机其实对标的是原生DOM中的input事件。

再比如在React中,focus事件是由原生DOM中的focusinfocusout实现的。

  1. 优先级机制。

React中,不同事件的优先级不同。在不同事件的event handler中触发的setState会以不同优先级执行。

合成事件的实现

以下实现的代码皆来自anu

合成事件的实现原理很好理解:

  1. document绑定event handler,通过事件委托的方式监听事件
  2. 当事件触发后,通过e.target获取触发事件的DOM,找到DOM对应的fiber
  3. 从该fiber根fiber遍历,收集遍历过程中所有绑定了该类型事件的fiberevent handler,保存在数组paths
  4. 遍历paths,依次调用event handler,模拟捕获流程
  5. 遍历paths.reverse(),依次调用event handler,模拟冒泡流程

接下来我们以click事件举例:

  1. 调用addGlobalEvent('click')注册全局handler用于事件委托

其中dispatchEventhandler

function addGlobalEvent(name, capture) {
  if (!globalEvents[name]) {
    globalEvents[name] = true;
    // addEventListener的实现
    addEvent(document, name, dispatchEvent, capture);
  }
}
  1. 当点击DOM,触发dispatchEvent
function dispatchEvent(e, type, endpoint) {
  
  e = new SyntheticEvent(e);
  // ...一些前置处理,省略

  Renderer.batchedUpdates(function() {
    // 3. 通过collectPaths收集fiber沿途的click handler
    let paths = collectPaths(e.target, terminal, {});
    let captured = bubble + 'capture';
    // 4. 模拟捕获流程
    triggerEventFlow(paths, captured, e);

    if (!e._stopPropagation) {
      // 5. 模拟冒泡流程
      triggerEventFlow(paths.reverse(), bubble, e);
    }
  }, e);
}

其中triggerEventFlow就是简单的遍历数组并执行回调。

function triggerEventFlow(paths, prop, e) {
  for (let i = paths.length; i--; ) {
    let path = paths[i];
    let fn = path.events[prop];
    if (isFn(fn)) {
        e.currentTarget = path.node;
        fn.call(void 666, e);
        if (e._stopPropagation) {
            break;
        }
    }
  }
}

总结

现在我们知道了,当向p组件传递onClick props,组件本身并不会绑定对应的handler,组件销毁后也不会有click handler的解绑操作。

“p对应DOM响应点击事件”的原因是:

DOM对应的fiber上的onClick回调在dispatchEvent方法中的collectPaths中被收集,并在triggerEventFlow中被调用。