利用插槽实现简单的组件封装
根据需要动态变化的内容,决定通过属性传递还是使用插槽传递。在这里,两个组件标签中标题名字和内容不一样,底部的按钮也是按需添加显示。
<>
<Dialog title='友情提示' content='今天天气很热'></Dialog>
<Dialog content='今天路上车很多'>
<button>确定</button>
<button>取消</button>
</Dialog>
</>
export const Dialog = (props) => {
let { title, content, children } = props
children = React.Children.toArray(children)
return <div className='dialogBox' style={{ width: '140px' }}>
<div className="header" style={{ display: 'flex', justifyContent: "space-between", alignItems: "center" }}>
<h2 className='title'>{title}</h2>
<span>X</span>
</div>
<div className="main">
{content}
</div>
{/* 按钮存放在children中,可能没有 */}
{children.length > 0 ? <div className="footer">{children}</div> : null}
</div>
}
Dialog.defaultProps = {
title: "温馨提示"
}
Dialog.propTypes = {
title: PropTypes.string,
content: PropTypes.string.isRequired
}
静态组件和动态组件
静态组件
静态组件就是初次渲染完成之后,往后如果数据再次更新的时候,不会重新渲染。函数组件就属于静态组件。
如下所示函数组件中,存在一个Vote函数执行的上下文(作用域),在该函数中定义两个变量计数,点击按钮的时候回调函数又会产生一个上下文,但是该上下文无所需要的变量,因此会去寻找外围即Vote函数的上下文中的私有变量。最终私有变量的数值会累加,但是页面不会更新。除非在父组件中,重新调用这个函数组件,即渲染最新的内容
export const Vote = (props) => {
let { title } = props
let supportNum = 10, opposeNum = 5
return <div className="box">
<header>标题:{title}</header>
<div className="main">
<h3>总人数:{supportNum + opposeNum}</h3>
<h3>支持人数:{supportNum}</h3>
<h3>反对人数:{opposeNum}</h3>
</div>
<footer>
<button onClick={() => {
supportNum++
console.log(supportNum);
}}>支持</button>
<button onClick={() => {
opposeNum++
console.log(opposeNum);
}}>反对</button>
</footer>
</div>
}
动态组件
在操作组件内部数据的时候,会重新渲染视图。类组件和Hooks组件(在函数组件内部调用Hooks函数)都属于动态组件
动态组件需要用到react
框架提供了Component
或PureComponent
。
类组件
类组件要求必须继承React.Component或React.PureComponent这个类
类组件要求必须在当前类的原型身上设置一个render方法,在render方法中返回一个JSX视图
类组件的定义可以使用ES6提供的class
语法定义并使用extends
继承,也可以使用ES5的函数实现,但是过于麻烦
ES5,函数实现混合继承
export function Vote() {
React.Component.call(this); //调用call,继承父类的属性和方法
this.state = {
...
}
}
// 推荐这样子设置,继承Component原型的所以属性和方法,添加属性的时候不会影响Component的原型
Object.setPrototypeOf(Vote.prototype, React.Component.prototype); //设置原型,继承父类原型链的属性和方法
Vote.prototype.render = function () {
return <div className="box">
<header>标题:投票</header>
<div className="main">
<h3>总人数:15</h3>
<h3>支持人数:10</h3>
<h3>反对人数:5</h3>
</div>
<footer>
<button onClick={() => {
}}>支持</button>
<button onClick={() => {
}}>反对</button>
</footer>
</div>
}
// 可以使用如下方法继承,原型完全指向了Component的原型,共享原型对象地址
// 后期添加的属性,Component身上也会添加
// Vote.prototype = React.Component.prototype
ES6 class实现继承,效果和上面完全一样
export class Vote extends React.Component {
render() {
return <div className="box">
<header>标题:投票</header>
<div className="main">
<h3>总人数:15</h3>
<h3>支持人数:10</h3>
<h3>反对人数:5</h3>
</div>
<footer>
<button onClick={() => {
}}>支持</button>
<button onClick={() => {
}}>反对</button>
</footer>
</div>
}
}
class基础知识
class Text {
// 往实例身上设置私有属性和方法
a = 100
constructor(b) {
this.b = b
}
getA = function () { }
getB = () => { } // 实例上的方法是不一样的
// 往原型身上设置公共的属性和方法,原型上的公共方法都是调用同一个
GetA() { } //注意不写=的时候,方法是往原型上添加
// 设置静态属性和方法
static d = 10
// 下面三种静态方法调用格式一致
static GetD() { console.log('D'); } //不可枚举
static GetDD = () => { console.log('DD'); } //可枚举
static GetDDD = function () { console.log('DDD'); } //可枚举
}
Text.prototype.c = 10 //原型上添加属性只能在外部定义
let text = new Text(10);
/*
使用extends继承React.Component
1:首先基于call方法继承父类的属性和方法,会默认存在一个无参构造器执行super()==>React.Component.call(this),
this为当前实例,之后继承的实例对象默认有context,props,refs,updater这四个参数
function Component(props, context, updater) { //若传参则赋值
this.props = props;
this.context = context;
this.refs = emptyObject; //空对象
this.updater = updater || ReactNoopUpdateQueue;
}
2:其次基于Son.proptotype.__proto__=>React.Component.prototype,因此,当访问当前实例找不到某一个属性的时候
就会去访问其原型,若原型没有,则访问父类React.Component的原型,该父类原型默认提供forceUpdate,isReactComponent,setState。
3:若自己手动定义了一个构造器,则必须在内部第一句调用super方法
*/
class Son extends React.Component {
constructor() {
// 可以在super中传递参数,会被Component()方法中的参数接收
super() // 基于call方法,执行React.Component.call(this)
}
x = 10
getX() { }
}
let son = new Son()
类组件初次渲染的底层逻辑
每次<Vote title= '投票' />
调用的时候,相当于new Vote({ title: '投票' })
创建类实例
/*
转换为React.createElement(Vote, { title: '投票' })格式
再次转换为虚拟DOM,{type:class Vote , props:{title:'投票'}}
render函数渲染的时候根据type的值进行不同的操作
1:如果type是一个字符则创建对应的标签
2:如果type为普通函数,则会去执行该函数,并将props作为参数传入
3:如果type是一个类(构造函数),每次<Vote .../>调用的时候,相当于
new Vote({ title: '投票' })创建类实例,同时也会将参数传入,在类组件
中基于render函数执行,返回JSX视图
*/
root.render(
<>
<Vote title='投票' />
</>
)
第一步:初始化数据
类组件中首先初始化数据和规则校验,最后才会处理属性的其他操作
根据之前类的知识,我们知道每次new Vote(参数)
实例化传入的参数,可以在构造器中接收到。继承的时候如果写了构造器,那么构造器内部必须先调用spuer
方法。在这里,给super
方法调用的时候传入参数,这个时候就会将参数传递给Component
函数的第一个参数props中,其余为传值均为undefined,同时修改了他们的this,。
export class Vote extends React.Component {
constructor(props) {
super(props) //必须调用父类方法
console.log(this.props); //此时默认的四个参数中,只有props有值
}
render() {
return JSX视图语法
}
}
尽管可以按照上面传值的时候就再构造器中接收,同时我们也可以不设置构造器接收。当不设置构造器的时候,会默认存在一个构造器去执行super
方法。这个时候在构造器中访问this.props
时为undefined。执行完毕构造器后,React内部会自动把传递的props挂载到实例身上。只需要保证this指向了当前实例即可。
export class Vote extends React.Component {
//其他位置访问的时候,均为undefined
render() {
console.log(this.props); //可以访问到传递的值
return JSX视图
}
}
第二步:类组件的规则校验
也是同函数组件一样设置静态私有属性。同样传递过来的属性也是被冻结的。
如下核心代码,这里需要注意,在组件标签中并未传递的属性,如果存在默认值的规则,那么最终会和标签中的属性一并挂载到this.props
上(先校验规则,后初始化)
// <Vote title='投票' />并没有传递num
static defaultProps = {
num: 0
}
static propTypes = {
num: PropTypes.number,
title: PropTypes.string.isRequired
}
constructor(props) {
super(props) //必须调用父类方法
console.log(this.props); //此时默认的四个参数中,只有props有值
}
初始化状态以及更新视图
第三步:初始化状态
在函数组件中,由于是一个静态组件,所以数据更新的时候无法更新视图,但是在类组件中,其属于动态组件,会存在一个状态保存数据,每次数据更新的时候,手动触发视图更新。
默认状态下,会往实例身上挂载一个state:null
的属性,因此我们需要手动初始化状态中的数据。
当我们初始化完状态state
中的数据后,在实例的身上state
的值就为当前初始化的值
state = {
supNum: 0,
oppNum: 0
}
更新视图
当我们初始化状态后,就可以在JSX中使用,我们修改代码如下,发现点击数值增加的时候,页面并没有更新,这种情况就和函数组件一样。然后我们直接修改state
状态中的值,发现也仅仅值变化,页面不会更新。
render() {
let { supNum, oppNum } = this.state
return <div className="box">
<button onClick={() => {
supNum++ //或 this.state.supNum++
console.log(supNum);
}}>支持</button>
</div>
}
我们需要调用React.Component.prototype提供的方法更新视图
- 使用
setState()
方法实现,每次都会重新触发render函数
执行
<button onClick={() => {
this.setState({ //设置部分状态的改变,同时更新视图
supNum: supNum + 1 //或this.state.supNum + 1
})
}}>支持</button>
- 使用
forceUpdate
方法强制视图更新,注意,这里需要直接更改状态中的值
<button onClick={() => {
this.state.oppNum++
this.forceUpdate()
}}>反对</button>
第四步:componentWillMount()生命周期函数
当类组件的数据已经完成初始化,规则校验,状态初始化操作后,之后便会触发componentWillMount()
钩子,该钩子在组件挂载之前触发,数据更新后不会触发。但是该钩子目前是不安全的。浏览器会给出黄色提示。这个时候搭配React.StrictMode
标签使用没有问题。
但是在使用React严格模式的时候使用UNSAFE_componentWillMount()
钩子,即明确告诉浏览器这个钩子是不安全的了,这个时候控制台就会报错,给出红色提示。
第五步:调用render函数渲染
componentWillMount()
钩子执行完毕后执行render()
钩子,即render函数
。
第六步:componentDidMount()钩子
componentDidMount()
生命周期函数会在render
钩子执行完毕后,将虚拟DOM渲染为真实DOM后执行,第一次渲染完毕后会执行,往后数据更新不会触发,这个时候可以获取页面的真实DOM结构。
类组件更新时底层逻辑
第一步:shouldComponentUpdate生命周期钩子
当状态发生改变的时候,首先会触发shouldComponentUpdate(nextProps, nextState)
,代表是否允许更新,该钩子默认接收两个参数。第一个为更新后的属性,第二个更新后状态的值。同时该钩子需要返回一个布尔值
如果返回true
,代表允许更新,会执行下一个操作,若返回false
,则代表不允许更新,不处理接下来的操作。
shouldComponentUpdate(nextProps, nextState) {
console.log('shouldComponentUpdate');
console.log(nextState, this.state); //注意此时状态还没有改变
return true
}
第二步:componentWillUpdate生命周期钩子
在将要更新前调用,该钩子也是在实验阶段存在安全性,所以使用UNSAFE_componentWillUpdate()
代替。该函数默认也会接收两个参数,与上面的允许更新钩子参数一致。
UNSAFE_componentWillUpdate(nextProps, nextState) {
console.log('componentWillUpdate', nextState, this.state); //注意此时状态还没有发生改变
}
第三步:根据最新的状态值/属性值修改对应的内容(如this.state.xxx)
第四步:再次触发render钩子
调用render
函数的时候,组件会更新,将最新的JSX代码编译为虚拟DOM,将新的虚拟DOM和缓存中的虚拟DOM进行比对(DOM-DIFF算法比对),将差异的部分渲染到页面中。
第五步:componentDidUpdate钩子
在更新完毕后,会执行componentDidUpdate(prevProps, prevState)
钩子,该函数默认接收两个参数分别为更新前的属性/状态值。
总流程
特殊情况
如果是采用setState()
更新,那么会走完所以的流程,期间更新的时候也会根据shouldComponentUpdate()
钩子的返回值决定是否继续更新。
同样如果采样forceUpdate()
强制更新,那么就不会走允许更新钩子,直接跳过往下执行。如果使用了forceUpdate
和PureComponent
那么控制台会报错提示告知不能一起使用。
上面这些情况是组件内部数据更新的时候,如何更新视图的过程。接下来是父组件中触发子组件更新的情况,会多一个生命周期函数
componentWillReceiveProps钩子
componentWillReceiveProps()
该钩子如果是组件内部触发更新,那么是不会被调用的。
如下这段代码,父组件在3秒后重新执行了渲染,这样子会间接性的渲染子组件,采样深度遍历优先处理子组件
root.render(
<>
<Vote title='投票' />
</>
)
setTimeout(() => {
root.render(
<>
<Vote title='今天天气很好' num={10} />
</>
)
}, 3000);
UNSAFE_componentWillReceiveProps(nextProps) {
console.log("componentWillReceiveProps", nextProps, this.props); //此时属性的值还没有修改
}
componentWillReceiveProps()
钩子在渲染的时候,会在shouldComponentUpdate()
允许更新钩子前调用,后续步骤和组件内部更新一致。
生命周期图
Component和PureComponent区别
当基于Component
继承的时候代码如下,每次点击都会成功添加一个元素。如下代码中,设置了一个状态,当该状态的值发生改变的时候,会去更新视图。但是如下代码,在继承PureComponent
的时候,视图就不会更新添加元素(但是状态所对应堆内存中的值会发生改变)。
export class DemoComponent extends React.Component {
state = {
arr: [10, 20, 30] //数组也属于对象,存放堆内存,具有引用地址 如0x0001
}
render() {
let { arr } = this.state //结构出来,引用地址也是0x0001
return <div>
{arr.map((item, index) => {
return <h4 key={index}>{item}</h4>
})}
<button onClick={() => {
arr.push(arr[arr.length - 1] + 10) //往0x0001地址中所对应的堆内存添加内容,非修改状态所对应地址
console.log(this.state.arr); //会被改变[10,20,30,40]
this.setState({
arr //重新将0x0001地址赋予,引用不变
})
}}>点击增加元素</button>
</div>
}
}
PureComponent
类默认存在shouldComponentUpdate
生命周期函数,如果手动添加了该钩子,那么控制台会给出报错提示。
- 在该钩子中,会对新旧属性/状态进行浅比较
- 如果经过浅比较,发现新旧属性和状态并没有改变,就返回false,不允许更新,否则才会去执行更新操作。
基于上面分析,写一段浅比较的代码
function isObj(obj) {
return obj !== null && /^(object|function)$/.test(typeof obj)
}
const shallowEuqal = (objA, objB) => {
if (!isObj(objA) || !isObj(objB)) return false
if (objA === objB) return true //相同引用直接相等
let keyA = Reflect.ownKeys(objA)
let keyB = Reflect.ownKeys(objB)
// 判断健是不是相等,或则健所对应的值是否相等(需要将NaN考虑)
for (let i = 0; i < keyA.length; i++) {
let key = keyA[i]
if (!objB.hasOwnProperty(key) || !Object.is(objA[key], objB[key])) return false
}
return true // 浅比较通过,代表相等,返回true
}
在PureComponent
继承中,通过shouldComponentUpdate钩子
判断是不是浅比较,实际是基于如下代码逻辑
shouldComponentUpdate(nextProps, nextState) {
let { props, state } = this //修改浅的值
return !shallowEuqal(props, nextProps) && !shallowEuqal(state, nextState)
}
如果想在PureComponent
实现更新视图的操作,有如下两种办法
- 直接使用强制更新
this.forceUpdate()
,会跳过shouldComponentUpdate()
判断 - 打破浅比较的判断逻辑,赋予新地址:
this.setState({arr: [...arr]})
React中如何使用ref
给标签绑定ref
- 在React中可以使用原生的JS语法获取DOM元素,不推荐使用
- 使用
ref
获取DOM元素,当给一个标签绑定了ref
属性后,在转换为createElement
格式的时候,会传入到第二个配置对象中。最终转换为虚拟DOM的时候,在虚拟DOM对象的ref
属性中被获取到。最终在使用传入Component()
函数的参数中接收并绑定到当前实例身上this.refs
。但是这种方法不推荐使用。(原理:如果检测到ref是一个字符串,那么会直接往当前的this.refs
身上增加一个由该字符串组成的成员,该成员的值就是其所对应的DOM)
<h1 className="title" ref='h1Title'>今天天气35度</h1>
React.createElement(
"h1",
{
className: "title",
ref: "h1Title"
},
"\u4ECA\u5929\u5929\u6C1435\u5EA6")
- 推荐将
ref
写成一个函数式,如果识别ref
为函数式,那么会将当前的DOM元素作为参数x,同时函数体中会往当前实例身上绑定一个新属性,其值为当前DOM元素。获取的时候直接使用this.dom
即可。
<h1 className="title" ref={x => this.dom = x}>今天天气35度</h1>
- 使用
React.createRef()
创建一个ref对象。如下代码,如果创建出来的ref对象,没有绑定到DOM元素身上,那么默认会存在一个{current:null}
的对象
h1Box = React.createRef()
console.log(this.h1Box);
如果绑定到DOM元素身上的时候,{current:绑定的DOM}
<h1 className="title" ref={this.h1Box}>今天天气35度</h1>
给类组件绑定ref
如果给一个类组件绑定一个ref
属性,那么获取的是当前组件实例
class Child1 extends React.Component {
state = {
x: 10
}
render() {
return <div>
child1
<em ref={x => this.em = x}>我是em</em>
</div>
}
}
// 父组件调用
<Child1 ref={x => this.child1 = x}></Child1>
console.log(this.child1);
在类组件调用过程中,可以获取一个子组件内部的DOM,给一个类组件绑定ref
属性即获取当前组件的实例,而在当前组件的实例身上又给em标签绑定了ref属性。所以可以直接获取
给函数组件绑定ref
如果直接给一个函数组件绑定ref属性,那么控制台会给出报错,给出的提示信息推荐使用React.forwardRef()
方法完成转换
const Child2 = () => {
return <div>
child2
</div>
}
<Child2 ref={x => this.child2 = x}></Child2>
React.forwardRef()
该方法可以获取一个函数组件内部的某个DOM元素
使用 forwardRef(render函数)
来让你的组件接收一个 ref 并将其传递给一个子组件。修改函数组件的代码。同时输出传入的ref值发现其值就是x => this.child2 = x
。这里的this是父组件实例。
const Child2 = React.forwardRef((props, ref) => {
console.log(ref);
return <div>
child2
<button ref={ref}>按钮</button> //等价于ref={x => this.child2 = x},只不过这里的this是指向父组件
</div>
})
某父组件内部调用
<Child2 ref={x => this.child2 = x}></Child2>
componentDidMount() {
console.log(this.child2);
}
setState()深入理解
setState
函数支持修改部分状态参数对象或函数作为第一个参数,和传入一个指定的回调函数作为第二个参数。callback回调函数不论更新状态后,页面是否更新都会执行
this.setState({}/函数,callback)
如下实例代码
state = {
x: 1,
y: 10,
z: 100
}
handle = () => {
// 使用箭头函数,this可以安全使用
this.setState({
x: 10 //不论原state状态中有多少值,只需要部分,不会影响其他
}, () => {
console.log("setState执行了");
})
}
componentDidUpdate() {
console.log("componentDidUpdate执行了");
}
<button onClick={this.handle}>点击</button>
- 在
componentDidUpdate
生命周期执行完毕后才会执行setState
函数指定的callback
回调函数。可以在指定的状态发生改变的时候,专门执行某些操作。
- 即使
shouldComponentUpdate
生命周期函数返回false不允许更新,callback
函数依旧会去执行
setState是同步还是异步
在上面代码的基础上进行修改,我们在render
函数中输出打印,如果setState
方法是同步,那么每次调用的时候都会执行一次render
方法
handle = () => {
// 使用箭头函数,this可以安全使用
this.setState({ x: this.state.x + 1 }) // x还是原值1
this.setState({ y: this.state.y + 1 })
this.setState({ z: this.state.z + 1 }) // z还是原值100
}
render() {
console.log("render执行了");
。。。
}
最终只执行了一次render
函数,代码setState方法
是异步的
React18中setState()
在任何地方执行都是异步,在React18中有一套更新队列的机制,基于异步操作,实现状态的批处理
React18中,无论任何地方使用setState()
都是异步的,都是基于update更新队列完成,实现批处理。
React16中,如果是基于合成事件Onxxx绑定事件,生命周期函数等,setState()
都是异步的。如果setState()
出现在其他异步操作中,如定时器,原生DOM操作事件等,那么setState()
就是同步的。
修改之前的代码,我们需要完成一个效果,就是每次更新完成x和y的值之后,拿着最新的x与y之和赋予z。但是如下代码不借助定时器的时候无法实现效果。那么该如何实现效果
state = {
x: 5,
y: 10,
z: 0
}
handle = () => {
let { x, y } = this.state
this.setState({ x: x + 1 })
this.setState({ y: y + 1 })
this.setState({ z: this.state.x + this.state.y })
}
借助flushSync ()
完成,首先需要引入。该函数的功能立即刷新update更新队列,即立即执行队列中的所有操作,执行一次批处理。强制 React 刷新所有挂起的工作,并同步更新 DOM。
import { flushSync } from 'react-dom'
当执行第一个setState
函数的时候,放入队列等待,同时执行输出语句state内部的值没有改变。接着执行flushSync
函数内部的代码,又将setState
函数放入队列,接着继续执行第二个输出,state还是不变。但是flushSync
函数执行完毕的时候会立即刷新队列,执行一次批处理。 所以第三个数组语句中显示的state是更新后的值。
handle = () => {
let { x, y } = this.state
this.setState({ x: x + 1 })
console.log(this.state); //5 10 0
flushSync(() => {
this.setState({ y: y + 1 })
console.log(this.state); //5 10 0
})
console.log(this.state); //6 11 0
this.setState({ z: this.state.x + this.state.y })
}
如下代码this.state.x
的初始值为0,那么循环二十次后页面渲染几次,x最终值为多少?
页面只会渲染一次,x最终为1
for (let i = 0; i < 20; i++) {
this.setState({
x: this.state.x + 1
})
}
如果想让上面的代码执行20次如何解决
for (let i = 0; i < 20; i++) {
flushSync(() => {
this.setState({
x: this.state.x + 1
})
})
}
如果只想执行一次,但是最终结果需要为20,如何处理
这个时候就需要将setState方法的第一个参数设置为函数了
,该函数参数需要返回一个对象修改状态。
修改后的代码如下。以下代码执行的过程:循环20次,每次都将setState
函数的第一个函数参数插入更新队列。当循环完毕后,开始执行队列中的函数,会进行批处理依次执行所有函数。这个时候prevState
从值为{x:0}开始依次计算。
for (let i = 0; i < 20; i++) {
this.setState(prevState => { //prevState的初始值为{x:0},记录修改前的状态值
return {
x: prevState.x + 1
}
})
}
合成事件
在React的合成事件中,围绕浏览器原生事件,将不同浏览器的行为合并为一起,解决了兼容性问题。
在React中使用合成事件格式:onXxx={}
如何处理事件所对应函数的this指向问题
如下代码中,将函数主体封装为一个handle
函数,但是该函数是挂载到原型身上,且为普通函数。在React内部处理的时候,如果给合成事件绑定的是一个普通函数,那么当事件触发的时候,该函数内部的this指向为undefined。
handle() {
console.log(this); //undefined
}
render() {
return <button onClick={this.handle}>点击</button>
}
解决方法是修改this指向:onClick={this.handle.bind(this)}
或者直接将函数写成一个箭头函数
默认参数
当我们给合成事件绑定回调函数的时候,如果不指定传递的参数,那么默认会帮助我们传递一个事件对象作为参数(这一点和vue一致)。
在React的合成事件中,合成事件对象叫SyntheticBaseEvent
,当我们触发事件的同时,不指定参数的情况下,默认会传递合成事件对象。但是该合成事件对象和内置的事件对象不同,经过React的内部处理,优化了部分内容,。
handleTwo = (e) => {
console.log(this, e); //this执行无问题
}
<button onClick={this.handleTwo}>点击</button>
如果,我们需要传递自己的参数,且希望也能接受合成事件对象,那么该如何处理?
当使用bind
方法的时候,可以预处理this指向问题同时也可以传递参数。无论什么情况,都会将合成事件对象作为最后一个参数传递
handleThree = (x, y, e) => {
console.log(x, y, e);
}
<button onClick={this.handleThree.bind(null, 10, 20)}>点击</button>
注意不能写成如下格式,否则就会自动取执行函数了,不需要用户点击。这一点和vue不同,包括vue中传递参数了后就不会默认传递事件对象
<button onClick={this.handleThree(10, 20)}>点击</button>
合成事件原理
事件及事件委托
首先需要理解什么是捕获什么是冒泡
stopPropagation()和stopImmediatePropagation()区别
代码基于上图完成,这里只写出主要代码。在这段代码中给最inner容器的冒泡阶段添加两次事件监听(addEventListener允许给同一个元素绑定同样的事件执行不同的函数)。下面这段代码基于stopPropagation()
阻止事件的默认行为查看效果,发现会将同一个元素的同一个冒泡或捕获阶段处理两次。
inner.addEventListener('click', function (e) {
console.log('click inner 冒泡');
e.stopPropagation()
}, false)
inner.addEventListener('click', function (e) {
console.log('click inner 冒泡');
}, false)
同样的代码如果是基于stopImmediatePropagation()
完成阻止事件默认行为,结果却不同
事件委托
当一个页面中只有几个部分不需要事件,或者一个容器里存在多个按钮需要绑定事件,或者为动态创建的元素绑定事件的时候,都需要用到事件委托。
事件委托利用了事件的传播机制,实现一套事件绑定实现多个处理的方案。使用事件委托可以提高代码的运行性能。
但是如果想使用事件委托,首先当前操作的事件必须支持冒泡传播机制,像mouseenter/mouseleave等事件是没有冒泡传播机制的。
详解合成事件原理(React17往后)
将上面的代码应用到React的合成事件中,给元素绑定事件,其中带Capture
字段的是捕获。如下代码中,目前没有问题,点击内部元素的时候依次从外部捕获后再从内部冒泡出去。
但是我们在上面代码的基础上添加如下代码会发现,捕获阶段好像和预期的不一样,这是什么原因。
我们在componentDidMout
钩子中额外为两个容器inner和outer绑定原生事件,查看对应的输出
以下是执行顺序图
- React中的合成事件绝对不是基于
addEventListener
做事件绑定,而是基于事件委托处理 - 在React17及往后版本,都是委托给
root
根容器,捕获和冒泡都做了委托 - 在React17以前,都是委托给document容器,并且只有冒泡做了委托
- 在React17往后,默认都是委托给root容器的,如果自己写了原生js委托root容器,那么优先执行react自己的。(在上面的代码中,原生委托是存放在didMount钩子中完成,时间晚)
- 对于没有实现传播机制的事件,才是单独进行事件绑定,如mouseenter/mouseleave等
- 在React17往后,如果发现JSX元素中有onXxx的合成事件,那么不会立即给元素做事件绑定。,只是将绑定的方法赋给元素的相关属性。
- 最后会对根容器#root做事件委托,并且捕获和冒泡都进行处理。因为组件中所有的渲染的内容,最终都是存放到根容器中,这样子点击页面的任何元素,都会将根容器的行为触发。而且在给根容器绑定的方法中,会把之前给元素设置的onXxxx属性/方法在相应的时候执行。
基于上面的总结,写一段源代码,其中e.composedPath()
保存的是当前事件源依次冒泡到最外围window对象。
<div id="root">
<div id="outer">
<div id="inner"></div>
</div>
</div>
let root = document.querySelector("#root")
let outer = document.querySelector("#outer")
let inner = document.querySelector("#inner")
// 原生点击事件名为小写onclick
outer.onClick = () => { console.log("outer 冒泡 [合成]"); }
outer.onClickCapture = () => { console.log("outer 捕获 [合成]"); }
inner.onClick = () => { console.log("inner 冒泡 [合成]"); }
inner.onClickCapture = () => { console.log("inner 捕获 [合成]"); }
root.addEventListener('click', (e) => {
let path = e.composedPath(); //返回一个数组,记录当前触发事件源一直冒泡到window(原来叫e.path属性)
// 这里处理的捕获阶段,所以需要从外往里处理,反转数组
[...path].reverse().forEach((item) => {
let onClickCapture = item.onClickCapture
if (onClickCapture) onClickCapture()
})
}, true)
root.addEventListener('click', (e) => {
let path = e.composedPath()
// 冒泡阶段处理
path.forEach((item) => {
let onClick = item.onClick
if (onClick) onClick()
})
}, false)
如图,点击最里侧图片情况
合成事件的stopPropagation和原生e.stopPropagation区别
代码依旧是前面的,在这里调用传递的默认合成事件对象,调用其stopPropagation()
查看效果。在合成事件对象的阻止事件默认传播行为中,会发现不仅是原生事件或者是合成事件,都会被阻止传播(因为使用的是stopPropagation,所以不会阻止同类root的行为)。
<div className="inner"
onClick={(e) => {
console.log("inner 冒泡 [合成]");
e.stopPropagation(); //调用合成事件对象中的阻止事件传播行为
}}
。。。。
</div>
但是将上面的代码修改为如下。发现原生的stopPropagation只能阻止原生的行为,无法阻止合成事件的行为
但是使用原生的好处是可以使用:e.nativeEvent.stopImmediatePropagation()
这在合成事件对象中是没有的
e.nativeEvent.stopPropagation();//调用原生的阻止事件传播行为
React16版本合成事件原理
但是同样的代码在React16版本中会发现处理结果不一样(目前没有使用任何阻止事件传播)
在React16版本中,合成事件的处理机制是将事件委托给了documen元素,并且只做了冒泡阶段的委托,在委托的方法中,会将onXxx和onXxxxCapture合成事件属性执行。
同时在React16版本中,事件合成对象也和React18不同,以下是16版本中的合成事件对象,会发现每一个属性默认都是三个点,展开都为空null,这是为什么?
在react16中,关于合成事件对象内部处理,是基于事件对象池(事件缓存池)处理的。而在17往后则取掉了这一机制。在每一次事件触发的时候,都会将事件委托给documen或root元素,在委托的具体方法中,react会对内置的原生事件对象进行处理,转换为合成事件对象。而在16版本中,为了防止每次都是创建新的合成事件对象,于是设置了一个事件对象池(缓存池)。初始情况缓存池中的事件对象属性均为空,一旦事件触发,会获取相关的操作信息,同时将缓存池中的合成事件对象取出。把相关的操作信息赋值给合成事件对象的某些属性。待到本次操作结束后,会将合成事件中的信息清空后再次存入事件对象池中。
这样子就会导致一个问题,如果在合成事件对象中,存在一个异步操作,如定时器,在定时器中需要延迟时间后获取合成事件对象的相关属性,这个时候就获取不到,因为本次操作结束后,将合成事件对象的属性清空了。这个时候就需要借助合成事件对象提供的方法了:合成事件对象.persist()
将每次操作的信息保存到对象池中缓存
如何在移动端中使用合成事件
在移动端中使用click事件存在300ms的延迟,在移动端中,如果点击了一下屏幕,则会立即检测300ms内是否有第二次点击操作,如果有就是双击,否则就是单击。例如:在PC端中连续点击两下,会触发两下click,也会触发dbclick事件,而在移动端中只会触发dbclick。
移动端推出了单手指事件模型:touchstart
,touchmove
,touchend
事件模拟点击效果。
下面这段代码是是模拟移动端点击事件,消除了300ms延迟
- 在
onTouchStart
合成事件中做如下操作
// 移动端点击事件首先需要排除触摸滑动
//记录手指按下的坐标
touchstart = (e) => {
console.log(e);
let fingerStart = e.changedTouches[0] //记录了每次手指的位置
let x = fingerStart.pageX, y = fingerStart.pageY
this.touch = {
x,
y,
isMove: false //默认没有移动
}
}
2. onTouchMove
合成事件做,记录手指移动的偏移值,进行比较。(手指触摸移动的过程中一直触发)
// 手指移动,记录偏移值,进行判断是否是触摸移动
touchmove = (e) => {
let fingerStart = e.changedTouches[0]
let x = fingerStart.pageX, y = fingerStart.pageY //记录最新的值
let changeX = x - this.touch.x, changeY = y - this.touch.y //记录偏差
// 设置标志位表示为触摸移动非点击
// 因为存在负数情况,所以转换绝对值
if (Math.abs(changeX > 10 || Math.abs(changeY) > 10)) {
this.touch.isMove = true
}
}
onTouchEnd
合成事件,进行最后操作,判断是触摸移动还是点击操作
// 手指离开,判断是触摸移动还是点击操作
touchend = (e) => {
let { isMove } = this.touch
if (isMove) return //代表是触摸移动非点击
// 代表执行点击操作
console.log("点击");
}
如上所示操作就显得过于麻烦,可以借助插件库:yarn add fastclick
,该库底层也会进行转换位touch模式完成
import FastClick from 'fastclick';
FastClick.attach(document.body) //位于该容器中的click事件都会被处理
最后还一个更加简单的办法,就是每次添加如下代码会自动处理掉移动端click点击300ms延迟。react中会自动生成以下代码
<meta name="viewport" content="width=device-width, initial-scale=1" />
循环事件绑定
在react当中,当遇见合成事件的时候,都是委托给根容器进行处理的,所以在这里不需要担心性能问题,也不需要自己进行手动委托。(vue中不同,vue中的@click就是基于原生addEventListener绑定,非事件委托)
state = {
arr: [
{ id: 1, title: "体育" },
{ id: 2, title: "新闻" },
{ id: 3, title: "科技" },
]
}
handleLog = (itme) => {
console.log(`我的标题是:${itme.title}`);
}
render() {
let { arr } = this.state
return <div>
{arr.map(item => {
let { title, id } = item
return <span key={id}
style={{
padding: '5px 10px',
marginRight: "10px",
border: "1px solid #333",
cursor: 'pointer'
}}
onClick={this.handleLog.bind(null, item)}
>{title}</span>
})}
</div>
}
TODOLIST案例
搭建案例所需的模块。该案例采用antd适配react的版本完成ui搭建。antd能够自动按需引入
首先需要安装antd有关的模块:antd
,@ant-design/icons
(antd提供的icon图标)
之后我们在入口文件中引入,并配置相关信息
import { ConfigProvider } from 'antd' //引入设置全局antd配置
import zhCN from 'antd/locale/zh_CN'; //设置汉化,将部分英文提示的组件汉化
root.render(
// 设置配置信息
<ConfigProvider locale={zhCN}>
<Task></Task>
</ConfigProvider>
);
然后创建一个Task.jsx
文件编写如下基本代码测试
import React from "react";
import './Task.less'
import { TimePicker } from 'antd'
export class Task extends React.Component {
render() {
return <div className="task-box">
<TimePicker></TimePicker>
</div>
}
}
页面可以成功显示组件即可
配种跨域代理信息
const { createProxyMiddleware } = require('http-proxy-middleware')
module.exports = function (app) {
app.use(
createProxyMiddleware('/api', {
target: "http://127.0.0.1:9000",
changeOrigin: true,
ws: true,
pathRewrite: { '^/api': '' }
})
)
}
二次封装axios,同时设置默认配置
const http = axios.create({
baseURL: '/api',
timeout: 60000
});
http.defaults.transformRequest = data => {
if (_.isPlainObject(data)) data = qs.stringify(data);
return data;
};
http.interceptors.response.use(response => {
return response.data;
}, reason => {
// 网络层失败:统一提示
message.error('当前网络繁忙,请您稍后再试~');
return Promise.reject(reason);
});
export default http;
import http from '@/api/http.js'
export const getTaskList = (state = 0) => {
return http({
url: "/getTaskList",
method: "GET",
params: {
state
}
})
}
export const addTask = (task, time) => {
return http({
url: "/addTask",
method: "POST",
data: {
task,
time
}
})
}
export const removeTask = (id) => {
return http({
url: "/removeTask",
method: "GET",
params: {
id
}
})
}
export const completeTask = (id) => {
return http({
url: "/completeTask",
method: "GET",
params: {
id
}
})
}
Task完整代码
import React from "react";
import './Task.less'
import { Button, Tag, Table, Popconfirm, Modal, Form, Input, DatePicker, message } from 'antd'
import { getTaskList, addTask, removeTask, completeTask } from "../api";
import { flushSync } from "react-dom";
function addzero(time) {
let res = String(time)
// 一个字符的话前面补零
if (res.length < 2) {
return `0${res}`
}
return res
}
const formatTime = (time) => {
// 2023-11-29 10:00:00传入,返回11-29 10
let [, month, day, hour, minute] = time.match(/\d+/g)
return `${addzero(month)}月${addzero(day)}日${addzero(hour)}::${addzero(minute)}`
}
export class Task extends React.Component {
columns = [
{
title: "编号",
dataIndex: "id", //将每次遍历的值放在当前单元格
align: 'center',
width: '8%'
},
{
title: "任务描述",
dataIndex: "task",
align: 'center',
ellipsis: true,//自动省略号代替多余的内容
width: '40%'
},
{
title: "状态",
dataIndex: "state",
render: (text, record, index) => {
// 处理dataIndex无法处理的复杂数据显示
// text:在绑定了dataIndex值的情况下,为当前单元格的值,否则就是这一行的内容
// record:当前行的数据
// index:当前行的索引
// 返回值为当前单元格的最终内容
return +text === 1 ? "未完成" : "已完成"
},
align: 'center',
width: '10%'
},
{
title: '完成时间',
render: (_, record) => {
let { state, time, complete } = record
if (+state === 2) time = complete //已完成时间,可以只写一个return处理
return formatTime(time)
},
align: 'center',
width: "20%"
},
{
title: "操作",
render: (_, record) => {
let { id, state } = record
// 根据渲染按钮的内容
return <>
{+state !== 2 ? <Popconfirm title='确定完成吗?' onConfirm={this.handleComplete.bind(null, id)}>
<Button type="link">完成</Button>
</Popconfirm> : null}
<Popconfirm title='确定删除吗?' onConfirm={this.handleRemove.bind(null, id)}>
<Button type="link" >删除</Button>
</Popconfirm>
</>
},
align: 'center',
}
]
state = {
dataSource: [
//#region
// {
// id: 1,
// task: "今天天气不错今天天气不错今天天气不错今天天气不错今天天气不错",
// state: 1, //1代表未完成,2代表完成
// time: "2023-11-29 12:00:00", //预计完成时间
// complete: "2023-11-29 16:00:00" //实际完成时间
// },
// {
// id: 2,
// task: "呵呵哈哈哈",
// state: 2, //1代表未完成,2代表完成
// time: "2023-11-11 10:00:00", //预计完成时间
// complete: "2023-12-12 16:00:00" //实际完成时间
// }
//#endregion
],
loading: false,
isShow: false, //是否显示弹出框
confirmLoading: false, //是否显示确认框加载
selectedIndex: 0 //tag栏切换的当前索引
}
// 取消
handleCancel = () => {
this.setState({
isShow: false,
confirmLoading: false,
})
this.formIns.resetFields() //清空内容
}
// 发送请求
submit = async () => {
try {
await this.formIns.validateFields() //手动校验
let { task, time } = this.formIns.getFieldsValue() //获取表单的值
this.setState({
confirmLoading: true //设置提交加载loading
})
// time在antd被封装为基于dayjs的日期格式,可以使用dayjs格式化时间
time = time.format('YYYY-MM-DD HH:mm:ss')
let { code } = await addTask(task, time)
if (+code !== 0) {
message.error("操作失败")
} else {
this.queryData()
this.handleCancel()
message.success("添加成功")
}
} catch {
message.error("添加失败")
} finally {
// 无论成功或失败,都会执行关闭loading效果
this.setState({
confirmLoading: false
})
}
}
// 修改tag栏索引
changeIndex = (index) => {
// 点击自己的时候不做任何事情
if (this.state.selectedIndex === index) return
// 确保索引改变后获取数据
// 方法一
// this.setState({
// selectedIndex: index
// }, () => {
// this.queryData()
// })
// 方法二
flushSync(() => {
this.setState({
selectedIndex: index
})
})
this.queryData()
}
// 渲染table列表数据
queryData = async () => {
try {
// 每次获取数据就显示loading效果
this.setState({
loading: true
})
let { code, list } = await getTaskList(this.state.selectedIndex)
if (+code !== 0) list = []//0 正常 1 不正常
this.setState({
dataSource: list
})
} catch {
// 在拦截器中做了处理,发生错误会返回,可以在这里做处理,或不做处理
message.warning("获取数据失败")
} finally {
// 无论数据成功或失败都取消loading效果
this.setState({
loading: false
})
}
}
// 处理删除
handleRemove = async (id) => {
let { code } = await removeTask(id)
if (+code !== 0) {
message.warning("删除失败")
} else {
this.queryData()
message.success("删除成功")
}
}
// 处理完成
handleComplete = async (id) => {
let { code } = await completeTask(id)
if (+code !== 0) {
message.warning("完成失败")
} else {
this.queryData()
message.success("设置成功")
}
}
render() {
let { dataSource, loading, isShow, confirmLoading, selectedIndex } = this.state
return <div className="task-box">
<div className="header">
<h2>TODOLIST 任务表</h2>
<Button onClick={() => { this.setState({ isShow: true }) }}>添加任务</Button>
</div>
<div className="tag">
{/* 通过循环绑定可以节省重复代码 */}
{["全部", "未完成", "已完成"].map((item, index) => {
return <Tag
key={index}
color={selectedIndex === index ? "blue" : ""}
onClick={this.changeIndex.bind(null, index)}>
{item}
</Tag>
})}
</div>
{/* rowKey每行的唯一主键,必须有 */}
<Table dataSource={dataSource} columns={this.columns} loading={loading} rowKey='id' pagination={false}></Table>
{/* 弹出对话框 */}
<Modal
title="添加任务"
open={isShow}
confirmLoading={confirmLoading}
keyboard={false}
maskClosable={false}
okText='提交'
onCancel={this.handleCancel}
onOk={this.submit}
>
<Form ref={x => this.formIns = x} layout="vertical" initialValues={{ task: "", time: "" }}>
<Form.Item
label='任务描述'
name="task" //代表将最后文本框的内容存入name字段
validateTrigger='onBlur' //设置校验时机
rules={[
{ required: true, message: "请输入内容" },
{ min: 6, message: "请输入至少6个字符" }
]}
>
<Input.TextArea rows={4} maxLength={200} showCount={true} />
</Form.Item>
<Form.Item
label='预计完成时间'
name="time"
validateTrigger='onBlur'
rules={[
{ required: true, message: "请选择时间" }
]}>
<DatePicker showTime ></DatePicker>
</Form.Item>
</Form>
</Modal>
</div>
}
componentDidMount() {
// 初次渲染
this.queryData()
}
}