前言
React合成事件算是八股文常客了,大家在背八股文的时候时常会看到他,一般来说他的答案也差不多是这样的:
React基于虚拟DOM实现了一个合成事件层,定义事件的处理器会接收到一个合成事件对象的实例,它符合W3C标准,且与原生的浏览器事件拥有同样的接口,支持冒泡机制、捕获机制,所有的事件都自动绑定在最外层上,吧啦吧啦。
总的来说就是把事件都绑在root上了,维护一个事件池,然后通过事件委托以及各种逻辑精准触发目标元素的绑定的事件回调。
以上概念大家基本都清楚,但正如本文标题一般,我们真的理解了这堆概念了么?鄙人认为肯定存在着没有完全理解的,本文后半段会放出几个小demo(可点击目录标题demo们跳转),如果都答对,不谈是否完全理解了合成事件,起码理解程度肯定不低。
ok,在看demo之前,先给给大家讲个小故事。
糟糕,一个小小开发问题暴露了基础不牢
最近同事接到了有个需求,需要实现的功能不难,但由于使用的是第三方库,这个库React库,所以在React中使用使用,灵活性稍微低了些。于是同事就卡住了,接连问了以下几点。
- 问题一:如何在原生节点插入React组件?
大致的情况是这样的。
有一个盒子(第三方库提供的),在一定条件下才会出现,这个盒子允许你去添加元素,但它的底层是采用innerHTML的方式,所以想要添加元素,就只能写字符串,这样就无法添加React元素了。不过幸运的是,它还提供了open(盒子出现了)和onBeforeClose(盒子关闭/卸载前)方法。
大致demo如下:
js
import { useRef } from 'react';
import { Button } from 'antd';
import './App.css';
let stage;
const onOpen = () => {};
const onBeforeClose = () => {};
function App() {
const containerRef = useRef();
const handleClick = () => {
if (stage) {
// 卸载前
onBeforeClose(stage);
containerRef.current.removeChild(stage);
stage = null;
} else {
stage = document.createElement('div');
stage.id = 'stage';
containerRef.current.appendChild(stage);
// 挂载后
onOpen(stage);
}
};
return (
<div ref={containerRef}>
<Button
type="primary"
onClick={handleClick}
>
点击
</Button>
</div>
)
}
export default App
稍微添加了点样式,这个盒子的标题采用的是伪元素添加的,不用在意。
好,现在我们是需要点击才会出现这个盒子,那回到问题上,我们如何给这个盒子添加React元素呢?
一般遇到这种情况,可以通过ReatDOM.render方法来解决。render方法主要是将一个React元素渲染到原生节点上,就像我们入口处,将渲染到root上一样。
那么我们的onOpen可以这样写。
js
// 要渲染的react组件
const InsertEle = () => {
return <div>InsertEle</div>
};
const onOpen = () => {
ReactDOM.render(
<InsertEle />,
document.getElementById('stage')
);
};
好了,我们的React组件就成功渲染进去了。
但是由于组件的复杂度有点高,性能负担上来了,所以进行了一个小优化。
js
const onOpen = () => {
const fragment = new DocumentFragment();
ReactDOM.render(
<InsertEle />,
fragment
);
document.getElementById('stage').appendChild(fragment);
};
最后的效果也仍然是把React组件渲染上去了。
- 问题二:渲染上去的React组件怎么事件都不生效了?
这个问题就涉及到React合成事件了。我们先还原下场景。
先给InsertEle这个组件添加一个点击事件。
js
const InsertEle = () => {
return <div onClick={() => console.log('insertEle')}>InsertEle</div>
};
每次点击就打印一次insertEle。
无论我们如何点击,控制台依旧空空如也,很明显,就是事件失效了。
解决方法一:不要增加fragment这一层,直接render到stage上
将onOpen改回优化前的一版。
js
const onOpen = () => {
ReactDOM.render(
<InsertEle />,
document.getElementById('stage')
);
};
看效果。
解决方法二:使用createPortal
将插入的组件使用createProtal包一下
js
import { createPortal } from 'react-dom'
const InsertEle = () => {
return createPortal(
<div onClick={() => console.log('insertEle')}>InsertEle</div>,
document.getElementById('stage'),
)
};
看效果。
知道了这两个技巧之后,同事的需求也是完美完成了,打着哈哈和我夸张地自嘲到:"完了,我完美地暴露了我的基础不牢,还好没去问leader,要不免不了被替掉😭。不过这次我对React合成事件理解更深了,肯定不会有问题了。"
听完后,我质疑地问了一句:"哦?是吗?"
于是乎,多了一下几个demo,从更多方面地考了下同事对React合成事件的理解。
Demo们
- demo1
上面故事的那个
问题:为何InsertEle的onClick事件无法触发?
- demo2
js
import { useRef, useEffect } from 'react';
import { Button } from 'antd';
import './App.css';
function App() {
const containerRef = useRef();
useEffect(() => {
containerRef.current.onclick = () => {
console.log('stage');
};
}, []);
const handleClick = (e) => {
e.stopPropagation();
console.log('阻止冒泡');
};
return (
<div id="stage" ref={containerRef}>
<Button
type="primary"
onClick={handleClick}
>
点击
</Button>
</div>
)
}
export default App
问题:点击按钮,请问会打印 stage 吗?
- demo3
js
import { useRef, useEffect } from 'react';
import './App.css';
function App() {
const containerRef = useRef();
useEffect(() => {
containerRef.current.onclick = () => {
console.log('native: stage');
};
}, []);
const handleClick = (e) => {
e.stopPropagation();
console.log('reactOnClik: stage');
};
return (
<div
id="stage"
ref={containerRef}
onClick={handleClick}
/>
)
}
export default App
问题:点击stage盒子后,控制台会打印什么?
- demo4
js
import { useRef, useEffect } from 'react';
import './App.css';
function App() {
const containerRef = useRef();
useEffect(() => {
containerRef.current.onclick = (e) => {
e.stopPropagation();
console.log('native: stage');
};
}, []);
const handleClick = () => {
console.log('reactOnClik: stage');
};
return (
<div
id="stage"
ref={containerRef}
onClick={handleClick}
/>
)
}
export default App
问题:点击stage盒子,控制台会打印什么?
答案们
-
demo1:略
-
demo2:会的
-
demo3:native: stage和reactOnClik: stage
-
demo4:native: stage
不知答案是否和各位的一致呢?
总的来说,demo2-demo4其实都是换汤不换药,一个考点换了几种方式出题而已。
所谓的合成事件,就是基于原本的事件冒泡
机制所做的,合成事件需要目标元素一直冒泡到root这,root再去事件池中梳理逻辑,这里的梳理逻辑包括了React合成事件自行维护的冒泡
和捕获
路径(这个冒泡和捕获是需要调用React事件的事件对象的stopPropagation才能阻止的),本次事件应该触发事件池中的哪些事件。
那么,也就是说,如果我们的目标元素到root冒泡的路径上进行了原生事件的阻止冒泡(如demo4),那么React事件将不会触发。明白了这一点之后,再回去看demo2-demo4,是否就更清晰一些了。
至于demo1,主要是考fragment这个元素了,他非常特殊。当fragment被插入到节点中时会变成将他的子元素们插进去,也就是说可以当作他不存在了。但是render会将react元素的事件都维护到目标元素上去,而目标元元素fragment却不存在了,所以就可以当做没进行维护,那些react事件都丢失了。
结尾
到这里,本文的所有内容就结束了。
本文主要讲了以下几点:
- 如何在原生节点插入React元素
- R从各角度验证React合成事件的执行
最后希望看到这里的jym都有所收获,也希望jym能给一个小小的赞🌹🌹🌹