React系列(八)——React进阶知识点拓展

前言

在之前的学习中,我们已经知道了React组件的定义和使用,路由配置,组件通信等其他方法的React知识点,那么本篇文章将针对React的一些进阶知识点以及React16.8之后的一些新特性进行讲解。希望对各位有所帮助。


一、setState
(一)更新状态的2种写法

在之前更新state的时候,我们常常使用的是this.setState()方法进行状态的更新,传入的参数是一个{key:value}对象,用于对state中指定的值进行更新。比如下面更新count值的写法:

import React, { Component } from 'react'
export default class Demo extends Component {
    state = {count:0}
    increment = ()=>{
        const {count} = this.state;
        this.setState({count:count+1}
    }
    render() {
        return (
            <div>
                当前的数值为: {this.state.count}
                <br/>
                <button onClick={this.increment}>点我+1</button>
            </div>
        )
    }
}

上面的写法固然不错,但实际上setState函数传入的参数不仅可以是一个对象,还可以是一个函数,在函数中我们可以接收到入参state,然后直接进行状态的更新。我们之前之间传入一个对象,其实可以看成是这种函数写法的简写方式。

export default class Demo extends Component {
    state = {count:0}
    increment = ()=>{
        this.setState(state=>({count:state.count+1}));
    }

    render() {
        return (
            <div>
                当前的数值为: {this.state.count}
                <br/>
                <button onClick={this.increment}>点我+1</button>
            </div>
        )
    }
}

值得一提的是,虽然这种写法自带的state可以方便我们后续操作,但实际上我们还是用的比较少,毕竟直接传入对象相对来说更加简单~

(二)指定setState的回调函数

setState函数除了可以传入要更新的状态之外, 还可以接收第二个参数,更新完状态后的回调函数。我们可以先看下面这个案例:

export default class Demo extends Component {
    state = {count:0}
    increment = ()=>{
        this.setState(state=>({count:state.count+1}));
        console.log(this.state.count)
    }

    render() {
        return (
            <div>
                当前的数值为: {this.state.count}
                <br/>
                <button onClick={this.increment}>点我+1</button>
            </div>
        )
    }
}

按道理说,当点击按钮时,会对count的值进行+1操作,并打印出更新完状态的最新值,但实际的结果是:

setState演示用例

我们可以看到,此时打印的结果却是count未更新前的值,原因是setState()虽然是同步方法,但具体更新状态的方法却是异步的,如果想要更新最新状态值做某些操作的话,就需要我们把对应的操作放在回调函数中。

export default class Demo extends Component {
    state = {count:0}
    increment = ()=>{
        this.setState(state=>({count:state.count+1}),()=>{
            console.log(this.state.count)
        });
    }

