day-075-seventy-five-20230522-原生事件进阶-React中的合成事件-React中的样式私有化
原生事件进阶
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>事件委托</title>
<link rel="stylesheet" href="src/assets/reset.min.css" />
<style>
html,
body {
height: 100%;
overflow: hidden;
}
.center {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
#root {
width: 300px;
height: 300px;
background: lightblue;
}
#outer {
width: 200px;
height: 200px;
background: lightgreen;
}
#inner {
width: 100px;
height: 100px;
background: lightcoral;
}
</style>
</head>
<body>
<div id="root" class="center">#root
<div id="outer" class="center">#outer
<div id="inner" class="center">#inner</div>
</div>
</div>
</body>
</html>
<!-- IMPORT JS -->
<script>
const html = document.documentElement;
const body = document.body;
const root = document.querySelector("#root");
const outer = document.querySelector("#outer");
const inner = document.querySelector("#inner");
// 手动做的事件绑定
window.addEventListener("click",() => {console.log(`捕获-window`);},true);
window.addEventListener("click",() => {console.log(`冒泡-window`);},false);
document.addEventListener("click",() => {console.log(`捕获-document`);},true);
document.addEventListener("click",() => {console.log(`冒泡-document`);},false);
html.addEventListener("click",() => {console.log(`捕获-html`);},true);
html.addEventListener("click",() => {console.log(`冒泡-html`);},false);
body.addEventListener("click",() => {console.log(`捕获-body`);},true);
body.addEventListener("click",() => {console.log(`冒泡-body`);},false);
root.addEventListener("click",() => {console.log(`捕获-root`);},true);
root.addEventListener("click",() => {console.log(`冒泡-root`);},false);
outer.addEventListener("click",() => {console.log(`捕获-outer`);},true);
outer.addEventListener("click",() => {console.log(`冒泡-outer`);},false);
inner.addEventListener("click",() => {console.log(`捕获-inner`);},true);
inner.addEventListener("click",() => {console.log(`冒泡-inner`);},false);
</script>
事件传播
-
示例:
-
结构示例:
<!DOCTYPE html> <html> <body> <div id="root" class="center">#root <div id="outer" class="center">#outer <div id="inner" class="center">#inner</div> </div> </div> </body> </html> <!-- IMPORT JS --> <script> const html = document.documentElement; const body = document.body; const root = document.querySelector("#root"); const outer = document.querySelector("#outer"); const inner = document.querySelector("#inner"); // 手动做的事件绑定 window.addEventListener("click",() => {console.log(`捕获-window`);},true); window.addEventListener("click",() => {console.log(`冒泡-window`);},false); document.addEventListener("click",() => {console.log(`捕获-document`);},true); document.addEventListener("click",() => {console.log(`冒泡-document`);},false); html.addEventListener("click",() => {console.log(`捕获-html`);},true); html.addEventListener("click",() => {console.log(`冒泡-html`);},false); body.addEventListener("click",() => {console.log(`捕获-body`);},true); body.addEventListener("click",() => {console.log(`冒泡-body`);},false); root.addEventListener("click",() => {console.log(`捕获-root`);},true); root.addEventListener("click",() => {console.log(`冒泡-root`);},false); outer.addEventListener("click",() => {console.log(`捕获-outer`);},true); outer.addEventListener("click",() => {console.log(`冒泡-outer`);},false); inner.addEventListener("click",() => {console.log(`捕获-inner`);},true); inner.addEventListener("click",() => {console.log(`冒泡-inner`);},false); </script> -
执行示例:
-
点击#inner后:
<!DOCTYPE html> <html> <body> <div id="root" class="center">#root <div id="outer" class="center">#outer <div id="inner" class="center">#inner</div> </div> </div> </body> </html> <!-- IMPORT JS --> <script> const html = document.documentElement; const body = document.body; const root = document.querySelector("#root"); const outer = document.querySelector("#outer"); const inner = document.querySelector("#inner"); // 执行顺序: window.addEventListener("click",() => {console.log(`捕获-window`);},true); document.addEventListener("click",() => {console.log(`捕获-document`);},true); html.addEventListener("click",() => {console.log(`捕获-html`);},true); body.addEventListener("click",() => {console.log(`捕获-body`);},true); root.addEventListener("click",() => {console.log(`捕获-root`);},true); outer.addEventListener("click",() => {console.log(`捕获-outer`);},true); inner.addEventListener("click",() => {console.log(`捕获-inner`);},true); inner.addEventListener("click",() => {console.log(`冒泡-inner`);},false); outer.addEventListener("click",() => {console.log(`冒泡-outer`);},false); root.addEventListener("click",() => {console.log(`冒泡-root`);},false); body.addEventListener("click",() => {console.log(`冒泡-body`);},false); html.addEventListener("click",() => {console.log(`冒泡-html`);},false); document.addEventListener("click",() => {console.log(`冒泡-document`);},false); window.addEventListener("click",() => {console.log(`冒泡-window`);},false); </script> -
点击#outer后:
<!DOCTYPE html> <html> <body> <div id="root" class="center">#root <div id="outer" class="center">#outer <div id="inner" class="center">#inner</div> </div> </div> </body> </html> <!-- IMPORT JS --> <script> const html = document.documentElement; const body = document.body; const root = document.querySelector("#root"); const outer = document.querySelector("#outer"); const inner = document.querySelector("#inner"); // 执行顺序: window.addEventListener("click",() => {console.log(`捕获-window`);},true); document.addEventListener("click",() => {console.log(`捕获-document`);},true); html.addEventListener("click",() => {console.log(`捕获-html`);},true); body.addEventListener("click",() => {console.log(`捕获-body`);},true); root.addEventListener("click",() => {console.log(`捕获-root`);},true); outer.addEventListener("click",() => {console.log(`捕获-outer`);},true); outer.addEventListener("click",() => {console.log(`冒泡-outer`);},false); root.addEventListener("click",() => {console.log(`冒泡-root`);},false); body.addEventListener("click",() => {console.log(`冒泡-body`);},false); html.addEventListener("click",() => {console.log(`冒泡-html`);},false); document.addEventListener("click",() => {console.log(`冒泡-document`);},false); window.addEventListener("click",() => {console.log(`冒泡-window`);},false); </script> -
点击#root后:
<!DOCTYPE html> <html> <body> <div id="root" class="center">#root <div id="outer" class="center">#outer <div id="inner" class="center">#inner</div> </div> </div> </body> </html> <!-- IMPORT JS --> <script> const html = document.documentElement; const body = document.body; const root = document.querySelector("#root"); const outer = document.querySelector("#outer"); const inner = document.querySelector("#inner"); // 执行顺序: window.addEventListener("click",() => {console.log(`捕获-window`);},true); document.addEventListener("click",() => {console.log(`捕获-document`);},true); html.addEventListener("click",() => {console.log(`捕获-html`);},true); body.addEventListener("click",() => {console.log(`捕获-body`);},true); root.addEventListener("click",() => {console.log(`捕获-root`);},true); root.addEventListener("click",() => {console.log(`冒泡-root`);},false); body.addEventListener("click",() => {console.log(`冒泡-body`);},false); html.addEventListener("click",() => {console.log(`冒泡-html`);},false); document.addEventListener("click",() => {console.log(`冒泡-document`);},false); window.addEventListener("click",() => {console.log(`冒泡-window`);},false); </script>
-
-
-
事件传播的特点:
-
事件传播中的捕获阶段从window到document到html到body,直到事件源的祖先元素。
-
事件源的捕获与传播就是目标阶段。
-
事件传播中的捕获阶段从事件源的祖先元素到body,再到html到document到window。
-
一个DOM元素在事件传播的过程中的每一步都可以绑定多个事件。
- 都会执行。
<script> const inner = document.querySelector("#inner"); inner.addEventListener("click",() => {console.log(`冒泡-inner-1`);},false); inner.addEventListener("click",() => {console.log(`冒泡-inner-2`);},false); inner.addEventListener("click",() => {console.log(`冒泡-inner`);},false); </script><script> const html = document.documentElement; const body = document.body; const root = document.querySelector("#root"); const outer = document.querySelector("#outer"); const inner = document.querySelector("#inner"); // 执行顺序: window.addEventListener("click",() => {console.log(`捕获-window`);},true); document.addEventListener("click",() => {console.log(`捕获-document`);},true); html.addEventListener("click",() => {console.log(`捕获-html`);},true); body.addEventListener("click",() => {console.log(`捕获-body`);},true); root.addEventListener("click",() => {console.log(`捕获-root`);},true); outer.addEventListener("click",() => {console.log(`捕获-outer`);},true); inner.addEventListener("click",() => {console.log(`捕获-inner`);},true); inner.addEventListener("click",() => {console.log(`冒泡-inner-1`);},false); inner.addEventListener("click",() => {console.log(`冒泡-inner-2`);},false); outer.addEventListener("click",() => {console.log(`冒泡-outer`);},false); root.addEventListener("click",() => {console.log(`冒泡-root`);},false); body.addEventListener("click",() => {console.log(`冒泡-body`);},false); html.addEventListener("click",() => {console.log(`冒泡-html`);},false); document.addEventListener("click",() => {console.log(`冒泡-document`);},false); window.addEventListener("click",() => {console.log(`冒泡-window`);},false); </script> -
event.stopPropagation():只会阻止下一步的处理,但是对于当前这一步的当前所在DOM元素绑定的多个方法依然都会被触发执行。- 阻止事件的传播,包括所有阶段:如冒泡阶段与捕获阶段都会被阻止。
<script> const html = document.documentElement; const body = document.body; const root = document.querySelector("#root"); const outer = document.querySelector("#outer"); const inner = document.querySelector("#inner"); // 执行顺序: window.addEventListener("click",() => {console.log(`捕获-window`);},true); document.addEventListener("click",() => {console.log(`捕获-document`);},true); html.addEventListener("click",() => {console.log(`捕获-html`);},true); body.addEventListener("click",() => {console.log(`捕获-body`);},true); root.addEventListener("click",() => {console.log(`捕获-root`);},true); outer.addEventListener("click",() => {console.log(`捕获-outer`);},true); inner.addEventListener("click",() => {console.log(`捕获-inner`);},true); inner.addEventListener("click",(event) => {event.stopPropagation();console.log(`冒泡-inner-1`);},false); inner.addEventListener("click",() => {console.log(`冒泡-inner-2`);},false); outer.addEventListener("click",() => {console.log(`冒泡-outer`);},false); root.addEventListener("click",() => {console.log(`冒泡-root`);},false); body.addEventListener("click",() => {console.log(`冒泡-body`);},false); html.addEventListener("click",() => {console.log(`冒泡-html`);},false); document.addEventListener("click",() => {console.log(`冒泡-document`);},false); window.addEventListener("click",() => {console.log(`冒泡-window`);},false); </script>冒泡-inner-1与冒泡-inner-2都会打印,但冒泡-outer及其后方都不会再执行了!
-
event.stopImmediatePropagation():不仅会阻止下一步的处理,对于当前这一步的当前所在DOM元素上绑定的其它方法,如果还没有执行,也不会再执行了!- 阻止事件的传播,包括所有阶段:如冒泡阶段与捕获阶段都会被阻止。
<script> const html = document.documentElement; const body = document.body; const root = document.querySelector("#root"); const outer = document.querySelector("#outer"); const inner = document.querySelector("#inner"); // 执行顺序: window.addEventListener("click",() => {console.log(`捕获-window`);},true); document.addEventListener("click",() => {console.log(`捕获-document`);},true); html.addEventListener("click",() => {console.log(`捕获-html`);},true); body.addEventListener("click",() => {console.log(`捕获-body`);},true); root.addEventListener("click",() => {console.log(`捕获-root`);},true); outer.addEventListener("click",() => {console.log(`捕获-outer`);},true); inner.addEventListener("click",() => {console.log(`捕获-inner`);},true); inner.addEventListener("click",(event) => {event.stopImmediatePropagation();console.log(`冒泡-inner-1`);},false); inner.addEventListener("click",() => {console.log(`冒泡-inner-2`);},false); outer.addEventListener("click",() => {console.log(`冒泡-outer`);},false); root.addEventListener("click",() => {console.log(`冒泡-root`);},false); body.addEventListener("click",() => {console.log(`冒泡-body`);},false); html.addEventListener("click",() => {console.log(`冒泡-html`);},false); document.addEventListener("click",() => {console.log(`冒泡-document`);},false); window.addEventListener("click",() => {console.log(`冒泡-window`);},false); </script>冒泡-inner-1会打印,但冒泡-inner-2与冒泡-outer及其后方都不会再执行了!
-
事件委托
-
事件例子:
<!DOCTYPE html> <html> <body> <div id="root" class="center">#root <div id="outer" class="center">#outer <div id="inner" class="center">#inner</div> </div> </div> </body> </html> <script> // 点击body中的任意元素,都会触发body的点击事件。 document.body.addEventListener("click", function (ev) { let target = ev.target; let targetTag = target.tagName; let targetId = target.is; if(targetTag==='BODY'){ console.log(`body`); return } if(targetId==='root'){ console.log(`root`); return } if(targetId==='outer'){ console.log(`outer`); return } console.log(`点击的是非预期元素`); },false); // 其它的处理逻辑比较复杂,可以单独绑定。 document.querySelector(`#inner`).addEventListener('click',function(ev){ console.log(`inner,这一行执行的代码比较多。`); ev.stopPropagation() },false) </script> -
事件委托,也被叫做事件代理。
-
事件委托就是利用
事件的传播机制来实现的一种项目解决方案。 -
例如:一个容器中有很多后代元素,其中大部分后代元素,在点击的时候,都会处理一些事情。有些元素被点击时做相同的事情,有些元素点击时做不同的事情。
-
传统方案:想操作那些元素,就把这些元素全部都获取到,然后逐一做事件绑定。
<!DOCTYPE html> <html> <body> <div id="root" class="center">#root <div id="outer" class="center">#outer <div id="inner" class="center">#inner</div> </div> </div> </body> </html> <script> document.querySelector(`body`).addEventListener("click", function (ev) { console.log(`body`); },false); document.querySelector(`#root`).addEventListener("click", function (ev) { console.log(`root`); if(targetId==='outer'){ console.log(`outer`); return } console.log(`点击的是非预期元素`); },false); document.querySelector(`#outer`).addEventListener("click", function (ev) { console.log(`outer`); },false); document.querySelector(`#inner`).addEventListener('click',function(ev){ console.log(`inner,这一行执行的代码比较多。`); ev.stopPropagation() },false) </script>- 这种方案不仅操作起来复杂,并且会开辟很多堆内存,性能也不是很好。
-
新方案:利用事件的传播机制,不逐一获取元素和单独绑定事件了,而是只给外层容器做一个事件绑定,这样不管点击其后代中的那一个元素,当前容器的点击事件也会触发,把容器元素上绑定的方法执行;在容器元素所绑定的方法中,我们只需要判断事件源是谁,从而处理不同的事件即可!===>事件委托。
-
事件对象就是事件函数的第一个形参,一般用e或ev或event来接收。
-
事件源通过事件对象的target属性来获取。
-
判断事件源不一定要判断到具体的某个元素,而是该元素有一些共同特点,如元素标签为某一类型或者该元素为某个类,或者该元素有某个特定属性。
- 事件对象.target 事件源。
- 事件对象.target.targetTag 事件源的大写形式的标签类型。
- 事件对象.target.class 事件源的字符串形式类名。
- 事件对象.target.classList 事件源的数组形式类名。
- 事件对象.target.id 事件源的id属性。
-
如果其中某个元素点击的时候,要干的事情比较复杂,不想和事件委托的代码混在一起,我们可以单独为这个元素做事件绑定。
<!DOCTYPE html> <html> <body> <div id="root" class="center">#root <div id="outer" class="center">#outer <div id="inner" class="center">#inner</div> </div> </div> </body> </html> <script> // 点击body中的任意元素,都会触发body的点击事件。 document.body.addEventListener("click", function (ev) { let target = ev.target; let targetTag = target.tagName; let targetId = target.id; if(targetTag==='BODY'){ console.log(`body`); return } if(targetId==='root'){ console.log(`root`); return } if(targetId==='outer'){ console.log(`outer`); return } console.log(`点击的是非预期元素`); },false); // 其它的处理逻辑比较复杂,可以单独绑定。 document.querySelector(`#inner`).addEventListener('click',function(ev){ console.log(`inner,这一行执行的代码比较多。`); ev.stopPropagation() },false) </script>- 这种新方案的性能比传统方案提高40%以上!
-
-
-
例如:我们容器中的元素不是写死的,而是动态绑定(动态创建)的,而且是会持续动态创建。我们需要在点击每个元素的时候,做一些事情!
-
传统方案:每一次动态创建完容器中的
新创建元素,都需要获取新创建元素,给这些新创建元素单独做事件绑定! -
事件委托:只需要给容器的点击事件绑定方法,无论其内部元素是写死的,还是动态绑定的,只要点击了,都说明元素已经存在了,基于事件传播机制,我们只需要在外层容器中判断事件源,做不同的事情即可!
-
判断事件源,可以通过事件源的类名或标签名。就执行一个特定的方法或操作。
-
这也就说明事件委托可以给动态绑定的元素做事件绑定!
-
-
例子:
-
原DOM结构:
<!DOCTYPE html> <html> <body> <div id="root" class="center">#root <div id="outer" class="center">#outer </div> </div> </body> </html> -
后续变成的DOM结构:在#outer添加了#inner。
<!DOCTYPE html> <html> <body> <div id="root" class="center">#root <div id="outer" class="center">#outer <div id="inner" class="center">#inner</div> </div> </div> </body> </html>
-
-
-
真实项目中,还有很多需求,必须基于事件委托来做。
-
比如说:
- 点击容器中非某个元素,该元素就隐藏!
-
所以:以后但凡再遇到事件绑定的需求,要首先想到
是否有必要基于事件委托处理,如果确定有必要,则直接基于事件委托来处理!
-
-
React中的合成事件
import React from "react";
import "./DemoEvent.less";
export default class Demo extends React.Component {
render() {
return (
<div
className="outer"
onClick={() => {
console.log("outer 冒泡「合成」");
}}
onClickCapture={() => {
console.log("outer 捕获「合成」");
}}
>
<div
className="inner"
onClick={() => {
console.log("inner 冒泡「合成」");
}}
onClickCapture={() => {
console.log("inner 捕获「合成」");
}}
></div>
</div>
);
}
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", () => console.log("inner 冒泡"), false);
}
}
-
React中的
合成事件SyntheticEvent-
什么是合成事件?
-
合成事件是围绕
浏览器原生事件,充当跨浏览器包装器的对象;它们将不同浏览器的行为合并为一个API,这样做是为了确保事件在不同浏览器中显示一致的属性! -
例如:
<div onClick={()=>{}} onClickCapture={()=>{}} ></div>- 这种在jsx元素上,基于onXxx绑定的事件,都是合成事件,就是React内部对这些
事件操作,进行过特殊的处理。
- 这种在jsx元素上,基于onXxx绑定的事件,都是合成事件,就是React内部对这些
-
-
-
React18中的合成事件绑定原理-事件委托:
const html = document.documentElement; const body = document.body; const root = document.querySelector("#root"); const outer = document.querySelector("#outer"); const inner = document.querySelector("#inner"); // React18合成事件原理。 const handlerSyntheticEv = function handlerSyntheticEv(ev){ // // ... return ev } root.addEventListener("click",function (ev) { //获取传播路径-默认是从内到外。 // console.log(`ev.path-->`, ev.path); // console.log(`ev.composedPath()-->`, ev.composedPath()); let path = ev.composedPath(); let syntheticBaseEvent = handlerSyntheticEv(ev) path.reverse().forEach((elem) => { if (!elem.hasOwnProperty("onClickCaptrue")) { return; } elem.onClickCaptrue(syntheticBaseEvent); }); },true); root.addEventListener("click",function (ev) { //获取传播路径-默认是从内到外。 let path = ev.composedPath(); let syntheticBaseEvent = handlerSyntheticEv(ev) path.forEach((elem) => { if (!elem.hasOwnProperty("onClick")) { return; } elem.onClick(syntheticBaseEvent); }); },false); // jsx渲染: outer.onClick=() => {console.log("outer 冒泡「合成」");} outer.onClickCapture=() => {console.log("outer 捕获「合成」");} inner.onClick=() => {console.log("inner 冒泡「合成」");} inner.onClickCapture=() => {console.log("inner 捕获「合成」");}-
DemoEvent.less.center { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); } .outer { .center; width: 200px; height: 200px; background: lightblue; .inner { .center; width: 100px; height: 100px; background: lightcoral; } } -
DemoEvent.jsximport React from "react" import './DemoEvent.less' export default class Demo extends React.Component { render() { return <div className="outer" onClick={(ev) => { console.log('outer 冒泡「合成」') }} onClickCapture={() => { console.log('outer 捕获「合成」') }}> <div className="inner" onClick={(ev) => { // ev 合成事件对象 // ev.nativeEvent 原生事件对象 // 合成事件对象的阻止事件传播:阻止合成事件的传播以及原生事件的传播 // ev.stopPropagation() // 原生事件对象的阻止事件传播:只能阻止原生的事件传播 // ev.nativeEvent.stopImmediatePropagation() console.log('inner 冒泡「合成」', ev) }} onClickCapture={() => { console.log('inner 捕获「合成」') }}> </div> </div> } 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.stopImmediatePropagation() console.log('inner 冒泡') }, false) } }
-
在jsx视图编译的时候,当遇到某个元素上出现onXxx合成事件绑定,其实并没有给元素做事件绑定,只是给这个元素设置了一个onXxx的私有属性,属性值就是我们绑定的方法。
-
jsx代码:
<div className="inner" onClick={() => {console.log("inner 冒泡「合成」"); }} onClickCapture={() => {console.log("inner 捕获「合成」");}} ></div> -
渲染的结果:
inner.onClick=函数 inner.onClickCapture=函数inner.onClick={() => {console.log("inner 冒泡「合成」"); }} inner.onClickCapture={() => {console.log("inner 捕获「合成」");}} -
没有做任何的浏览器事件绑定,仅仅是给inner设置了两个私有属性!
-
-
在React内部,会给#root容器的点击行为,做事件绑定,捕获和冒泡阶段都处理了!React所有的视图编译完毕后,都会插入到#root容器中,这样点击任何一个元素,都会把#root的点击行为触发!
-
在React内部,默认对大部分浏览器标准事件都进行了这样的委托处理。
- 这些做了委托处理的浏览器标准事件的前提:它们是支持事件传播机制的标准事件。
-
在React内部,对于不支持事件传播机制的事件,在jsx渲染的时候,就单独给元素做了相关的事件绑定!
-
jsx代码:
root.addEventListener("Xxx", function(){ //获取传播路径。 //按照从外到内的顺序,把元素的onXxxCapture合成事件的私有属性,依次触发执行。 }, true); root.addEventListener("Xxx", function(){ //获取传播路径。 //按照从内到外的顺序,把元素的onXxx合成事件的私有属性,依次触发执行。 }, false); -
<!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>事件委托</title> <link rel="stylesheet" href="src/assets/reset.min.css" /> <style> html, body { height: 100%; overflow: hidden; } .center { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); } #root { width: 300px; height: 300px; background: lightblue; } #outer { width: 200px; height: 200px; background: lightgreen; } #inner { width: 100px; height: 100px; background: lightcoral; } </style> </head> <body> <div id="root" class="center"> <div id="outer" class="center"> <div id="inner" class="center"></div> </div> </div> <!-- IMPORT JS --> <script> const html = document.documentElement, body = document.body, root = document.querySelector('#root'), outer = document.querySelector('#outer'), inner = document.querySelector('#inner') /* React18合成事件原理 */ const handlerSyntheticEv = function handlerSyntheticEv(ev) { // 把原生的事件对象进行处理,让其变为合成事件对象「目的:让各个浏览器统一」 // ... return ev } root.addEventListener('click', function (ev) { // 获取传播路径「从内到外」 let path = ev.composedPath() // 获取合成事件对象 let syntheticBaseEvent = handlerSyntheticEv(ev) path.reverse().forEach(elem => { if (!elem.hasOwnProperty('onClickCapture')) return elem.onClickCapture(syntheticBaseEvent) }) }, true) root.addEventListener('click', function (ev) { let path = ev.composedPath() let syntheticBaseEvent = handlerSyntheticEv(ev) path.forEach(elem => { if (!elem.hasOwnProperty('onClick')) return elem.onClick(syntheticBaseEvent) }) }, false) /* JSX渲染 */ outer.onClickCapture = function () { console.log('outer 捕获「合成」') } outer.onClick = function () { console.log('outer 冒泡「合成」') } inner.onClickCapture = function () { console.log('inner 捕获「合成」') } inner.onClick = function () { console.log('inner 冒泡「合成」') } /* 手动做的事件绑定 */ window.addEventListener('click', function () { console.log('window 捕获') }, true) window.addEventListener('click', function () { console.log('window 冒泡') }, false) document.addEventListener('click', function () { console.log('document 捕获') }, true) document.addEventListener('click', function () { console.log('document 冒泡') }, false) html.addEventListener('click', function () { console.log('html 捕获') }, true) html.addEventListener('click', function () { console.log('html 冒泡') }, false) body.addEventListener('click', function () { console.log('body 捕获') }, true) body.addEventListener('click', function () { console.log('body 冒泡') }, false) root.addEventListener('click', function () { console.log('root 捕获') }, true) root.addEventListener('click', function () { console.log('root 冒泡') }, false) outer.addEventListener('click', function () { console.log('outer 捕获') }, true) outer.addEventListener('click', function () { console.log('outer 冒泡') }, false) inner.addEventListener('click', function () { console.log('inner 捕获') }, true) inner.addEventListener('click', function () { console.log('inner 冒泡') }, false) </script> </body> </html> -
-
React中的合成事件对象:
-
合成事件对象的阻止事件传播函数:阻止合成事件的传播以及原生事件的传播。
- 合成事件对象没有stopImmediatePropagation()。
-
原生事件对象的阻止事件传播:只能阻止原生的事件传播。
-
-
React16中,其合成事件的底层处理机制和React18有很大的区别:
-
首先绑定的合成事件,对于有传播机制的标准事件来讲,也不是做的事件绑定,而是变为元素的
onXxx/onXxxCapture私有属性! -
只不过React16不是给#root做事件委托,而是委托给了document,并且只做了
冒泡阶段的委托。-
即合成事件的捕获与冒泡都是在事件冒泡到document时才执行的。
-
获取传播路径-默认是从内到外。
-
处理合成事件对象;
-
这里和React16中的合成事件对象与React中的合成事件对象不一样。
- React16中的合成对象是做了get与set劫持。
-
-
把传播路径按照倒序,依次执行元素.onXxxCapture()方法;
-
把传播路径按照正序,依次执行元素.onXxx()方法;
-
const html = document.documentElement; const body = document.body; const root = document.querySelector("#root"); const outer = document.querySelector("#outer"); const inner = document.querySelector("#inner"); // React16合成事件原理。 const handlerSyntheticEv = function handlerSyntheticEv(ev){ // // ... return ev } document.addEventListener("click",function (ev) { //获取传播路径-默认是从内到外。 //处理合成事件对象; //把传播路径按照倒序,依次执行元素.onXxxCapture()方法; //把传播路径按照正序,依次执行元素.onXxx()方法; // console.log(`ev.composedPath()-->`, ev.composedPath()); },false); // jsx渲染: outer.onClick=() => {console.log("outer 冒泡「合成」");} outer.onClickCapture=() => {console.log("outer 捕获「合成」");} inner.onClick=() => {console.log("inner 冒泡「合成」");} inner.onClickCapture=() => {console.log("inner 捕获「合成」");}-
DemoEvent.less.center { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); } .outer { .center; width: 200px; height: 200px; background: lightblue; .inner { .center; width: 100px; height: 100px; background: lightcoral; } } -
DemoEvent.jsximport React from "react" import './DemoEvent.less' export default class Demo extends React.Component { render() { return <div className="outer" onClick={(ev) => { console.log('outer 冒泡「合成」') }} onClickCapture={() => { console.log('outer 捕获「合成」') }}> <div className="inner" onClick={(ev) => { // ev 合成事件对象 // ev.nativeEvent 原生事件对象 // 合成事件对象的阻止事件传播:阻止合成事件的传播以及原生事件的传播 // ev.stopPropagation() // 原生事件对象的阻止事件传播:只能阻止原生的事件传播 // ev.nativeEvent.stopImmediatePropagation() console.log('inner 冒泡「合成」', ev) }} onClickCapture={() => { console.log('inner 捕获「合成」') }}> </div> </div> } 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.stopImmediatePropagation() console.log('inner 冒泡') }, false) } }
-
-
React16中除了合成事件底层处理机制和React18不同,对于合成事件对象的处理,和React18也是不一样的!
-
在React16版本中,存在事件对象池,用的合成事件对象只有一个,每一次事件触发,都是把合成事件对象的值,改为最新的值!但是一旦用完,则把这些值又释放掉了!
- React16中的合成对象是做了get与set劫持。
-
在React18版本中,取消了事件对象池机制,每一次事件触发都是创建新的合成事件对象,之前创建的合成事件对象,在没有用处的情况下,会被销毁掉!
-
React中的样式私有化
-
在React项目中,我们在
.css/.less/.sass等文件中编写的样式,默认都是全局样式。-
这样在组件合并渲染的时候,很可能导致
各组件之间编写的样式发生冲突。-
因为不同组件之间的类名可能由不同的人来写,类名就可能会起一样了。
-
所以我们期望:在
各组件中写的样式,可以只对当前组件起作用。- 这也就是组件样式的私有化处理方案。
-
-
例子:
-
Demo1.jsximport React from "react"; import './Demo1.less' export default class Demo1 extends React.Component { render() { return ( <div className="demo"> <h2 className="title"> 组件1 <span>span</span> </h2> </div> ); } } -
Demo1.less.demo { background: skyblue; .title { font-size: 18px; color: red; } } -
Demo2.jsximport React from "react" import './Demo2.less' export default class Demo2 extends React.Component { render() { return <div className="demo"> <h2 className="title">组件2</h2> </div> } } -
Demo2.less.demo { background: skyblue; .title { font-size: 18px; color: red; } } -
Demo1.jsx与Demo2.jsx中的样式用的都是全局的样式。具体那个生效,就看那个的权重及所处的前后位置了。
-
-
-
样式私有化处理方案:
-
样式私有化处理方案一:人为有意识地、有规范地规避冲突问题。
-
这种是主流的蛮常用方案。
-
使用
less、sass、stylus等预编译语言,主要是是利用其提供的嵌套功能。 -
制定组件命名规范,尤其是
组件根节点样式的命名规范,保证所有组件在复用多次的情况下当前组件根节点的样式类名是唯一的。-
例如:
-
使用类似于
组件所在的文件夹路径-组件名-box的命名规范。/src/components/TabBar.jsx根节点的样式类名为common-tabbar-box;/views/home/TableList.jsx根节点的样式类名为home-table-list-box;/views/device/Login.jsx根节点的样式类名为device-table-list-box;/views/Login.jsx根节点的样式类名为login-box;
//假设目录为: |-components |-TabBar.jsx -> common-tabbar-box |-... |-views |-home |-TableList.jsx -> home-table-list-box |-... |-device |-TableList.jsx -> device-table-list-box |-... |-Login.jsx -> login-box |-...
-
-
-
而组件内部所有元素的样式,都要保证在该组件的样式类名下编写。
.device-table-list-box{ .title{ ... .num{ ... } } span{ ... } }
-
存在的弊端:
-
如果选择器有过长的前缀,会影响渲染的性能。
//从右到左渲染 .box a{}//先找到所有的a标签,再筛选出哪些在 .box 下。 a{}//找到所有的a标签。所以这个选择器的性能好。- 如果是多级,那么就要查找多层,会导致筛选过程浪费大量性能。
- 因为这个是类似于DOM树的结构来写的css树,所以导致从其末尾一步一步向主干查询,会有很多的筛选流程及无效筛选。比如筛选着筛选着,发现没一个DOM元素符合该选择器。
-
在样式表中编写的样式都是静态样式,无法基于js等业务逻辑控制其动态化。
- 这个是所有css与js与html分离都有的问题,不用太过在意。
-
-
例子:
-
Demo1.jsximport React from "react"; import './Demo1.less' export default class Demo1 extends React.Component { render() { return ( <div className="views-demo1-box"> <h2 className="title"> 组件1 <span>span</span> </h2> </div> ); } } -
Demo1.less.views-demo1-box { background: skyblue; .title { font-size: 18px; color: red; } } -
Demo2.jsximport React from "react" import './Demo2.less' export default class Demo2 extends React.Component { render() { return <div className="views-demo2-box"> <h2 className="title">组件2</h2> </div> } } -
Demo2.less.views-demo2-box { background: skyblue; .title { font-size: 18px; color: red; } }
-
-
-
样式私有化处理方案二:
CSS Modules。-
这是一种基于技术手段,保证编写样式类名的
唯一性。
-
样式表的命名:
xxx.module.css与xxx.module.less。如果不使用less后缀与scss后缀等,可以不再使用less/sass等预编译语言了。 -
在webpack编译的时候,会把
.module.css中所有的样式类名,都进行编译处理,按照规则变为唯一的新式类。-
例如:在
Demo1.module.css文件中:.demo {...} .title {...} .title span {...} -
经过编译后的样式:
.Demo1_demo__bLprq {...} .Demo1_title__EpCHF {...} .Demo1_title__EpCHF span {...}
-
-
在组件中使用的时候:
import sty from './Demo1.module.css' /* sty = { demo: "Demo1_demo__bLprq", title: "Demo1_title__EpCHF" } */ <div className={sty.demo}> <h2 className={sty.title}></h2> </div>
-
-
样式私有化处理方案三:基于技术手段,把css写在js中。这样的处理思想,我们称之为
css in js;-
依赖第三方插件来进行处理:
React-JSS。styled-components,常用的。- …。
-
-
所有样式都写成内联样式行内样式。
- 这种不太常见,难维护,比较少见。
- 不用偶尔小范围也有使用,比如做动态样式时。
-
React-JSS的使用
styled-components的使用
-
安装
vscode插件:vscode-styled-components。 -
使用步骤:
-
导入插件,基于插件编写样式「相当于创建一个样式组件,其内部包含的是需要的样式」
import styled from "styled-components" const colorRed = 'red' const DemoBox = styled.div` ... .title{ ..., color: ${colorRed}; } ` -
视图中,直接基于创建的组件来渲染样式即可
export default class Demo1 extends React.Component { render() { return <DemoBox> <h2 className="title">...</h2> </DemoBox> } }
-
-
编译的原理:在渲染DemoBox组件的时候,会被渲染为一个div标签,并且给其设置一个唯一的样式类名。
- 而我们写的样式,最后也会编译为正常的CSS样式,不过都是在 “sc-bcXHqe bscqLV” 唯一的样式类下编写的!
-
基于css-in-js
- 一方面基于技术手段保证了样式的私有化
- 另一方面,因为CSS样式是写在JS中的,我们就可以根据业务逻辑,动态控制其渲染的样式
-
实战开发中的细节问题:
-
公共样式的设置。
-
公共样式优先级。
- 如果冲突了,想让谁生效,就提高谁的优先级。
- 在多个“样式组件”嵌套的情况下,如果对相同元素都设置了样式,最后以谁的样式为主,就看优先级,如果想指定以谁为主,也是提高其优先级(!important)
-
动态样式:
-
css性能问题
//从右到左渲染
.box a{}//先找到所有的a标签,再筛选出哪些在 .box 下。
a{}//找到所有的a标签。所以这个选择器的性能好。