
【React】合成事件原理
React中的合成事件**绝对不是**给当前元素基于addEventListener单独做的事件绑定,而是基于**事件委托**处理的!

React合成事件
React中的合成事件绝对不是给当前元素基于addEventListener
单独做的事件绑定,而是基于事件委托处理的!
-
在React17及以后版本,都是委托给
#root
这个容器「捕获和冒泡都做了委托
」; -
在React17以前的版本,都是为委托给
document
容器的「且只做了冒泡阶段
的委托」; -
对于没有实现事件传播机制的事件,才是单独做的事件绑定「例如:onMouseEnter/onMouseLeave…」
在组件渲染的时候,如果发现JSX元素属性中有
onXxx/onXxxCapture
这样的属性,不会给当前元素直接做事件绑定,只是把绑定的方法赋值给元素的相关属性
!
例如:(onClick不是dom事件绑定,onclick才是事件绑定)
outer.onClick=() => {console.log('outer 冒泡「合成」');} //这不是DOM事件绑定「这样的才是 outer.onclick」
outer.onClickCapture=() => {console.log('outer 捕获「合成」');}
inner.onClick=() => {console.log('inner 冒泡「合成」');}
inner.onClickCapture=() => {console.log('inner 捕获「合成」');}
然后对#root这个容器做了事件绑定「捕获和冒泡都做了」
组件中所渲染的内容,最后都会插入到
#root容器
中,这样点击页面中任何一个元素,最后都会把#root的点击行为触发。而在给#root绑定的方法
中,把之前给元素设置的onXxx/onXxxCapture
属性,在相应的阶段执行!!
React 18版本
React 合成事件事件传播
render() {
return <div className="outer"
onClick={() => {
console.log('outer 冒泡「合成」');
}}
onClickCapture={() => {
console.log('outer 捕获「合成」');
}}>
<div className="inner"
onClick={(ev) => {
// ev:合成事件对象
console.log('inner 冒泡「合成」', ev, ev.type);
}}
onClickCapture={() => {
console.log('inner 捕获「合成」');
}}
></div>
</div>;
}
点击inner
元素之后
在componentDidMount周期中,给元素绑定事件,可以看到此时已经“乱序”
componentDidMount() {
document.addEventListener('click', () => {
console.log('document 捕获');
}, true);
document.addEventListener('click', () => {
console.log('document 冒泡');
}, false);
document.body.addEventListener('click', () => {
console.log('body 捕获');
}, true);
document.body.addEventListener('click', () => {
console.log('body 冒泡');
}, false);
let root = document.querySelector('#root');
root.addEventListener('click', () => {
console.log('root 捕获');
}, true);
root.addEventListener('click', () => {
console.log('root 冒泡');
}, false);
}
接着我们对inner、outer元素做事件绑定
componentDidMount() {
document.addEventListener('click', () => {
console.log('document 捕获');
}, true);
document.addEventListener('click', () => {
console.log('document 冒泡');
}, false);
document.body.addEventListener('click', () => {
console.log('body 捕获');
}, true);
document.body.addEventListener('click', () => {
console.log('body 冒泡');
}, false);
let root = document.querySelector('#root');
root.addEventListener('click', () => {
console.log('root 捕获');
}, true);
root.addEventListener('click', () => {
console.log('root 冒泡');
}, false);
let outer = document.querySelector('.outer');
outer.addEventListener('click', () => {
console.log('outer 捕获「原生」');
}, true);
outer.addEventListener('click', () => {
console.log('outer 冒泡「原生」');
}, false);
let inner = document.querySelector('.inner');
inner.addEventListener('click', () => {
console.log('inner 捕获「原生」');
}, true);
inner.addEventListener('click', (ev) => {
// ev:原生事件对象
// ev.stopPropagation();
console.log('inner 冒泡「原生」');
}, false);
}
React 合成事件原理
<script>
const root = document.querySelector('#root'),
outer = document.querySelector('#outer'),
inner = document.querySelector('#inner');
// 经过视图渲染解析,outer/inner上都有onXxx/onXxxCapture这样的属性
/* <div className="outer"
onClick={() => { console.log('outer 冒泡「合成」'); }}
onClickCapture={() => { console.log('outer 捕获「合成」'); }}>
<div className="inner"
onClick={() => { console.log('inner 冒泡「合成」'); }}
onClickCapture={() => { console.log('inner 捕获「合成」'); }}
></div>
</div>; */
outer.onClick = () => { console.log('outer 冒泡「合成」'); }
outer.onClickCapture = () => { console.log('outer 捕获「合成」'); }
inner.onClick = () => { console.log('inner 冒泡「合成」'); }
inner.onClickCapture = () => { console.log('inner 捕获「合成」'); }
// 给#root做事件绑定
root.addEventListener('click', (ev) => {
let path = ev.path; // path:[事件源->....->window] 所有祖先元素
[...path].reverse().forEach(ele => {
let handle = ele.onClickCapture;
if (handle) handle();
});
}, true);
root.addEventListener('click', (ev) => {
let path = ev.path;
path.forEach(ele => {
let handle = ele.onClick;
if (handle) handle();
});
}, false);
</script>
1、在视图渲染时,遇到合成事件,并没有给元素做事件绑定,而是给元素设置相对应的属性,即合成属性
「 onXxx/onXxxCapture
」
<div className="outer"
onClick={() => { console.log('outer 冒泡「合成」'); }}
onClickCapture={() => { console.log('outer 捕获「合成」'); }}>
<div className="inner"
onClick={() => { console.log('inner 冒泡「合成」'); }}
onClickCapture={() => { console.log('inner 捕获「合成」'); }}
></div>
</div>;
2、给#root
做事件绑定,包括冒泡、捕获,#root
上绑定的方法执行,把所有规划的路径
中,有合成事件属性都执行。
其中ev
是原生事件对象
// 给#root做事件绑定
//捕获阶段 方法A
root.addEventListener('click', (ev) => {
let path = ev.composedPath(); // path:[事件源->....->window] 所有祖先元素
[...path].reverse().forEach(ele => {
let handle = ele.onClickCapture;
if (handle) handle();
});
}, true);
//冒泡阶段 方法B
root.addEventListener('click', (ev) => {
let path = ev.composedPath();
path.forEach(ele => {
let handle = ele.onClick;
if (handle) handle();
});
}, false);
方法A,打印出path
,事件源——>window
方法B,打印出path
,事件源——>window
其中handle
方法,具体的处理逻辑
const handleEv = function(ev){
//ev 原生事件对象
......
//返回 合成事件对象
}
//调用的时候
handleEv(ev)
其中在执行绑定的合成事件handle()时
- 如果不经过处理,方法中的
this
是undefined
- 如果绑定的方法是箭头函数,则找上级上下文中的
this
- 在执行这些方法之前,会把
原生对象ev
做特殊处理,返回合成事件
,传递给函数
点击inner元素的时候,按照原生的事件传播机制
捕获阶段:
window捕获、
document捕获、
html捕获
body捕获
root捕获 ->执行:方法A
- window.onClickCapture (无)
- document.onClickCapture(无)
- html.onClickCapture(无)
- body.onClickCapture(无)
- root.onClickCapture(无)
- outer.onClickCapture => outer 捕获「合成」
- inner.onClickCapture => inner 捕获「合成」
outer 捕获
innner 捕获
冒泡阶段:
inner冒泡
outer冒泡
root冒泡->执行:方法B
- inner.onClick = > inner 冒泡「合成」
- outer.onClick = > inner 冒泡「合成」
- body.onClick
- html.onClick
- document.onClick
- window.onClick
可以用图来表示:
React 合成事件中阻止事件传播的影响
<div className="inner"
onClick={(ev) => {
// ev:合成事件对象
console.log('inner 冒泡「合成」', ev, ev.type);
// ev.stopPropagation(); //合成事件对象中的“阻止事件传播”:阻止原生的事件传播 & 阻止合成事件中的事件传播
// ev.nativeEvent.stopPropagation(); //原生事件对象中的“阻止事件传播”:只能阻止原生事件的传播
// ev.nativeEvent.stopImmediatePropagation(); //原生事件对象的阻止事件传播,只不过可以阻止#root上其它绑定的方法执行
/* setTimeout(() => {
console.log(ev, ev.type); //React18中并没有事件对象池机制,所以也不存在:创建的事件对象信息清空问题!!
}, 500); */
}}
onClickCapture={() => {
console.log('inner 捕获「合成」');
}}
合成事件stopPropagation
首先 ev.stopPropagation()
,合成事件对象中的“阻止事件传播”:阻止原生的事件传播& 阻止合成事件中的事件传播。
合成事件nativeEvent.stopPropagation
ev.nativeEvent.stopPropagation
:原生事件对象中的“阻止事件传播”:只能阻止原生事件的传播。
合成事件nativeEvent.stopImmediatePropagation
ev.nativeEvent.stopImmediatePropagation :原生事件对象的阻止事件传播,只不过可以阻止#root
上其他绑定的方法执行,此时root冒泡
就没了
原生stopPropagation
inner.addEventListener('click', (ev) => {
// ev:原生事件对象
ev.stopPropagation();
console.log('inner 冒泡「原生」');
}, false);
React 16版本
合成事件原理
16版本中,合成事件的处理机制不再把事件委托给root元素,而是委托给document元素
;并且只做了冒泡阶段的委托;在委托的方法中,把onXxx/onXxxCapture合成事件属性执行。
点击inner元素之后,打印结果
1、对视图进行解析
给元素添加合成事件元素,并不是直接做事件绑定
outer.onClick = ()=>{...}
outer.onClickCapture = ()=>{...}
inner.onClick = ()=>{...}
innner.onClickCapture = ()=>{...}
2、对document
的冒泡阶段做了事件委托
document.addEventListener("click",(ev)=>{
//ev:原生事件对象
let path = ev.composedPath();//传播路径:[事件源-> window];
let syntheticEv=处理事件对象(ev)
//把捕获阶段的合成事件执行
[...path].reverse().forEach(ele=>{
let handle = ele.onClickCapture;
if(handle) handle(syntheticEv)
})
//把冒泡阶段的合成事件执行
path.forEach(ele=>{
let handle = ele.onClick;
if(handle) handle(syntheticEv)
})
})
合成事件中阻止事件传播的影响
<div className="inner"
onClick={(ev) => {
// ev:合成事件对象
console.log('inner 冒泡「合成」', ev, ev.type);
// ev.stopPropagation(); //合成事件对象中的“阻止事件传播”:阻止原生的事件传播 & 阻止合成事件中的事件传播
// ev.nativeEvent.stopPropagation(); //原生事件对象中的“阻止事件传播”:只能阻止原生事件的传播
// ev.nativeEvent.stopImmediatePropagation(); //原生事件对象的阻止事件传播,只不过可以阻止#root上其它绑定的方法执行
/* setTimeout(() => {
console.log(ev, ev.type); //React18中并没有事件对象池机制,所以也不存在:创建的事件对象信息清空问题!!
}, 500); */
}}
onClickCapture={() => {
console.log('inner 捕获「合成」');
}}
</div>
合成事件stopPropagation()
合成事件nativeEvent.stopPropagation()
合成事件nativeEvent.stopImmediatePropagation()
原生事件stopPropagation
inner.addEventListener('click', (ev) => {
// ev:原生事件对象
ev.stopPropagation();
console.log('inner 冒泡「原生」');
}, false);
事件对象池
React 16中,关于合成事件对象的处理,React内部是基于“事件对象池”,做了一个缓存机制。
React 17及以后,是去掉了这套事件对象池和缓存机制
- 当每一次事件触发的时候,如果传播到了委托的元素上「
document/#root
」,在委托的方法中,首先会对内置事件对象做统一处理,生成合成事件对象
React 16 版本中:
为了防止每一次都是重新创建出新的合成事件对象,它设置了一个事件对象池
「缓存池」
- 本次事件触发,获取到事件操作的相关信息后,从 事件对象池 中获取存储的合成事件对象,把信息赋值给相关的成员
- 等待本次操作结束,把合成事件对象中的成员信息都清空掉,再放入到「事件对象池」中
<div className="inner"
onClick={(ev) => {
// ev:合成事件对象
console.log('inner 冒泡「合成」', ev, ev.type);
setTimeout(() => {
console.log(ev, ev.type); //React18中并没有事件对象池机制,所以也不存在:创建的事件对象信息清空问题!!
}, 500);
}}
onClickCapture={() => {
console.log('inner 捕获「合成」', ev, ev.type);
}}
></div>
500ms之后,合成事件对象还在,但是里面的成员信息都被清空了
在React 18 中,并没有事件对象池机制,所以不存在创建的事件对象信息清空的问题。但是React 合成事件中 ev.persist()
可以把合成事件对象中的信息保留下来。
更多推荐
所有评论(0)