Skip to main content

react16合成事件

js 事件机制

先回顾下 js 的事件机制

事件触发三阶段

  1. window 往事件触发处传播,遇到捕获事件会触发回调
  2. 传播到事件触发处时触发事件回调
  3. 从事件触发往 window 传播,遇到冒泡事件会触发回调

事件委托

在事件机制中,执行完目标事件回调后,事件会往父元素冒泡,最终直到根元素。事件委托其实就是依赖于事件冒泡实现的。事件委托相较于监听目标事件的优势在于 统一管理事件节省内存开销

<ul id="ul">
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
<script>
// 事件委托小栗子
let ul = document.querySelector("#ul");
ul.addEventListener("click", (event) => {
console.log(event.target);
});
</script>

事件常用方法

  • e.stopPropagation 阻止事件捕获或冒泡
  • e.preventDefault 阻止默认行为
  • e.stopImmediatePropagation 阻止触发同一元素的其他相同事件

react 事件机制

源码执行流程图如下: 事件机制.png

1.事件注册

react在解析jsx props时会取出绑定的事件,通过事件的 map 对象找到原生事件类型,然后通过listenTo方法将事件代理到document上。listenTo会将事件分为trapCapturedEvent(捕获)和trapBubbledEvent(冒泡)两种类型进行注册

trapCapturedEvent:scroll/focus/blur/close...,其余默认是冒泡事件,通过 trapBubbledEvent 注册,本质上其实就是 addEventListener 的第三个参数设为 false 或 true

ps:video、audio、iframe、object对象的事件得单独处理。例如 video、audio 上存在一些媒体事件(例如 onplay、onpause),而这些事件是 document 不具有的,那么只能在这些标签上进行事件绑定

2.存储事件回调

react通过EventPluginHub来统一管理事件存储和plugins,首先看下怎么存储

var listenerBank = {};
var getDictionaryKey = function (inst) {
return '.' + inst._rootNodeID;
}
var EventPluginHub = {
putListener: function (inst, registrationName, listener) {
...
var key = getDictionaryKey(inst);
var bankForRegistrationName = listenerBank[registrationName] || (listenerBank[registrationName] = {});
bankForRegistrationName[key] = listener;
...
}
}

可以看到,react 通过全局对象listenerBank来存储事件回调,以事件类型registrationName为一级 key,存储该事件所有回调,以组件标识(_rootNodeID)为二级 key,对应 value 为该事件的回调函数。后面执行事件时,就可以通过这两个 key 取到触发元素以及父级元素的事件回调。到这里事件注册和存储告一段落,等待事件执行

3.事件执行

执行过程

主要是通过ReactEventListenerdispatchEvent进行事件分发。下面跟着源码理解下整体执行流程

var ReactEventListener = {
dispatchEvent: function (topLevelType, nativeEvent) {
...
ReactUpdates.batchedUpdates(handleTopLevelImpl, bookKeeping);
...
}
}
// react 事务更新,这里先简单理解为设置isBatching,调用handleTopLever
export function batchedUpdates(fn, bookkeeping) {
if (isBatching) {
return fn(bookkeeping)
}
isBatching = true
try {
return _batchedUpdatesImpl(fn, bookkeeping)
} finally {
isBatching = false
const controlledComponentsHavePendingUpdates = needsStateRestore()
if (controlledComponentsHavePendingUpdates) {
_flushInteractiveUpdatesImpl()
restoreStateIfNeeded()
}
}
}

let _batchedUpdatesImpl = function(fn, bookkeeping) {
return fn(bookkeeping)
}

可以看到,react 的核心事件执行函数是handleTopLevel,看下该函数的源码

function handleTopLevel(bookKeeping) {
let targetInst = bookKeeping.targetInst; // 获取触发对象

let ancestor = targetInst;
// do-while循环,主要是找到触发元素的所有父级元素
// 依次加入bookKeeping.ancestors数组
do {
if (!ancestor) {
bookKeeping.ancestors.push(ancestor);
break;
}
// 查找元素对应的根元素
// 如果根元素不存在,说明该元素已从react tree移除,也就没有必要设置事件了
const root = findRootContainerNode(ancestor);
if (!root) {
break;
}
bookKeeping.ancestors.push(ancestor);
// 获取最近的父元素
ancestor = getClosestInstanceFromNode(root);
} while (ancestor);

// 遍历所有父元素
for (let i = 0; i < bookKeeping.ancestors.length; i++) {
targetInst = bookKeeping.ancestors[i];
runExtractedEventsInBatch(
bookKeeping.topLevelType,
targetInst,
bookKeeping.nativeEvent,
getEventTarget(bookKeeping.nativeEvent)
);
}
}