    render() {
        return (
            <div>
                当前的数值为: {this.state.count}
                <br/>
                <button onClick={this.increment}>点我+1</button>
            </div>
        )
    }
}

使用回调函数之后,我们可以看到,打印的结果就是我们想要的了:


setState演示用例2

二、懒加载:lazyLoad

实际上当页面导航栏的对应着多个组件时,即使用户只需要使用其中的1个或者几个,但页面在加载的时候还是会将所有组件都加载出来,这其实是不太合理的。组件的懒加载就可以帮助我们解决这个问题。

同时,懒加载需要配合Suspense 一起使用。这样做的原因很简单:一次性把组件加载出来,虽然一开始全部加载的时候速度会比较慢,但后面打开的速度就会很快。而懒加载的话,由于还要再发一次网络请求,所以react会要求我们在等待结果返回的过程中,定义一个类似于Loading提示的组件,以此来提高用户体验(否则用户看到的将是白屏)

未使用懒加载之前,如上述所说,页面就可以把对应的组件都加载好了(我们可以直接在main.chunk.js文件中找到对应组件的代码):

懒加载演示用例1

而实际上,React本身就提供了lazy函数供我们对组件进行懒加载配置:

步骤1: 引入 lazy函数和Susqense组件
import React, { Component,lazy,Suspense} from 'react'
步骤2:使用lazy函数,把需要懒加载的组件作为参数传入
const About = lazy(()=>import('./About'))
const Home = lazy(()=>import('./Home'))
步骤三:使用Suspense组件,对路由进行包裹
 <Suspense>
    <Route path="/about" component={About} />
   <Route path="/home" component={Home} />
</Suspense>
步骤四:定义Loading组件

在开头我们就解释过,使用懒加载的话只有当每次用户点击后,才会真正发起获取组件的网路请求,Loading组件存在的意义就是在请求的响应回来之前,页面上先显示Loading组件,不让页面出现白屏的效果。

import React, { Component } from 'react'

export default class Loading extends Component {
    render() {
        return (
            <div>
                <h1 style={{backgroundColor: "gray",color:'orange'}}>Loading...</h1>
            </div>
        )
    }
}
步骤五:引入组件并将Loading组件作为回调传入Suspense组件中:
 <Suspense fallback={<Loading/>}>
   <Route path="/about" component={About} />
   <Route path="/home" component={Home} />
</Suspense>
最终,我们可以在浏览器的控制台上查看效果:


lazyLoading演示用例2

当使用懒加载之后,只有当我们点击的时候,才会发起新的请求,这里的 0.chunk.js就是请求回来的结果,如果我们点进去看的话,就可以在其中发现我们的组件代码了。

三、Fragment标签的使用

我们知道,使用JSX语法定义组件时,会要求我们返回唯一一个根标签,但是这样有一个缺点:让我们的页面嵌套层次过深,我们可以使用 Fragment 或者 <></> 这种空标签来避免上述问题。需要注意的是,虽然二者都可以解决根标签的问题,但是在使用上二者还是有些不同的,Fragment标签中可以传递参数key,而空标签是不允许传递参数的。

我们可以看看未使用Fragment标签时,页面上的元素:

App组件
export default class App extends Component {
    render() {
        return (
            <div>
                <Demo/>
            </div>
        )
    }
}
Demo组件
export default class Demo extends Component {
    render() {
        return (
            <div>
                <input /><br />
                <input />
            </div>
        )
    }
}


Fragment演示用例1

我们可以看到,虽然页面上的元素正常显示,但还是会嵌套在不需要使用到的div标签中,我们可以使用Fragment标签或者空标签来解决这个问题:

App组件:
import React, { Component,Fragment } from 'react'
import Demo from './4_fragement'

export default class App extends Component {
    render() {
        return (
            <Fragment>
                <Demo/>
            </Fragment>
        )
    }
}
Demo组件
import React, { Component, Fragment } from 'react'

export default class Demo extends Component {
    render() {
        return (
            <>
                <input /><br />
                <input />
            </>
        )
    }
}


Fragment演示用例2

我们可以看到,使用<Fragment>或者使用<></>标签都有着使组件不需要有一个真实DOM标签的功能,但实际上二者在使用上还是略有不同的,Fragment标签可以接收key属性的参数,而空标签是不可以接收属性参数的。

四、实例对象Context属性的应用

我们知道,使用props属性自然可以将方法或者数据从父组件上逐层传递到子组件中。但是当组件间的嵌套关系比较深的时候,这时再通过props来传递值就会很麻烦,针对这种情况,我们可以考虑用Context来解决这个问题:Context其实是组件实例对象上的一个属性,利用ProviderConsumer,我们可以实现值的传递。

假如目前有A、B、C三个组件,B是A的子组件,C是B的子组件:

export default class A extends Component {
    state = { name: '小明', age: 18 }
    render() {
        const {name,age} = this.state;
        return (
            <div className="parent">
                我是A组件
                <br />
                <h4>姓名是:{this.state.name},年龄是:{this.state.age}</h4>
                    <B />
            </div>
        )
    }
}
class B extends Component {
    render() {
        return (
            <div className="child">
                我是B组件
                <C />
            </div>
        )
    }
}
class C extends Component {
    render() {
        return (
            <div className="grand">
                我是C组件
                <h4>
                    从A组件获取到的值为
                </h4>
            </div>
        )
    }
}

如果C组件想要获取A组件的值,我们可以通过以下步骤来实现:

步骤1:通过React.createContext(),获取ProviderConsumer
const MyContext = React.createContext();
const { Provider, Consumer } = MyContext;
步骤2:在A组件中使用Provider包裹住子组件
export default class A extends Component {
    state = { name: '小明', age: 18 }
    render() {
        const {name,age} = this.state;
        return (
            <div className="parent">
                我是A组件
                <br />
                <h4>姓名是:{this.state.name},年龄是:{this.state.age}</h4>
                {/* 使用Provider包裹住子组件,有需要的后代组件通过Consumer标签即可获取,需要注意,value传入的值是一个对象 */}
                <Provider value={{name,age}}>
                    <B />
                </Provider>
            </div>
        )
    }
}
步骤3:在C组件中通过Consumer标签获取A组件的值
class C extends Component {
    render() {
        return (
            <div className="grand">
                我是C组件
                <h4>
                    从A组件获取到的值为
                    <Consumer>
                        {value => `姓名为:${value.name},年龄为:${value.age}`}
                    </Consumer>
                </h4>
            </div>
        )
    }
}
五、PureComponent的使用

对于子组件来说,一旦父组件重新render,那么其也会跟着一起重新执行一次render方法,即使子组件并没有使用到父组件传递的任何属性。或者当父组件空调用一次setState方法,那么此时state并没有真的改变,但是子组件还是要重新执行一次render,这其实是不太合理的。

export default class Parent extends Component {
    state = {carName:'哈雷摩托',stus:['小明','小红']}
    changeCar = ()=>{
        //this.setState({carName:'特斯拉'})
        this.setState({})
    }
    addStus = ()=>{
        this.setState({stus:['小明',...this.state.stus]})
    }
    render() {
        console.log('父组件的render方法调用了')
        const {carName,stus} = this.state;
        return (
            <div className="parent">
                <h3>父组件的车名为:
                    {carName}
                </h3>
                <h4>
                    {stus}
                    <button onClick={this.addStus}>点我加人</button>
                </h4>
                <button style={{marginBottom: '5px'}} onClick={this.changeCar}>点击换车</button>
                <Child carName={carName}></Child>
            </div>
        )
    }
}
class Child extends Component {
    render() {
        console.log('子组件的render方法调用了')
        return (
            <div className="child">
                <h4>接收到父组件的车名为:
                    {this.props.carName}
                </h4>
            </div>
        )
    }
}


pureComponent演示用例1

解决这个问题的方法有2个:

(1)手动写 componentShouldUpdate方法,对状态是否改变进行判断

比如我们可以通过下面这个钩子来避免组件发生无效的render:

