共计 8025 个字符,预计需要花费 21 分钟才能阅读完成。
React 是一个 Facebook 开源的,用于构建用户界面的 JavaScript 库。
React 目标在于解决:构建随着工夫数据一直变动的大规模应用程序。
其中 React 合成事件是较为重要的知识点,浏览完本文,你将播种:
- 合成事件的概念和作用;
- 合成事件与原生事件的 3 个区别;
- 合成事件与原生事件的执行程序;
- 合成事件的事件池;
- 合成事件 4 个常见问题。
接下来和我一起开始学习吧~
一、概念介绍
React 合成事件(SyntheticEvent)是 React 模仿原生 DOM 事件所有能力的一个事件对象 ,即浏览器原生事件的跨浏览器包装器。它依据 W3C 标准 来定义合成事件,兼容所有浏览器,领有与浏览器原生事件雷同的接口。
看个简略示例:
const button = <button onClick={handleClick}>Leo 按钮 </button>
在 React 中,所有事件都是合成的,不是原生 DOM 事件,但能够通过 e.nativeEvent
属性获取 DOM 事件。
const handleClick = (e) => console.log(e.nativeEvent);;
const button = <button onClick={handleClick}>Leo 按钮 </button>
学习一个新常识的时候,肯定要晓得为什么会呈现这个技术。
那么 React 为什么应用合成事件?其次要有三个目标:
- 进行浏览器兼容,实现更好的跨平台
React 采纳的是顶层事件代理机制,可能保障冒泡一致性,能够跨浏览器执行。React 提供的合成事件用来抹平不同浏览器事件对象之间的差别,将不同平台事件模仿合成事件。
- 防止垃圾回收
事件对象可能会被频繁创立和回收,因而 React 引入 事件池 ,在事件池中获取或开释事件对象。 即 React 事件对象不会被开释掉,而是寄存进一个数组中,当事件触发,就从这个数组中弹出,防止频繁地去创立和销毁(垃圾回收)。
- 不便事件对立治理和事务机制
本文不介绍源码啦,对具体实现的源码有趣味的敌人能够查阅:《React SyntheticEvent》。
二、原生事件回顾
在开始介绍 React 合成事件之前,咱们先简略回顾 JavaScript 原生事件中几个重要知识点:
1. 事件捕捉
当某个元素触发某个事件(如 onclick
),顶层对象 document
就会收回一个事件流,随着 DOM 树的节点向指标元素节点流去,直到达到事件真正产生的指标元素。在这个过程中,事件相应的监听函数是不会被触发的。
2. 事件指标
当达到指标元素之后,执行指标元素该事件相应的处理函数。如果没有绑定监听函数,那就不执行。
3. 事件冒泡
从指标元素开始,往顶层元素流传。途中如果有节点绑定了相应的事件处理函数,这些函数都会被触发一次。如果想阻止事件起泡,能够应用 e.stopPropagation()
或者 e.cancelBubble=true
(IE)来阻止事件的冒泡流传。
4. 事件委托 / 事件代理
简略了解就是 将一个响应事件委托到另一个元素 。
当子节点被点击时,click
事件向上冒泡,父节点捕捉到事件后,咱们判断是否为所需的节点,而后进行解决。其长处在于 缩小内存耗费和动静绑定事件。
二、合成事件与原生事件区别
React 事件与原生事件很类似,但不完全相同。这里列举几个常见区别:
1. 事件名称命名形式不同
原生事件命名为纯小写(onclick, onblur),而 React 事件命名采纳 小驼峰式(camelCase),如 onClick
等:
// 原生事件绑定形式
<button onclick="handleClick()">Leo 按钮命名 </button>
// React 合成事件绑定形式
const button = <button onClick={handleClick}>Leo 按钮命名 </button>
2. 事件处理函数写法不同
原生事件中事件处理函数为字符串,在 React JSX 语法中,传入一个 函数 作为事件处理函数。
// 原生事件 事件处理函数写法
<button onclick="handleClick()">Leo 按钮命名 </button>
// React 合成事件 事件处理函数写法
const button = <button onClick={handleClick}>Leo 按钮命名 </button>
3. 阻止默认行为形式不同
在原生事件中,能够通过返回 false
形式来阻止默认行为,然而在 React 中,须要显式应用 preventDefault()
办法来阻止。
这里以阻止 <a>
标签默认关上新页面为例,介绍两种事件区别:
// 原生事件阻止默认行为形式
<a href="https://www.pingan8787.com"
onclick="console.log('Leo 阻止原生事件~'); return false"
>
Leo 阻止原生事件
</a>
// React 事件阻止默认行为形式
const handleClick = e => {e.preventDefault();
console.log('Leo 阻止原生事件~');
}
const clickElement = <a href="https://www.pingan8787.com" onClick={handleClick}>
Leo 阻止原生事件
</a>
4. 小结
小结后面几点区别:
原生事件 | React 事件 | |
---|---|---|
事件名称命名形式 | 名称全副小写 <br/>(onclick, onblur) | 名称采纳小驼峰 <br/>(onClick, onBlur) |
事件处理函数语法 | 字符串 | 函数 |
阻止默认行为形式 | 事件返回 false |
应用 e.preventDefault() 办法 |
三、React 事件与原生事件执行程序
在 React 中,“合成事件”会以事件委托(Event Delegation)形式绑定在组件最上层,并在组件卸载(unmount)阶段主动销毁绑定的事件。这里咱们手写一个简略示例来察看 React 事件和原生事件的执行程序:
class App extends React.Component<any, any> {
parentRef: any;
childRef: any;
constructor(props: any) {super(props);
this.parentRef = React.createRef();
this.childRef = React.createRef();}
componentDidMount() {console.log("React componentDidMount!");
this.parentRef.current?.addEventListener("click", () => {console.log("原生事件:父元素 DOM 事件监听!");
});
this.childRef.current?.addEventListener("click", () => {console.log("原生事件:子元素 DOM 事件监听!");
});
document.addEventListener("click", (e) => {console.log("原生事件:document DOM 事件监听!");
});
}
parentClickFun = () => {console.log("React 事件:父元素事件监听!");
};
childClickFun = () => {console.log("React 事件:子元素事件监听!");
};
render() {
return (<div ref={this.parentRef} onClick={this.parentClickFun}>
<div ref={this.childRef} onClick={this.childClickFun}>
剖析事件执行程序
</div>
</div>
);
}
}
export default App;
触发事件后,能够看到控制台输入:
原生事件:子元素 DOM 事件监听!原生事件:父元素 DOM 事件监听!React 事件:子元素事件监听!React 事件:父元素事件监听!原生事件:document DOM 事件监听!
通过下面流程,咱们能够了解:
- React 所有事件都挂载在
document
对象上; - 当实在 DOM 元素触发事件,会冒泡到
document
对象后,再解决 React 事件; - 所以会先执行原生事件,而后解决 React 事件;
- 最初真正执行
document
上挂载的事件。
四、合成事件的事件池 **
1. 事件池介绍
合成事件对象池,是 React 事件零碎提供的一种 性能优化形式 。 合成事件对象在事件池对立治理 , 不同类型的合成事件具备不同的事件池。
- 当事件池未满时,React 创立新的事件对象,派发给组件。
- 当事件池装满时,React 从事件池中复用事件对象,派发给组件。
对于“事件池是如何工作”的问题,能够看看上面图片:
(图片来自:ReactDeveloper https://juejin.cn/post/6844903862285893639)
2. 事件池剖析(React 16 版本)
React 事件池仅反对在 React 16 及更早版本中,在 React 17 曾经不应用事件池 。
上面以 React 16 版本为例:
function handleChange(e) {console.log("原始数据:", e.target)
setTimeout(() => {console.log("定时工作 e.target:", e.target); // null
console.log("定时工作:e:", e);
}, 100);
}
function App() {
return (
<div className="App">
<button onClick={handleChange}> 测试事件池 </button>
</div>
);
}
export default App;
能够看到输入:
在 React 16 及之前的版本,合成事件对象的事件处理函数全副被调用之后,所有属性都会被置为 null
。这时,如果咱们须要在事件处理函数运行之后获取事件对象的属性,能够应用 React 提供的 e.persist()
办法,保留所有属性:
// 只批改 handleChange 办法,其余不变
function handleChange(e) {// 只减少 persist() 执行
e.persist();
console.log("原始数据:", e.target)
setTimeout(() => {console.log("定时工作 e.target:", e.target); // null
console.log("定时工作:e:", e);
}, 100);
}
再看下后果:
3. 事件池剖析(React 17 版本)
因为 Web 端的 React 17 不应用事件池,所有不会存在上述“所有属性都会被置为 null
”的问题。
五、常见问题
1. React 事件中 this 指向问题
在 React 中,JSX 回调函数中的 this 常常会出问题,在 Class 中办法不会默认绑定 this,就会呈现上面状况,this.funName
值为 undefined
:
class App extends React.Component<any, any> {childClickFun = () => {console.log("React 事件");
};
clickFun() {console.log("React this 指向问题", this.childClickFun); // undefined
}
render() {
return (<div onClick={this.clickFun}>React this 指向问题 </div>
);
}
}
export default App;
咱们有 2 种形式解决这个问题:
- 应用
bind
办法绑定this
:
class App extends React.Component<any, any> {constructor(props: any) {super(props);
this.clickFun = this.clickFun.bind(this);
}
// 省略其余代码
}
export default App;
- 将须要应用
this
的办法改写为应用 箭头函数 定义:
class App extends React.Component<any, any> {clickFun = () => {console.log("React this 指向问题", this.childClickFun); // undefined
}
// 省略其余代码
}
export default App;
或者在回调函数中应用 箭头函数:
class App extends React.Component<any, any> {
// 省略其余代码
clickFun() {console.log("React this 指向问题", this.childClickFun); // undefined
}
render() {
return (<div onClick={() => this.clickFun()}>React this 指向问题 </div>
);
}
}
export default App;
2. 向事件传递参数问题
常常在遍历列表时,须要向事件传递额定参数,如 id
等,来指定须要操作的数据,在 React 中,能够应用 2 种形式向事件传参:
const List = [1,2,3,4];
class App extends React.Component<any, any> {
// 省略其余代码
clickFun (id) {console.log('以后点击:', id)}
render() {
return (
<div>
<h1> 第一种:通过 bind 绑定 this 传参 </h1>
{List.map(item => <div onClick={this.clickFun.bind(this, item)}> 按钮:{item}</div>)
}
<h1> 第二种:通过箭头函数绑定 this 传参 </h1>
{List.map(item => <div onClick={() => this.clickFun(item)}> 按钮:{item}</div>)
}
</div>
);
}
}
export default App;
这两种形式是等价的:
- 第一种通过
Function.prototype.bind
实现; - 第二种通过 箭头函数 实现。
3. 合成事件阻止冒泡
官网文档形容了:
从 v0.14 开始,事件处理器返回 false 时,不再阻止事件传递。你能够酌情手动调用 e.stopPropagation() 或 e.preventDefault() 作为代替计划。
也就是说,在 React 合成事件中,须要阻止冒泡时,能够应用 e.stopPropagation()
或 e.preventDefault()
办法来解决,另外还能够应用 e.nativeEvent.stopImmediatePropagation()
办法解决。
3.1 e.stopPropagation
对于开发者来说,更心愿应用 e.stopPropagation()
办法来阻止以后 DOM 事件冒泡,但事实上,从前两节介绍的执行程序可知,e.stopPropagation()
只能阻止合成事件间冒泡,即上层的合成事件,不会冒泡到下层的合成事件。事件自身还都是在 document 上执行。所以 最多只能阻止 document 事件不能再冒泡到 window 上。
class App extends React.Component<any, any> {
parentRef: any;
childRef: any;
constructor(props: any) {super(props);
this.parentRef = React.createRef();}
componentDidMount() {this.parentRef.current?.addEventListener("click", () => {console.log("阻止原生事件冒泡~");
});
document.addEventListener("click", (e) => {console.log("原生事件:document DOM 事件监听!");
});
}
parentClickFun = (e: any) => {e.stopPropagation();
console.log("阻止合成事件冒泡~");
};
render() {
return (<div ref={this.parentRef} onClick={this.parentClickFun}>
点击测试“合成事件和原生事件是否能够混用”</div>
);
}
}
export default App;
输入后果:
阻止原生事件冒泡~
阻止合成事件冒泡~
3.2 e.nativeEvent.stopImmediatePropagation
该办法能够 阻止监听同一事件的其余事件监听器被调用 。
在 React 中,一个组件只能绑定一个同类型的事件监听器,当反复定义时,前面的监听器会笼罩之前的。
事实上 nativeEvent 的 stopImmediatePropagation
只能阻止绑定在 document 上的事件监听器。而合成事件上的 e.nativeEvent.stopImmediatePropagation()
能 阻止合成事件不会冒泡到 document 上。
举一个理论案例:实现点击空白处敞开菜单的性能:
当菜单关上时,在 document 上动静注册事件,用来敞开菜单。
- 点击菜单外部,因为不冒泡,会失常执行菜单点击。
- 点击菜单内部,执行 document 上事件,敞开菜单。
在菜单敞开的一刻,在 document 上移除该事件,这样就不会反复执行该事件,节约性能,也能够在 window 上注册事件,这样能够避开 document。
**
4. 合成事件和原生事件是否能够混用
合成事件和原生事件最好不要混用 。
原生事件中如果执行了 stopPropagation
办法,则会导致其余 React
事件生效。因为所有元素的事件将无奈冒泡到 document
上。
通过后面介绍的两者事件执行程序来看,所有的 React 事件都将无奈被注册。通过代码一起看看:
class App extends React.Component<any, any> {
parentRef: any;
childRef: any;
constructor(props: any) {super(props);
this.parentRef = React.createRef();}
componentDidMount() {this.parentRef.current?.addEventListener("click", (e: any) => {e.stopPropagation();
console.log("阻止原生事件冒泡~");
});
document.addEventListener("click", (e) => {console.log("原生事件:document DOM 事件监听!");
});
}
parentClickFun = (e: any) => {console.log("阻止合成事件冒泡~");
};
render() {
return (<div ref={this.parentRef} onClick={this.parentClickFun}>
点击测试“合成事件和原生事件是否能够混用”</div>
);
}
}
export default App;
输入后果:
阻止原生事件冒泡~
好了,本文就写到这里,倡议大家能够再回去看下官网文档《合成事件》《事件处理》章节了解,有趣味的敌人也能够浏览源码《React SyntheticEvent.js》。
总结
最初在回顾下本文学习指标:
- 合成事件的概念和作用;
- 合成事件与原生事件的 3 个区别;
- 合成事件与原生事件的执行程序;
- 合成事件的事件池;
- 合成事件 4 个常见问题。
你是否都分明了?欢送一起探讨学习。
参考文章
1.《事件处理与合成事件(react)》
2. 官网文档《合成事件》《事件处理》
3.《React 合成事件和 DOM 原生事件混用须知》
4.《React 合成事件零碎之事件池》