function findRootContainerNode(inst) {
while (inst.return) {
inst = inst.return;
}
if (inst.tag !== HostRoot) {
return null;
}
return inst.stateNode.containerInfo;
}

上面已经取到触发事件的元素以及父级元素,并通过runExtractedEventsInBatch方法来生成合成事件和合并事件,得到一个回调队列,等待执行。最后通过executeDispatchesAndReleaseTopLevel遍历执行回调队列

const executeDispatchesAndReleaseTopLevel = function (e) {
return executeDispatchesAndRelease(e, false);
};

const executeDispatchesAndRelease = function (
event: ReactSyntheticEvent,
simulated: boolean
) {
if (event) {
executeDispatchesInOrder(event, simulated);
if (!event.isPersistent()) {
// 主要用于异步访问事件属性,在事件上调用 event.persist()
// 此方法会从池中移除合成事件,允许用户代码保留对事件的引用
event.constructor.release(event);
}
}
};
export function executeDispatchesInOrder(event, simulated) {
const dispatchListeners = event._dispatchListeners;
const dispatchInstances = event._dispatchInstances;
if (Array.isArray(dispatchListeners)) {
for (let i = 0; i < dispatchListeners.length; i++) {
// 阻止react事件冒泡
if (event.isPropagationStopped()) {
break;
}
executeDispatch(
event,
simulated,
dispatchListeners[i],
dispatchInstances[i]
);
}
} else if (dispatchListeners) {
executeDispatch(event, simulated, dispatchListeners, dispatchInstances);
}
event._dispatchListeners = null;
event._dispatchInstances = null;
}
function executeDispatch(event, simulated, listener, inst) {
const type = event.type || "unknown-event";
event.currentTarget = getNodeFromInstance(inst);
invokeGuardedCallbackAndCatchFirstError(type, listener, undefined, event);
event.currentTarget = null;
}

executeDispatchesInOrder中我们就按照顺序执行了所有监听的事件了。

综上,首先事件统一派发函数dispatchEvent会根据 react 事件类型和原生事件对象获取事件节点_rootNodeID和原生事件类型,再根据它俩从EventPluginHub找到对应事件的回调函数。EventPluginHub会调用预先注入好的plugins将原生事件对象转换成合成事件对象

合成事件

合成事件相比较原生事件的优势有:

  • 抹平浏览器事件差异
  • 更完善的事件函数

事件池 pool

用于存储合成事件对象,相当于缓存作用,不用频繁创建合成事件对象,React 在启动时就会为每种合成对象分配内存池,用到某一个事件对象时就可以从这个内存池进行复用,节省内存

react 阻止事件冒泡的正确姿势

首先要明白几点,react合成事件的stopPropagation非原生事件对象的 stopPropagation,合成事件是在原生事件机制的冒泡阶段执行的,所以原生事件的执行是先于合成事件的。解决事件冒泡的方法主要有下面几点:

  1. 不要混用 react 事件和原生事件
  2. e.stopPropagation阻止合成事件的冒泡。react在执行事件队列时,如果判断e.isPropagationStopped()true,会break跳出队列的循环,达到阻止冒泡的效果
  3. 第二步并不能阻止 document 上的多个相同事件的依次执行(因为事件代理在 document 上,并且 react 中后定义的事件会覆盖前面定义的相同事件),需要借助原生事件e.nativeEventstopImmediatePropagation方法来阻止
function handleClick(e) {
e.stopPropagation();
e.nativeEvent.stopImmediatePropagation();
}

<button onClick={handleClick}></button>;
  1. 要是混用了 react 事件和除了 document 上的原生事件,还想要阻止冒泡,可以通过判断e.target来实现
function contains(root, node) {
while (node) {
if (node === root) {
return true;
}
node = node.parentNode;
}
return false;
}
document.body.addEventListener("click", function (e) {
if (contains(root, e.target)) {
return;
}
});