    shouldComponentUpdate(nextProps,nextState){
        return nextState.carName !== this.state.carName 
    }
(2)利用PureComponent组件,里面帮我们重写了shouldComponentUpdate方法

我们可以利用组件内componentShouldUpdate这个钩子,来根据实际需要判断是否执行render,但如果说状态比较多,就需要我们手动写很多判断条件,比较麻烦。我们更加常用的是直接继承和使用PureComponent组件。

import React, { Component,PureComponent } from 'react'
import './index.css'

export default class Parent extends PureComponent {
...
}

class Child extends PureComponent {
 ...
}


pureComponent案例演示2

六、render props属性的应用

对于关系明确的父子组件来说,我们可以比较简单的从组件的嵌套关系来判断组件间的父子关系。但是有时候我们可能并不能确定父组件中的子组件具体是哪一个,而是等到具体调用的时候才知道。

比如说有A、B、C三个组件,且我们知道A是B的父组件,B是C的父组件,那么对应的代码为:

export default class A extends Component {
    render() {
        return (
            <div className="parent">
                <B/>
            </div>
        )
    }
}

class B extends Component {
    render() {
        return (
            <div className="parent">
                <C/>
            </div>
        )
    }
}

class C extends Component {
    render() {
        return (
            <div className="parent">
            </div>
        )
    }
}

但有时候我们可能只知道B组件内需要传递一个组件,但具体是哪个组件却不能确定,只能等到具体使用的时候才能确定,这个时候就需要用props的方法来解决了。

Vue中,这种问题的解决方案被称为插槽技术

React中,我们通常使用children props以及render props属性来解决这类问题。其中,children props虽然可以完成组件的传递,但是却不能实现父子组件的数据传递而render props则可以解决这类问题

export default class A extends Component {
    render() {
        return (
            <div className="parent">
                <B>
                  <C/>
                </B>
            </div>
        )
    }
}

class B extends Component {
    render() {
        return (
            <div className="parent">
            {this.props.children}
            </div>
        )
    }
}

class C extends Component {
    render() {
        return (
            <div className="parent">
            </div>
        )
    }
}

用上面这种方式,我们可以通过 通过组件标签体的方式传入组件,在对应的组件内使用{this.props.children}进行展示。这样就使得组件的灵活性大大提高,但这种方式的缺点也很明显,C组件虽然是B组件的子组件,但是却不能获取B组件的值。这时,就需要用到第二种方式:使用render props进行参数的传递。

export default class A extends Component {
    render() {
        return (
            <div className="parent">
                <B render={name => <C name={name} />}/>
            </div>
        )
    }
}

class B extends Component {
    state = {name:'小明'}
    render() {
        return (
            <div className="parent">
            {this.props.render(name)}
            </div>
        )
    }
}

class C extends Component {
    render() {
        return (
            <div className="parent">
            <h4>接收到的父组件参数是: {this.props.name}</h4>
            </div>
        )
    }
}

通过上述这种方式,我们就可以把父组件的参数传递给子组件了,需要注意的是本质上这里使用的还是组件的props属性,所以props不一定名字要叫render,只是很多时候我们约定俗成地将这种传递值的方式的props值设为render

七、错误边界:ErrorBoundary

当子组件发生错误时,我们会希望子组件的错误不影响到父组件其他功能的使用,这时候我们就可以在父组件上设置错误边界,来防止由于子组件错误而影响到整个页面不能使用的情况出现。解决方法是利用react的一个生命周期 getDerivedStateFromError

我们先来看一下案例,现在存在有Parent组件和Child组件:

Parent组件:
export default class Parent extends Component {
    state = {hasError:''}
    static getDerivedStateFromError(err){
        return {hasError: err}
    }
    render() {
        return (
            <div>
                <h2>我是父组件</h2>
                 <Child/>
            </div>
        )
    }
}
Child组件:
export default class Child extends Component {
    state = {
        person:[
            {name:'小明',age:10},
            {name:'大明',age:31},
            {name:'小红',age:16},
        ]
    }
    render() {
        return (
            <div>
                <h2>我是子组件</h2>
                {
                    this.state.person.map(ele=>{
                    return <li>{ele.name}--{ele.age}</li>
                    })
                }
            </div>
        )
    }
}

页面上显示的效果如下:


errorBoundary演示用例1

组件中state的数据往往是通过请求后台返回回来的,如果说后台发生意外,传来的数据格式有问题,如果没有合理的解决这个问题,页面可能就直接用不了了。

export default class Child extends Component {
    state = {person: 'hh'}
    render() {
        return (
            <div>
                <h2>我是子组件</h2>
                {
                    this.state.person.map(ele=>{
                    return <li>{ele.name}--{ele.age}</li>
                    })
                }
            </div>
        )
    }
}


errorBoundary演示用例2

由于返回的数据有问题,使得原先的代码逻辑判断发生了错误,遇到这种情况,我们会希望把错误缩小到当前组件上,而不会对父组件以及其他组件的使用造成干扰。这时,我们就可以使用 getDerivedStateFromError来对子组件的错误进行捕获和限制。

给父组件加上钩子
export default class Parent extends Component {

