7、React事件机制
一、是什么
React
基于浏览器的事件机制自身实现了一套事件机制,包括事件注册、事件的合成、事件冒泡、事件派发等
在React
中这套事件机制被称之为合成事件
合成事件(SyntheticEvent)
合成事件是 React
模拟原生 DOM
事件所有能力的一个事件对象,即浏览器原生事件的跨浏览器包装器
根据 W3C
规范来定义合成事件,兼容所有浏览器,拥有与浏览器原生事件相同的接口,例如:
const button = <button onClick={handleClick}>按钮</button>
如果想要获得原生DOM
事件,可以通过e.nativeEvent
属性获取
const handleClick = (e) => console.log(e.nativeEvent);;
const button = <button onClick={handleClick}>按钮</button>
从上面可以看到React
事件和原生事件也非常的相似,但也有一定的区别:
- 事件名称命名方式不同
// 原生事件绑定方式
<button onclick="handleClick()">按钮命名</button>
// React 合成事件绑定方式
const button = <button onClick={handleClick}>按钮命名</button>
- 事件处理函数书写不同
// 原生事件 事件处理函数写法
<button onclick="handleClick()">按钮命名</button>
// React 合成事件 事件处理函数写法
const button = <button onClick={handleClick}>按钮命名</button>
虽然onclick
看似绑定到DOM
元素上,但实际并不会把事件代理函数直接绑定到真实的节点上,而是把所有的事件绑定到结构的最外层,使用一个统一的事件去监听
这个事件监听器上维持了一个映射来保存所有组件内部的事件监听和处理函数。当组件挂载或卸载时,只是在这个统一的事件监听器上插入或删除一些对象
当事件发生时,首先被这个统一的事件监听器处理,然后在映射里找到真正的事件处理函数并调用。这样做简化了事件处理和回收机制,效率也有很大提升
二、执行顺序
关于React
合成事件与原生事件执行顺序,可以看看下面一个例子:
import React from 'react';
class App extends React.Component{
constructor(props) {
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 上挂载的事件
对应过程如图所示:
所以想要阻止不同时间段的冒泡行为,对应使用不同的方法,对应如下:
- 阻止合成事件间的冒泡,用
e.stopPropagation()
- 阻止合成事件与最外层 document 上的事件间的冒泡,用
e.nativeEvent.stopImmediatePropagation()
- 阻止合成事件与最外层document上的原生事件上的冒泡,通过判断
e.target
来避免
document.body.addEventListener('click', e => {
if (e.target && e.target.matches('div.code')) {
return;
}
this.setState({ active: false,});});
}
三、总结
React
事件机制总结如下:
- React 上注册的事件最终会绑定在
document这个 DOM
上,而不是 React 组件对应的 DOM(减少内存开销就是因为所有的事件都绑定在 document 上
,其他节点没有绑定事件) - React 自身实现了一套事件
冒泡机制
,所以这也就是为什么我们event.stopPropagation()
无效的原因。 - React 通过
队列
的形式,从触发的组件向父组件回溯
,然后调用他们JSX
中定义的callback
- React 有一套自己的合成事件
SyntheticEvent
8、React组件生命周期有哪几个阶段?
1.初始渲染阶段:组件即将开始其生命周期并进入DOM的阶段
getDefaultProps
:获取实例的默认属性。getInitialState
:获取每个实例的初始化状态。componentWillMount
:组件即将被挂载、渲染到页面。render
:组件在这里生成虚拟DOM
节点。componentDidMount
:组件真正被挂载之后。
2.更新阶段:一旦组件被添加到DOM,它只有在props
或者state
变化时才有可能更新或者重新渲染。这些只发生在这个阶段。
componentWillReceiveProps
:组件将要接受到属性的时候调用。shouldComponentUpdate
:组件接收到新属性
或者新状态
的时候(可以返回false,接收到数据后不更新,组织render调用,后面的函数不会在执行了)componentWillUpdate
:组件即将更新不能修改属性以及状态。render
:组件重新绘制。componentDidUpdate
:组件已经更新。
3.卸载阶段:组件生命周期的最后阶段,组件被销毁并从DOM中删除
componentWillUnmount
:组件即将销毁。
9、React组件的生命周期方法
componentWillMount()
:在渲染之前执行,客户端与服务端均会执行(获取渲染页面的数据)componentDidMount()
:仅在第一次渲染后在客户端执行。componentWillReceiveProps()
:当从父类接收到props并且在调用另一个渲染器之前调用。shouldComponentUpdate()
:根据特定条件返回true
或者false
。如果希望更新组件,请返回true
,不想更新组件则返回false
,就会阻止render渲染。默认情况下,返回true。componentWillUpdate()
:在DOM中进行渲染之前调用。componentDidUpdate()
:在渲染完成立即调用。componentWillUnmount()
:从DOM卸载组建后调用。用于清理内存空间
。
10、为何虚拟DOM可以提高性能
虚拟DOM(Virtual DOM
)是一个JS对象
,是一个真实DOM 的js对象
;虚拟dom
相当于在js
和真实dom
中加了一个缓存
,利用dom Diff
算法避免不必要的dom操作,从而提高性能。
用JavaScript 对象结构
表示 DOM树的结构
;然后利用这个构建一个真实的DOM 树,查到文档当中当状态变更的时候,重新构造一棵树的对象树
。然后用新的树与旧的树进行比较,记录两棵树的差异,把两所树的差异应用到真正构建的真实DOM树上
,这样视图就更新了。
11、React中的key有何作用?
React
和Vue
一样,均有diff
算法,元素key的作用就是判断元素是新创建的还是被移动的,从而减少不必要的Diff
,因此key的值就是为每一个元素赋予一个标识。
- 如果列表数据渲染中,在数据后面插入一条数据,
key
作用并不大;前面的元素在diff
算法中,前面的元素由于是完全相同的,并不会产生删除创建操作,在最后一个比较的时候,则需要插入到新的DOM树中。因此,在这种情况下,元素有无key属性意义并不大。 - 如果列表数据渲染中,在前面插入数据时,当拥有key的时候,react根据key属性匹配原有树上的子元素以及最新树上的子元素,只需要将元素插入到最前面位置,当没有key的时候,
所有的li标签
都需要进行修改 - 并不是拥有key值代表性能越高,如果说只是文本内容改变了,不写key反而性能和效率更高,主要是因为不写key是将所有的文本内容替换一下,节点不会发生变化,而写key则涉及到了节点的增和删,发现旧key不存在了,则将其删除,新key在之前没有,则插入,这就增加性能的开销
总结
良好使用key
属性是性能优化的非常关键的一步,注意事项:
key
应该是唯一
的。key
不要使用随机值
(随机数在下一次 render 时,会重新生成一个)- 避免使用
ndex
作为key
12、react 中的 diff
算法怎么完成的
- 将树结构按照层级分解,只比较同级元素。
- 通过给列表结构的每一个单元添加的唯一key值 进行区分同层次的子节点的比较。
- React只会匹配相同
组件名字
的component。 - 合并操作,调用component 的
setState
方法的时候。React将其标记为dirty
,到每一个事件循环结束
,React检查所有标记为 dirty 的component 重新绘制。 - 选择性渲染。开发人员可以重写
shouldComponentUpdate
提高diff
de 性能。