前言
我们都知道 React 组件绑定事件的本质是代理到 document 上,然而面试被问到,为什么要这么设计,有什么好处吗?
我知道肯定不会是因为虚拟 DOM 的原因,因为 Vue 的事件就能挂载到真实的 DOM 节点。所以继续往下探究吧
React 模拟 DOM 事件冒泡的原理
设有一段代码如下
react demo
我们在 child 和 parent 两个节点都挂上了 onClick 函数,并且点击 child 触发事件,的确先输出 child!后输出 parent!。此刻你们或许留意到了下图,浏览器的反馈是事件的确只有一个,就是挂在 document 上的。
这个事件就是 dispatchDiscreteEvent。简言之,react 自己定了一个 event 对象,存放着 onClick 回调们,在用户触发点击点击事件时,挨个检查并执行。
利弊
我们都知道事件委托的好处,可以减少 DOM 上的事件对象节省内存,优化页面性能。这么说还是抽象,举个例子,若有一 10w 项列表,点击列表某一项要提示这一列表的某个信息,若你使用 Vue,会在每一个 li 节点挂载事件,10w 个事件将会极大程度上拖慢你的浏览器性能,你可以运行下面的例子明显感到 DOM 加载慢。
- {{ item }}
let list = [];
for (let i = 0; i < 1000000; i++) {
list.push(i);
}
var app = new Vue({
el: '#app',
data: {
list: list,
},
methods: {
handleFn() {
}
}
})
解决这个问题的唯一途径就是事件代理,只需要把事件挂载到 ul 上,并判定 event.target 来自某个 li。
react 挂载到 document 上的行为天生做了事件代理,省了你这一步操作。
但是弊端还是有的,由于 react 的机制,使得它包装了一层,开发者没法在冒泡阶段拿到原生的事件对象,那么就提高了学习成本。
并且在开发者“不知情”的情况下埋下了一个坑,若你在 document 上挂载自定义的事件,并且调用了 e.stopImmediatePropagation() 就不会再执行 react 自身绑定在 document 上的事件。见下面的例子
React 怎么禁止事件的冒泡
设上文代码,点击了 child 后,只希望 child 的事件被触发,parent 的不被触发怎么做?
结论显而易见是 stopPropagation。
{ console.log('child!'); e.stopPropagation() }}>
然而上文我们提到过,react 提供的事件对象是它自己合成的事件对象,它的冒泡是模拟的,它的事件模型应该如下图,下文图片出自 github-youngwind -React 事件代理与 stopImmediatePropagation
那么这个 e.stopPropagation() 是什么?
贴上了部分源码,简单解释下,react 的合成事件里删除了原生事件的 stopPropagation,并自己模拟实现了一个,它标记了一下 this.isPropagationStopped
为 true,挨个遍历合成事件对象里的回调之中,回去检查这个属性,为 true 则不继续向下执行。
function SyntheticEvent(dispatchConfig, targetInst, nativeEvent, nativeEventTarget) {
{
// these have a getter/setter for warnings
delete this.nativeEvent;
delete this.preventDefault;
delete this.stopPropagation;
delete this.isDefaultPrevented;
delete this.isPropagationStopped;
}
// ......省略代码
_assign(SyntheticEvent.prototype, {
preventDefault: function() {
// ......省略代码
},
stopPropagation: function () {
var 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;
},
})
// ......省略代码
}
但其实吧,我们还有另外一种方式可以组织这种冒泡,就是拿到原生事件对象调用 stopImmediatePropagation,如 e.nativeEvent.stopImmediatePropagation。stopImmediatePropagation 能够阻止挂载到某个 DOM 节点上多个事件的后续执行。下文图片出自 github-youngwind -React 事件代理与 stopImmediatePropagation
那么来探究一下这个合成事件到底是个怎么回事儿?
首先 document 上挂载的是 dispatchDiscreteEvent 回调函数
function dispatchDiscreteEvent(topLevelType, eventSystemFlags, container, nativeEvent) {
flushDiscreteUpdatesIfNeeded(nativeEvent.timeStamp);
discreteUpdates(dispatchEvent, topLevelType, eventSystemFlags, container, nativeEvent);
}
上面函数代理了一堆操作,但总之接下来尝试分发事件。
function attemptToDispatchEvent(topLevelType, eventSystemFlags, container, nativeEvent) {
// TODO: Warn if _enabled is false.
var nativeEventTarget = getEventTarget(nativeEvent);
// 这个东西就是 react 的虚拟节点 FiberNode {tag: 5, key: null, elementType: "div", type: "div", stateNode: div#child, …}
var targetInst = getClosestInstanceFromNode(nativeEventTarget);
// ...... 省略判断触发节点是否有效性
{
dispatchEventForLegacyPluginEventSystem(topLevelType, eventSystemFlags, nativeEvent, targetInst);
} // We're not blocked on anything.
return null;
}
跳过两步,执行到一个叫 executeDispatchesInOrder 的函数,就要开始按顺序的触发事件。注意函数参数 event 对象,此对象中存放了所有我们 onClick 预设的回调函数。
function executeDispatchesInOrder(event) {
// event._dispatchListeners 其实就是 onClick 的回调函数。
// (2) [ƒ, ƒ]
// 0: ƒ onClick(e)
// 1: ƒ onClick()
var dispatchListeners = event._dispatchListeners;
// event._dispatchInstances 其实就是 child 和 parent 的两个虚拟节点
// (2) [FiberNode, FiberNode]
var dispatchInstances = event._dispatchInstances;
if (Array.isArray(dispatchListeners)) {
// 循环执行回调,除非有 e.stopPropagation() 被触发,让 isPropagationStopped 的标记为 true。
for (var i = 0; i < dispatchListeners.length; 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;
}
那么真正执行事件并触发回掉的过程是这样的,创造了一个叫 react 的假节点,创造了一个事件 evt 并挂到这个节点上,手动触发它,最后再销毁。evt 的回掉内容就是我们的 onClick 里的内容
{
// ......省略代码
var fakeNode = document.createElement('react');
// ......省略代码
var evt = document.createEvent('Event');
// ......省略代码
function callCallback() {
fakeNode.removeEventListener(evtType, callCallback, false);
// ......省略代码
// 注意这个 func 就是我们的回掉 ƒ onClick() { console.log('child!') }
func.apply(context, funcArgs);
}
// ......省略代码
var evtType = "react-" + (name ? name : 'invokeguardedcallback'); // Attach our event handlers
fakeNode.addEventListener(evtType, callCallback, false); // Synchronously dispatch our fake event. If the user-provided function
// errors, it will trigger our global error handler.
evt.initEvent(evtType, false, false);
fakeNode.dispatchEvent(evt);
// ......省略代码
}
接着循环去执行下一个事件。
参考
github-youngwind -React 事件代理与 stopImmediatePropagation
React官网-事件处理
知乎-超厉害-React事件机制