说说React的事件机制
是什么
React基于浏览器的事件机制自身实现了一套事件机制,包括事件注册,事件的合成,事件派发等
在React中这套事件机制被称之为合成事件
合成事件(SyntheicEvent)
合成事件是React模拟原生DOM事件所有能力的一个事件对象,即浏览器原生事件的跨浏览器包装器
根据W3C规范来定义合成事件,兼容所有浏览器,拥有与浏览器原生事件相同的接口,例如:
javascript
const button = <button onClick={handleClick}> 按钮 </button>
如果想要原生的DOM事件,可以通过e.nativeEvent属性获取
javascript
const handleClick = (e) => console.log(e.nativeEvent);
const botton = <button onClick={handleClick}>按钮</button>
从上面可以看到React事件和原生事件也非常的相似,但也有一定的区别:
- 事件名称命名方式不同
javascript
// 原生事件绑定方式
<button onclick="handleClick()">按钮命名</button>
// React 合成事件绑定方式
const button = <button onClick={handleClick} > 按钮命名</button>
- 事件函数书写不同
javascript
// 原生事件 事件处理函数写法
<button onclick="handleClick()">按钮命名</button>
// React 合成事件 事件处理函数写法
const button = <button onclick={handleClick}> 按钮命名 </button>
虽然 onclick 看似绑定到 DOM 元素上,但实际并不会把事件代理函数直接绑定到真实的节点上,而是把所有的事件绑定到结构的最外层,使用一个统一的事件去监听这个事件监听器上维持了一个映射来保存所有组件内部的事件监听和处理画数。当组件挂截或卸时,只是在这个统一的事件监听器上插入或删除一些对象
当事件发生时,首先被这个统一的事件监听器处理,然后在映射里找到真正的事件处理函数并调用。这样做简化了事件处理和回收机制,效率也有很大提升
执行顺序
关于React合成事件与原生事件执行顺序,可以看看下面一个例子:
javascript
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",()=>{
console.log("原生事件:document DOM事件监听!")
});
}
parentClickFunc = ()=>{
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;
输出顺序为:
javascript
原生事件:子元素DOM事件监听!
原生事件:父元素DOM事件监听!
React事件:子元素事件监听!
React事件:父元素事件监听!
原生事件:document DOM事件监听!
可以得出以下结论:
- React所有事件都挂载在document对象上
- 当真实DOM元素触发事件,会冒泡到document对象后,再处理React事件
- 所以会先执行原生事件,然后处理React事件
所以想要阻止不同时间段的冒泡行为,对应使用不同的方法,对应如下:
- 阻止合成事件间的冒泡,用e.stopPropagation()
- 阻止合成事件与外层document上的事件冒泡,用e.nativeEvent.stopImmediatePropagation()
- 阻止合成事件与最外层document上的原生事件上的冒泡,通过判断e.target来避免
javascript
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
React事件绑定的方式有那些?区别?
是什么
在React应用中,事件名都是用小驼峰格式进行书写,例如onclick要改写成onClick最简单的事件绑定如下:
javascript
class ShowAlert extends React.Component{
showAlert(){
console.log("Hi");
}
render(){
return <button onClick={this.showAlert}>show</button>;
}
}
从上面可以看到,事件绑定的方法要使用{}包住
上述的代码看似没问题,但是当处理函数输出代码换成console.log(this)的时候,点击按钮,则会发现控制台输出undefined
如何绑定
为了解决上面正确输出this的问题,常见的绑定方式有如下:
- render方法中使用bind
- render方法中使用箭头函数
- constructcx中bind
- 定义阶段使用箭头函数绑定
render方法中使用bind
如果使用一个类组件,在其中给某个组件/元素一个onClick属性,它现在并会自定绑定其this到当前组件,解决这个问题的方法是在事件函数后使用.bind(this)将this绑定到当前组件中
javascript
class App extends React.Component{
handleClick(){
console.log('this >',this);
}
render(){
return {
<div onClick={this.handleClick.bind(this)}>test</div>
}
}
}
这种方式在组件每次render渲染的时候,都会重新进行bind的操作,影响性能
render方法中使用箭头函数
通过ES6的上下文来将this的指向绑定给当前组件,同样再每一次render的时候都会生成新的方法,影响性能
javascript
class App extends React.Component{
handleClick(){
console.log('this >',this);
}
render(){
return (
<div onClick={e => this.handleClick(e)}>test</div>
)
}
}
constructor中bind
再construct中预先bind当前组件,可以避免再render操作中重复绑定
javascript
class App extends React.Component{
constructor(props){
supper(props);
this.handleClick = this.handleClick.bind(this);
}
handleClick(){
console.log('this >',this)
}
render(){
return (
<div onClick={this.handleClick}>test</div>
)
}
}
定义阶段使用箭头函数绑定
跟上述方式三一样,能够避免在render操作中重复绑定,实现也非常的简单,如下:
javascript
class App extends React.Component{
constructor(props){
super(props);
}
handleClick = () => {
console.log('this >',this);
}
render(){
return (
<div onClick={this.handleClick}>test</div>
)
}
}
区别
上述四种方法的方法,区别主要如下:
- 编写方面:方式一,方式二写法简单,方式三的编写过于冗杂
- 性能方面:方式一方式二在每次组件render的时候会生成新的方法实例,性能问题欠缺,若该函数作为属性值传递给子组件的时候,都会导致额外的渲染,而方式三,方式四只会生成一个方法实力
综合上述,方式四是最优的事件绑定方式
React构建组件的方法有那些,区别
是什么
组件就是把图形,非图形的各种逻辑均抽象为一个统一的概念(组件)来实现开发的模式
在React中,一个类,一个函数都可以视为一个组件
- 降低整个系统的耦合度,在保持接口不变的情况下,我们可以替换不同的组件快速完成需求。例如输入框,可以替换为日历,时间,范围等组件作具体的实现
- 调试方便,由于整个系统是通过组件组合起来的,在出现问题的时候,可以用排除法直接移除组件,或者根据报错的组件快速定位问题,之所以能够快速定位,是因为每个组件之间低耦合,职责单一,所以逻辑会比分析整个系统要简单
- 提高可维护性,由于每个组件的职责单一,并且组件在系统中是被复用的,所以对代码进行优化可获得系统的整体升级
如何构建
在 React目前来讲,组件的创建分成了三种方式:
- 函数式创建
- 通过React.createClass 方式创建
- 继承 React.Component 创建
函数式创建
在 React Hooks出来之前,函数式组件可以视为无状态组件,只负责根据传入的props来展示视图,不涉及对state状态的操作
大多数组件可以写成无状态组件,通过简单组合构建其他组件
在React中,通过函数简单创建组件的示例如下:
javascript
function HelloComponent(props,/* context */){
return <div>Hello {props.name}</div>
}
通过React.createClass 方法创建
React.createClass 是react刚开始推荐的创建组件的方式,目前这种创建方式已经不怎么用了,像上述通过函数式创建的方式,最终会通过Babel转化成React.createClass这中形式,转化如下:
javascript
function HelloComponent(props)/* context */{
return React.createElement(
"div",
null,
"Hello ",
props.name
)
}
由于上述的编写方式过于冗余,目前基本上使用不上
继承React.Component创建
同样在react hooks出来之前,有状态的组件只能通过继承React.Component这种形式进行创建有状态的组件也就是组件内部存在维护的数据,在类创建的方式中通过this.state进行访问当调用this.setState修改组件的状态时,组件会再次调用render()方法进行重新渲染通过继承React.Component创建一个时钟实例如下:
javascript
class Timer extends React.Component{
constructor(props){
super(props);
this.state = { seconds:0 };
}
tick(){
this.setState(state=>({
seconds:state.seconds+1
}));
}
componentDidMount(){
this.interval = setInterval(() => this.tick(),1000);
}
componentWillUnmount(){
clearInterval(this.interval);
}
render(){
return (
<div>
Seconds:{this.state.seconds}
</div>
)
}
}
区别
由于React.createClass创建的方式过于冗余,并不建议使用,而像函数式创建和类组件创建的区别主要在于需要创建的组件是否需要为有状态组件:
- 对于一些无状态的组件创建,建议使用函数式创建的方式
- 由于react hooks的出现,函数式组件创建的组件通过使用hooks方法也能使之成为有状态组件,再加上目前推崇函数式编程,所以这里建议使用函数式的方式来创建组件
在考虑组件的选择原则上,能用无状态组件则用无状态组件
说说react中引入css的方式有哪几种?区别?
是什么
组件式开发选择合适的css解决方案尤为重要
通常会遵循以下规则
- 可以编写局部css,不会随意污染其他组件内的原生;
- 可以编写动态的css,可以获取当前组件的一些状态,根据状态的变化生成不同的css样式;
- 支持所有的css特性,伪类,动画,媒体查询等;
- 编写起来简洁方便,最好符合一贯的css风格特点
在一方面,vue使用css起来更为简洁:
- 通过style标签编写样式
- scoped属性决定编写的样式是否局部有效
- lang属性设置预处理器
- 内联样式风格的方法来跟根据最新状态设置和改变css
而在React中,引入css就不如vue方便简洁,其引入css的方式有很多,各有利弊
方式
常见的css引入方式有以下:
- 在组件内直接使用
- 组件中引入.css文件
- 组件中引入.module.css文件
- CSS in JS
在组件内直接使用
直接在组件中书写css样式,通过style属性直接引入,如下:
javascript
import React,{ Component } from "react";
const div1 = {
width:"300px",
margin:"30px auto",
backgroundColor:"#44014C", // 驼峰法
minHeight:"200px",
boxSizing:"border-box"
};
class Test extends Component {
constructor(props,context){
super(props);
}
render(){
return (
<div>
<div style={div1}>123</div>
<div style={{backgroundColor:"red"}} >
</div>
);
}
}
export default Test;
上面可以看到,css属性需要转换成驼峰写法
这种方式优点:
- 内联样式,样式之间不会有冲突
- 可以动态获取当前state中的状态
缺点: - 写法上都需要使用驼峰标识
- 某些样式没有提示
- 大量的样式,代码混乱
- 某些样式无法编写(比如伪类/伪元素)
组件中引入css文件
将css单独写在css文件中,然后在组件中直接引入
App.css文件:
javascript
.title{
color:red;
font-size:20px;
}
.desc{
color:green;
text-decoration:underline;
}
组件中引入:
javascript
import React,{ PureComponent } from 'react';
import Home from './Home';
import './App.css';
export default class App extends PureComponent{
render(){
return (
<div className="app">
<h2 className="title">我是App的标题</h2>
<p className="desc">我是App中的一段文字描述</p>
</div>
)
}
}
这种方式存在不好的地方在于样式是全局生效,样式之间会相互影响
组件中引入.module.css文件
将css文件作为一个模块引入,这个模块中的所有css,只作用于当前组件,不会影响当前组件的后代组件
这种方式是webpack特工的方案,只要配置webpack配置文件中modules:true即可
javascript
import React,{ PureComponent } from 'react';
import Home from './Home';
import './App.module.css';
export default class App extends PureComponent{
render(){
return (
<div className="app">
<h2 className="title">我是App的标题</h2>
<p className="desc">我是App中的一段文字描述</p>
</div>
)
}
}
这种方式能够解决局部作用域问题,但也有一定的缺陷:
- 引用的类名,不能使用连接符(.xxx-xx),在JavaScript中是不识别的
- 所有的className都必须使用(style.className)的形式来编写
- 不方便动态来修改某些样式,依然需要使用内联的方式;
CSS in JS
CSS-in-JS,是指一种模式,其中CSS由JavaScript生成而不是在外部文件中定义此功能并不是React的一部分,而是由第三方库提供,例如:
- styled-components
- emotion
- glamorous
下面主要看看styled-components的基本使用
本质是通过函数的调用,最终创建出一个组件:
- 这个组件会被自动添加上一个不重复的class
- styled-components会给该class添加相关的样式
基本使用如下:
创建一个style.js文件用于存放样式组件:
javascript
export const SelfLink = styled.div`
height:50px;
border:1px solid red;
color:yellow;
`;
export const selfButton = styled.div`
height:150px;
width:150px;
color:${props => props.color};
background-image:url(${props=>props.src});
background-size:150px 150px;
`
引入样式组件也很简单:
javascript
import React,{ Component } from "react";
import { SelfLink,SelfButton } from "./style";
class Test extends Componet{
constructor(props,context){
super(props);
}
render(){
return (
<div>
<SelfLink title="People's Republic of China">app.js</SelfLink>
<SelfButton color="palevioletred" style={{color:"pink"}} src={{fist}}>
SelfButton</SelfButton>
</div>
);
}
}
export default Test;
区别
通过上面四种样式的引入,可以看到:
- 在组件内部使用css该方式编写方便,容易能够根据状态修改样式属性,但是大量的颜色编写容易导致代码混乱
- 组件中引入.css文件符合我们日常的编写习惯,但是作用域是全局的,样式直接会层叠
- 引入.module.css文件能够解决局部作用域问题,但是不方便动态修改样式,需要使用内联的方式进行样式的编写
- 通过css in js这种方法,可以满足大部分场景的应用,可以类似于预处理器一样样式嵌套,定义,修改状态等
至于使用React用哪种方案引入css,并没有一个绝对的答案,可以根据各自情况选择合适的方案
说说React生命周期有那些不同阶段?每个阶段对应的方法是?
是什么
生命周期(Life Cycle) 的概念应用很广泛,特别是在经济,环境技术,社会等诸多领域经常出现,其基本含义可以通俗的理解为"从摇篮到坟墓"(Cradle-to-Grave)的过程跟Vue一样,React整个组件生命周期包括从创建,初始化数据,编译模版,挂载Dom->渲染,更新->渲染,卸载等一系列过程
流程
这里主要讲述React16.4之后的生命周期,可以分成三个阶段:
- 创建阶段
- 更新阶段
- 卸载阶段
创建阶段
创建阶段主要分成了以下几个生命周期的方法:
- constructor
- getDerivedStateFromProps
- render
- componentDidMount
contructor
实例过程中自动调用的方法,在方法内部通过super关键字获取来自父组件的props在该方法中,通常的操作为初始化state状态或者在this上挂载方法
componentDidMount
该方法是新增的生命周期方法,是一个静态的方法,因此不能访问到组件的实例执行时机:组件创建和更新阶段,不论是props变化还是state变化,也会调用在每次 render 方法前调用,第一个参数为即将更新的 props ,第二个参数为上一个状态的 state,可以比较props 和 state 来加一些限制条件,防止无用的state更新该方法需要返回一个新的对象作为新的 state 或者返回 null 表示 state 状态不需要更新
render
类组件必须实现方法,用于渲染DOM结构,可以访问组件state与prop属性
注意:不要在render里面setState,否则会触发死循环导致内存崩溃
componentDidMount
组件挂载到真实DOM节点后执行,其在render方法之后执行
此方法多用于执行一些数据获取,事件监听等操作
更新阶段
该阶段的函数主要为如下方法:
- getDerivedStateFromProps
- shouldComponentUpdate
- render
- getSnapshotBoforeUpdate
- componentDidUpdate
getDerivedStateFormProps
该方法介绍同上
shouldComponentUpdate
用于告知组件本身基于当前的 props 和 state 是否需要重新渲染组件,默认情况返回 true执行时机:到新的props或者state时都会调用,通过返回true或者false告知组件更新与否一般情况,不建议在该周期方法中进行深层比较,会影效率同时也不能调用 setState,否则会导致无限循环调用更新
render
介绍同上
getSnapshotBeforeUpdate
该周期函数在 render 后执行,执行之时D0M 元豪还没有被更新该方法返回的一个 5napshot值,作为componentDidUpdate 第三个参数传入
javascript
getSnapshotBeforeUpdate(prevProps,prevstate){
console.log( '#enter getSnapshotBeforeUpdate');
return 'foo';
}
componentDidUpdate(prevProps,prevState,snapshot){
console.log( '#enter componentDidUpdate snapshot =',snapshot);
}
此方法的目的在于获取组件更新前的一些信息,比如组件的滚动位置之类的,在组件更新后可以根据这些信息恢复一些UI视觉上的状态
componentDidUpdate
执行时机:组件更新结束后触发
在该方法中,可以根据前后的props和state的变化做出相应的操作,如获取数据,修改DOM样式等
卸载阶段
componentWillUnmount
此方法用于组件卸载前,清理一些注册是监听时间,或者取消订阅的网络请求等,一旦一个组件实例被卸载,其不会被再次挂载,而只可能是被重新创建
总结
通过两个图的对比,可以发现新版的生命周期减少了以下三种方法:
- componentWillMount
- componentWillReceiveProps
- componentWillUpdate
其实这三个方法仍然存在,只是在前者加上了 UNSAFE_的前缀,如 UNSAFE_componentWillMount ,并不像字面意思那样表示不安全,而是表示这些生命周期的代码可能在未来的 react 版本可能度除
同时也新增了两个生命周期函数:
- getDerivedStateFromProps
- getSnapshotBeforeUpdate
感谢大家观看,我们下次见