在这里插入图片描述

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()时

  • 如果不经过处理,方法中的thisundefined
  • 如果绑定的方法是箭头函数,则找上级上下文中的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() 可以把合成事件对象中的信息保留下来。

Logo

助力广东及东莞地区开发者,代码托管、在线学习与竞赛、技术交流与分享、资源共享、职业发展,成为松山湖开发者首选的工作与学习平台

更多推荐