    state = {hasError:''}

    static getDerivedStateFromError(err){
        return {hasError: err}
    }

    render() {
        return (
            <div>
                <h2>我是父组件</h2>
                {this.state.hasError ? '网络不通畅,请稍后再试...':<Child/>}
            </div>
        )
    }
}

getDerivedStateFromError会捕获到子组件传递的错误,我们可以利用这个特性,对组件的显示进行判断。

需要注意的是,要想更好的观察错误边界这个效果,最好是打包后在服务器上面看效果。(在开发模式下,错误边界不起作用)。

errorBoundary演示用例3

我们可以看到,此时虽然子组件发生错误,但还是不影响页面的正常使用。另外,错误边界只能捕获后代组件生命周期产生的错误,不能捕获自己组件产生的错误和其他组件在合成事件、定时器中产生的错误。

八、Hook

我们知道,函数式组件由于无法使用this,所以也就不能像类式组件一样使用state和ref等属性。但是在React16.8 之后,官方推出了3个hook,使得函数式组件也可以像类式组件一般使用对应的属性以及常用的生命周期函数:

(1). State Hook: React.useState(): 可以使用state
(2). Effect Hook: React.useEffect(): 可以模拟生命周期钩子
(3). Ref Hook: React.useRef(): 可以使用ref
(一)State Hook 钩子
export default function Demo() {
    function add(){
      
      }
    return (
        <div>
            <h2>当前总和为:???</h2>
            <button onClick={add}>点我+1</button>
        </div>
    )
}

我们想要赋予给函数式组件Demo一个state属性count,此时我们就可以这样实现:

使用React.useState()方法,我们可以传入对应属性的初始值,方法会返回一个数组,第一个参数是当前属性的值,第二个参数是修改这个属性的方法,我们可以在后续使用的时候,具体来设置方法的逻辑。

export default function Demo() {
    const [count,setCount] = React.useState(0);
    function add(){
      // setCount(count+1);  setCount的第一种写法
        setCount(count => count+1)
      }
    return (
        <div>
            <h2>当前总和为:???</h2>
            <button onClick={add}>点我+1</button>
        </div>
    )
}
(二)Effect Hook

Effect Hook 可以让你在函数组件中执行副作用操作(用于模拟类组件中的生命周期钩子)。Effect Hook算是三个Hook之中较为复杂的一个了,我们可以用它来模拟componentDidMountcomponentDidUpdatecomponentWillUnmount这三个生命周期函数。

我们现在在上个案例的基础上再新增一个功能:每隔1秒钟,将count的值进行+1,如果是放在类式组件中,我们很容易就会想到使用componentDidMount来调用一个定时器,但是在函数式组件中,由于没有生命周期钩子函数,所以我们需要用React.useEffect()来实现我们的代码。

export default function Demo() {
    const [count,setCount] = React.useState(0);
    // 使用 Ref Hook,用法和React.createRef()一样
    const myRef = React.useRef();
    React.useEffect(()=>{
        let timer = setInterval(()=>{
            setCount(count => count+1);
        },1000)
    },[])
    function add() {
        // setCount(count+1);  setCount的第一种写法
        setCount(count => count+1)
    }

    return (
        <div>
            <input type="text" ref={myRef}/>
            <h2>当前总和为:{count}</h2>
            <button onClick={add}>点我+1</button>
        </div>
    )
}

userEffect需要传入2个参数,第一个相当于是组件加载好后执行的函数,第二个是指定具体监测的state

当第二个参数为空数组时,表示不监测任何一个state,即组件只加载一次,相当于是 componentDidMount钩子, 当第二个参数传入真实的state时,那么一旦监测的state状态发生改变,那么第一个函数就会调用,此时相当于是componentDidUpdate钩子 。同时,useEffect的return需要传入一个方法,在组件销毁的时候调用,相当于是 componentWillUnmount钩子。

我们现在再新增一个需求,利用useEffect的return方法,在卸载组件后停止运行定时器:

export default function Demo() {
    const [count,setCount] = React.useState(0);
    // 使用 Ref Hook,用法和React.createRef()一样
    const myRef = React.useRef();
    React.useEffect(()=>{
        let timer = setInterval(()=>{
            setCount(count => count+1);
        },1000)
    return ()=>{
           clearInterval(timer)
        }
    },[])
    function add() {
        // setCount(count+1);  setCount的第一种写法
        setCount(count => count+1)
    }

    function unmount(){
        ReactDOM.unmountComponentAtNode(document.getElementById('root'));
    }

    return (
        <div>
            <input type="text" ref={myRef}/>
            <h2>当前总和为:{count}</h2>
            <button onClick={add}>点我+1</button>
            <button onClick={unmount}>点击卸载组件</button>
        </div>
    )
}

实际上,在点击卸载组件的按钮时,就会触发useEffect函数中return中定义的函数,成功停止计时器。

(三)Ref Hook

Ref Hook可以在函数组件中存储/查找组件内的标签或任意其它数据,其实也就是赋予函数式组件使用类似于ref属性的功能。在上面的案例的基础上,我们在添加一个需求,新增一个文本框和按钮,当点击按钮时,把文本框的内容进行弹框显示:

    const [count,setCount] = React.useState(0);
    // 使用 Ref Hook,用法和React.createRef()一样
    const myRef = React.useRef();
    function add() {
        // setCount(count+1);  setCount的第一种写法
        setCount(count => count+1)
    }
    function show(){
        alert(myRef.current.value);
    }
    return (
        <div>
            <input type="text" ref={myRef}/>
            <h2>当前总和为:{count}</h2>
            <button onClick={add}>点我+1</button>
            <button onClick={show}>点击提示文本框内容</button><br/>
        </div>
    )
}
说在最后:

本篇文章的代码已经放在了码云上,有需要的可以通过下面的链接下载:
https://gitee.com/moutory/react-extension
最后编辑于:2024-12-10 21:53:25
© 著作权归作者所有,转载或内容合作请联系作者

喜欢的朋友记得点赞、收藏、关注哦!!!

相关推荐
来吧~7 分钟前
vue3使用video-player实现视频播放(可拖动视频窗口、调整大小)
前端·vue.js·音视频
呆呆小雅25 分钟前
二、创建第一个VUE项目
前端·javascript·vue.js
AI人H哥会Java29 分钟前
【Spring】基于XML的Spring容器配置——<bean>标签与属性解析
java·开发语言·spring boot·后端·架构
fnd_LN29 分钟前
Linux文件目录 --- mkdir命令,创建目录,多级目录,设置目录权限
linux·运维·服务器
计算机学长felix33 分钟前
基于SpringBoot的“大学生社团活动平台”的设计与实现(源码+数据库+文档+PPT)
数据库·spring boot·后端
sin220133 分钟前
springboot数据校验报错
spring boot·后端·python
Fighting_p35 分钟前
【记录】列表自动滚动轮播功能实现
前端·javascript·vue.js
会飞的土拨鼠呀39 分钟前
Flannel是什么,如何安装Flannel
运维·云原生·kubernetes
木与子不厌41 分钟前
微服务自定义过滤器
运维·数据库·微服务
Domain-zhuo1 小时前
Git和SVN有什么区别?
前端·javascript·vue.js·git·svn·webpack·node.js