React组件的使用总结

React组件的使用总结React 通过组件化的思想,将界面拆分成一个个可以复用的模块,每一个模块就是一个 React 组件。学好如何使用组件将会是你学好 React 不可或缺的一部分。本文是我在实战中使用 React 与学习 React 时总结的一些使用 React 组件的经验,目的在于帮助大家在使…

前言

React 通过组件化的思想,将界面拆分成一个个可以复用的模块,每一个模块就是一个 React 组件。学好如何使用组件将会是你学好 React 不可或缺的一部分。本文是我在实战中使用 React 与学习 React 时总结的一些使用 React 组件的经验,目的在于帮助大家在使用React时避开一些由于对React的相关机制理解不够深入而以为的“坑”。

1. 对 React 组件的理解

组件是一个具备UI 描述UI 数据的完整体。
它的数据结构是类或函数,返回 React 元素。 简单来说,可以把组件看成是一个函数,输入的是 props 和 state,输出的是组件的 UI(UI 描述)。

2. 创建 React 组件

组件的创建方式有两种:

  • 使用 function(函数组件)(一般用于创建无状态组件)
    注意:目前React 16.8版本推出了React Hook,使得用function创建的组件也可以拥有自己的内部状态了。
  • 使用 ES6 的 class(类组件)

2.1 通过 class 创建 React 组件

必要条件

  1. class 必须继承自 React.Component
  2. 内部必须定义 render 方法,必须返回代表该组件的 React 元素
import React extends "react"

class Component extends React.Component {
    render(){
        return (
            <div>我是一个react组件<div>
        )
    }
}

以上代码可以创建一个简单的 React 组件

2.2 props、state 与普通属性

props是一个结构简单的对象,是组件作为 JSX 使用时的属性所组成,是一个组件对外的接口。

import React from "react"

class ParentComponent extends React.Component {
    render(){
        return (
            <ChildrenComponent value="aaa" />
        )
    }
}
class ChildrenComponent extends React.Component {
    render(){
        console.log(this.props.value) // 输出aaa
        return (
            <div>我是一个react组件</div>
        )
    }
}

props 的特性

  1. 一般来说,与渲染有关(当然也可以传递函数,这个后面会说到)
  2. 从父组件中传入
  3. 只读(需要注意的是如果传入的 props 属于引用类型的数据,那么其实是可以修改其里面的属性值的,但是一般来说开发者必须遵守这个只读的约定,避免产生不可预测的问题)

React 组件间的通信是遵循单向数据流的思想,实现是通过 props。具体表现为数据主要从父组件传递给子组件。如果父组件的传递给子组件的某个 props 改变了,子组件将会重新渲染。在子组件中可以通过 this.props 访问到父组件中传进来的数据。根据约定,子组件中不能直接使用 this.props 修改 props。一般来说,我们可以在父组件中定义一个方法用于修改某个状态,然后把该函数通过 props 传入子组件,这样子组件就可以通过调用该函数修改 props。

import React from "react"

class ParentComponent extends React.Component {
    constructor(props){
        super(props)
        this.state = {
           isShow: true
        }

        // 该方法在父组件中定义,通过props传入子组件中调用。由于该方法内部使用了this,为了避免this的指向不明,需要手动绑定一下this
        this.updateIsShow = this.updateIsShow.bind(this)
    }

    updateIsShow(){
        this.setState({
            isShow: !this.state.isShow
        })
    }

    render(){
        const {isShow} = this.state
        return (
            <div> {isShow ? <div>实例内容</div> : null} <Button isShow={isShow} updateIsShow={this.updateIsShow}/> </div>
        )
    }
}
class Button extends React.Component {
    constructor(props){
        super(props)

        this.clickHandler = this.clickHandler.bind(this)
    }

    clickHandler(e){
        this.props.updateIsShow()
    }

    render(){
        const {isShow} = this.props
        return (
            <button onClick={this.clickHandler}>
                {isShow ? "隐藏" : "显示"}
            </button>
        )
    }
}

state是组件的内部状态,代表的是组件 UI 呈现的完整状态集。state 中所有的状态都应反映组件 UI 的变化。

state 的两种使用场景:

  1. 用于渲染组件时用到的数据的来源
  2. 用作组件展示形式的判断依据
import React from "react"

class ParentComponent extends React.Component {
    construtor(props){
        super(props)
        this.state = {
            text: "我是父组件",
            isShow: true
        }
    }
    render(){
        const { text, isShow } = this.state
        return (
            <div> <div>{text}</div> { isShow && <ChildrenComponent value="aaa" /> } </div>
        )
    }
}
class ChildrenComponent extends React.Component {
    render(){
        console.log(this.props.value) // 输出aaa
        return (
            <div>我是一个react组件<div>
        )
    }
}

由以上代码可以看到 state 的两种使用场景,text 是作为组件渲染的数据来源,isShow 是作为 ChildrenComponent 组件是否渲染的判断依据

普通属性是 class 中 this 上的属性,也就是组件实例的属性。与渲染无关的变量都可以定义成普通属性。

2.3 setState的作用

在组件中我们通过调用 setState()来更新 state,并通知 React 重新渲染视图。

setState()通过两种传参的方式来更新 state

(1) 传入一个对象和一个可选的函数

import React from "react"

class ParentComponent extends React.Component {
    construtor(props){
        super(props)
        this.state = {
            text: "我是父组件",
            isShow: true
        }
        this.clickHandler = this.clickHandler.bind(this)
    }

    clickHandler(e){
        // 假设点击的事件处理程序执行前,this.state.isShow 为 true
        this.setState({
            isShow: !this.state.isShow
        }, () => {
            console.log(this.state.isShow) // false
        })
        console.log(this.state.isShow) // true
    }

    render(){
        const { text, isShow } = this.state
        return (
            <div>
                <div>{text}</div>
                { isShow && <ChildrenComponent value="aaa" /> }
                <button onClick={this.clickHandler}>
                    {isShow ? "隐藏ChildrenComponent" : "显示ChildrenComponent"}
                <button>
            </div>
        )
    }
}
class ChildrenComponent extends React.Component {
    render(){
        console.log(this.props.value) // 输出aaa
        return (
            <div>我是一个React组件<div>
        )
    }
}

通过以上代码可以看到通过点击按钮触发绑定在按钮上的事件处理程序的调用,从而更新 state。通过给 setState 方法传入一个对象,React 会把这个对象通过 Object.assign()的方式合并到当前 state 中从而更新 state。需要注意的是如果你需要使用更新后的 state,不能直接在 setState 方法调用后直接使用 this.state 访问更新后的 state,这是由于 React 内部优化了 setState 的执行时机而导致你在 setState 方法调用后通过 this.state 直接访问的 state 仍然是更新前的 state。因此,如果你实在有一些行为需要依赖更新后的 state 的话,可以通过传入第二个参数(一个回调函数),并把你的这部分代码写到该函数内即可。因为在此回调函数内使用 this.state 访问到的是更新后的 state。这样看,setState 方法看起来像一个异步方法。

(2) 传入一个函数和一个可选函数
setState((prevState, props) => ({}), () => {})
这种调用方式是通过传入一个回调函数作为第一个参数,且该回调函数必须返回一个对象用于更新 state。(React 会把这个对象通过 Object.assign()的方式合并到当前 state 中,从而更新 state)。该回调函数可以传入两个参数,第一个为 prevState, 第二个为 props。理解这两个参数之前,我先介绍一下 setState 的执行机制。

前面说到 setState 方法像一个异步方法,而实际上 setState 方法并没有直接修改 state,它只是把要修改的状态放入一个队列中,最后由 React 统一把这些修改与当前 state 合并。setState 的更新过程实际上是把当前修改的状态跟当前 state 合并的过程。所以说 setState 是一个异步方法并不准确,只是 state 更新使用的这种批处理机制从而给人一种 setState 方法是一个异步方法的假象。

那么说这个 setState 的执行机制跟理解 setState 方法的第一个参数传入函数的两个参数的有什么关系呢?先别着急,让我们来一起看看以下场景。

我们在一次更新的过程中多次调用了 setState 更新同一个状态,为了简单明了,我就把两次 setState 的调用放在同一函数内了。那么实际开发的情况可能是在一次更新的过程执行了多个函数,且这些函数均有调用 setState。
我们想要在一次点击事件中更新 num1 和 num2,期望结果为
num1:1
num2:1

import React from "react"

class ParentComponent extends React.Component {
    construtor(props){
        super(props)
        this.state = {
            count: {
                num1: 0,
                num2: 0
            }
        }
        this.clickHandler = this.clickHandler.bind(this)
    }

    clickHandler(e){
        this.setState({
            count: {
                ...this.state.count,
                num1: this.state.count.num1 + 1,
            }
        })
        this.setState({
            count: {
                ...this.state.count,
                num2: this.state.count.num2 + 1,
            }
        })
    }

    render(){
        const { count } = this.state
        return (
            <div>
                <div>num1:{count.num1}</div>
                <div>num2:{count.num2}</div>
                <button onClick={this.clickHandler}>
                    add
                <button>
            </div>
        )
    }
}

实际结果为:
num1:0
num2:1

使用第一种 setState 的传参方式的实际结果与我们的期望不符,那么我们尝试第二种使用传入函数的方式调用 setState 方法会怎么样呢?

import React from "react"

class ParentComponent extends React.Component {
    construtor(props){
        super(props)
        this.state = {
            count: {
                num1: 0,
                num2: 0
            }
        }
        this.clickHandler = this.clickHandler.bind(this)
    }

    clickHandler(e){
        this.setState((prevState) => ({
            count: {
                ...prevState.count,
                num1: prevState.count.num1 + 1,
            }
        }))
        this.setState((prevState) => ({
            count: {
                ...prevState.count,
                num2: prevState.count.num2 + 1,
            }
        }))
    }

    render(){
        const { count } = this.state
        return (
            <div>
                <div>num1:{count.num1}</div>
                <div>num2:{count.num2}</div>
                <button onClick={this.clickHandler}>
                    add
                <button>
            </div>
        )
    }
}

实际结果为:
num1:1
num2:1

很明显第二中传参方式的调用得到了我们想要的结果,那么为什么会产生两个不同的结果呢?
解答这个问题的话又要进一步解释 setState 的机制了。用以上第一种方式更新 state 的过程类似于以下代码

Object.assign(
    this.state,
    {
        count: {
            ...this.state.count,
            num1: 1
        }
    },
    {
        count: {
            ...this.state.count,
            num2: 1
        }
    })

React 对多次调用 setState 方法传入的对象进行了一次性合并的操作,显然最后一次的修改会把前面的覆盖。与传入对象不同,React 对传入函数做了特殊处理,会把在此之前的修改先合并完成后的 state 当作参数传入函数中,因此不会覆盖修改。

详细可查阅:探究 setState 执行机制

3. React 组件的事件

相信大家在最初接触 React 的时候使用事件处理程序时都会遇到一种情况:在事件处理程序中访问不到 this。然后各种查文档,一般来说都会找到各种个样的方法解决。那么这是为什么呢?
其实 React 事件并没有绑定在真实的 Dom 节点上,而是通过事件代理,在最外层的 document 上对事件进行统一分发。这是 React 自己去实现的事件机制,并不是使用原生 JS 的事件机制。在源码中,绑定的事件处理程序是当作普通函数直接调用的,事件处理程序内部使用的 this 在其调用时并不会指向定义时所在的组件实例,因此会出现访问不到 this 的情况。

function invokeGuardedCallback(name, func, a) {
    try {
        func(a);
    } catch(x) {
        if (caughtError === null) {
            caughtError = x
        }
    }
}

事件触发时会调用 invokeGuardedCallback 函数,事件处理程序作为参数 func 传入到该函数内部直接调用,因此直接获取的 this 是 undefined。

我们可以通过两种思路解决此问题

  1. 通过给方法手动绑定 this
  2. 使用箭头函数定义事件处理程序
import React from "react"

class ParentComponent extends React.Component {
    construtor(props){
        super(props)
        this.state = {
            num0: 0,
            num1: 0,
            num2: 0,
        }
        this.zeroClickHandler = this.zeroClickHandler.bind(this)
    }

    zeroClickHandler(e){
        let {num0} = this.state
        this.setState({
            num0: num0 + 1
        })
    }
    oneClickHandler = (e) => {
        let {num1} = this.state
        this.setState({
            num1: num1 + 1
        })
    }
    twoClickHandler(e) {
        let {num2} = this.state
        this.setState({
            num2: num2 + 1
        })
    }

    render(){
        const { num0, num1, num2 } = this.state
        return (
            <div>
                <div>num0: {num0}</div>
                <button onClick={this.zeroClickHandler}>
                    add num0
                <button>
                <div>num1: {num1}</div>
                <button onClick={this.oneClickHandler}>
                    add num1
                <button>
                <div>num2: {num2}</div>
                <button onClick={(e) => this.twoClickHandler(e)}>
                    add num2
                <button>
            </div>
        )
    }
}

4. React 组件的分类

4.1 有状态组件与无状态组件

有状态组件:拥有自己可以维护的内部状态
无状态组件:没有自己的内部状态

一般来说有状态组件里会维护一些自己的内部状态,拥有一些处理状态变化的业务逻辑。而无状态组件没有自己的内部状态,主要关注 UI 渲染,根据 props 进行渲染,更容易被复用。

无状态组件通常使用函数定义

import React from "react"

class ParentComponent extends React.Component {
    constructor(props) {
        super(props)
        this.state = {
            value = "aaa"
        }
    }
    render(){
        return (
            <ChildrenComponent value={this.state.value} />
        )
    }
}
// 无状态组件
function ChildrenComponent(props) {
    return (
        <div> {props.value} </div>
    )
}

4.2 受控组件与非受控组件

在 React 中,所谓受控组件和非受控组件,是针对表单而言的。

受控组件:显示的内容受 React 控制,来源为 state 或 props。
非受控组件:表单元素的状态由表单元素自己管理,而不交给 React 管理。

三种表单受控组件的实现:

文本框: 通过给 value 属性绑定状态控制显示内容,并监听 onChange 事件更新状态。
下拉框: 通过给 value 属性绑定状态决定哪一个 option 处于选中状态,并监听 onChange 事件更新状态。
复选框: 通过给 checked 属性绑定状态控制其是否处于选中状态,并监听 onChange 事件更新状态。

import React from "react"

class ParentComponent extends React.Component {
    constructor(props){
        super(props)
        this.state = {
            text: "",
            activeSelect: "",
            checkboxs: {
                el: false,
                al: false
            }
        }

        this.changeHandler = this.changeHandler.bind(this)
    }

    changeHandler(e){
        const {name, value} = e.target
        this.setState({
            [name]: value
        })
    }
    checkboxChangeHandler(key, e) {
        var checked = e.target.checked
        this.setState((prevState) => ({
            checkboxs: {
                ...prevState.checkboxs,
                [key]: checked
            }
        }))
    }

    render(){
        const {text, activeSelect, checkboxs} = this.state
        return (
            <div> <input type="text" name="text" value={text} onChange={this.changeHandler}/> <select name="activeSelect" value={activeSelect} onChange={this.changeHandler}> <option value="aaa">aaa</option> <option value="bbb">bbb</option> <option value="ccc">ccc</option> </select> <div> {Object.keys(this.state.checkboxs).map((title, index) => ( <div> <span>{title}</span> <input type="checkbox" name="checkbox" checked={checkboxs[title]} onChange={(e) => this.checkboxChangeHandler(title, e)}/> </div>)) } </div> </div>
        )
    }
}

非受控组件的实现
通过设置 defaultValue 控制其默认值,通过 ref 属性获取元素的 DOM 实例引用并挂载到实例属性上,从而访问数据。

import React from "react"

class ParentComponent extends React.Component {
    constructor(props){
        super(props)
        this.state = {}

        this.submitHandler = this.submitHandler.bind(this)
    }

    submitHandler(e){
        let textInputValue = this.textInput.value
        console.log(textInputValue)
    }

    render(){
        const {text} = this.props
        return (
            <div> <input type="text" defaultValue={text} ref={textInput => this.textInput = textInput}/> <button onClick={this.submitHandler}>提交</button> </div>
        )
    }
}

5. JSX

JSX 是用于描述 UI 的 Javascript 扩展语法,与 XML 一样都是使用成对的标签构成树状结构的数据。

表现为

import React from "react"

const element = <div className="jsx">jSX</div>

标签类型有两种,可以是 DOM 类型,也可以是 React 组件类型。当使用 DOM 类型的标签时,标签的首字母必须小写。当使用 React 组件类型的标签时,标签名的首字母必须大写。

需要注意的是使用 JSX 的地方,React 必须在作用域内,因为 JSX 只是 React.createElement(component, props, …children)的语法糖。如上 JSX 例子转换后表现为

import React from "react"

const element = React.createElement("div", {className: "jsx"}, "JSX")

而这一步转换过程一般是 babel 帮我们完成。React.createElement()的第一个参数 component 分为两种类型:string 和 ReactClass。babel 编译 JSX 时会根据标签名首字母大小写辨别 DOM 类型还是 React 自定义组件类型。

ref

ref 是 React 元素的一个特殊的属性。常见的使用方式是接收一个回调函数作为值。在组件被挂载时,回调函数会被调用,回调函数会接收当前 DOM 元素或者当前组件实例作为参数。主要是看当前使用 ref 属性的 React 元素是一个 DOM 类型的元素还是一个 React 组件类型的元素。组件卸载时,回调函数会接收 null 作为参数。
在介绍非受控组件的使用时已经展示了 DOM 类型的 ref 的使用,以下展示 React 组件类型的 ref 的使用。

import React from "react"

class ParentComponent extends React.Component {
    constructor(props){
        super(props)
        this.state = {}
    }

    handleClick(type, e){
        this.ChildrenComponentRef[type]( "type")
    }

    render(){
        const {text} = this.props
        return (
            <div> <button onClick={this.handleClick.bind(this, "show")}>显示</button> <button onClick={this.handleClick.bind(this, "hide")}>隐藏</button> <ChildrenComponent ref={ref => this.ChildrenComponentRef = ref}/> </div>
        )
    }
}
class ChildrenComponent extends React.Component {
    constructor(props){
        super(props)
        this.state = {
            isShow: true
        }

        this.show = this.show.bind(this)
        this.hide = this.hide.bind(this)
    }
    show() {
        this.setState({
            isShow: true
        })
    }

    hide() {
        this.setState({
            isShow: false
        })
    }

    render(){
        const {isShow} = this.state
        if(isShow) {
            return  (
                <div>
                    我是一个子组件
                </div>
            )
        } else {
            return null
        }
    }
}

可以看到这里利用 ref 属性,在父组件中获取到当前组件的实例,得以在当前组件外部使用到组件内部定义的方法来操作组件。
注意:只能为类组件定义 ref 属性,不能为函数组件定义 ref 属性(也不是不能只是定义了也没有意义,传进来的是 undefined)。

key

key 也是 React 元素的一个特殊的属性。他不会传入子组件的内部,只是会给 React 传递信息。一般我们在渲染列表的时候会用到。官方建议我们传入一个唯一的值(例如这条 item 的 id)作为 key。key 的作用与 React 的 diff 算法的机制有关。简单来说就是 React 每次执行 render()函数时都会生成一棵新的关于 UI 描述的树,diff 算法就是要比较这新旧两棵树之间的差别,从而把得出的结果(就是比较完成后得到的所有的不同之处)一次性更新到 DOM 树上。而 key 值则是让 React 知道当前元素是否需要更新的其中一种判断依据。当 key 值改变了,如果当前的结点是一个 DOM 类型,则会把旧的结点从 DOM 树中删除,重新生成新的 DOM 结点并插入到 DOM 树中;如果当前的结点是一个 React 组件类型,则会调用该组件实例上的 componentWillUnMount 方法,把该组件卸载,然后重新挂载该组件。
一般我们在开发列表时会使用到。如果在开发列表时,不给列表的每一条 item 加上属性 key,React 会在控制台中报警告。为了免除警告对你的视觉影响,你可以用 index 作为 key 的值。但是如果该列表有增删的需求时,我建议还是给每条 item 加上一个在该列表中是唯一的且不会改变的值作为属性 key 的值,否则会存在性能问题。

详细可查阅:React 的调和过程(Reconciliation)

结尾语

本文介绍了本人对React组件的理解,总结了在实战开发中对React组件的使用以及部分重要API的相关用法,希望对您有所帮助。

由于本人水平和经验有限,如有纰漏或建议,欢迎留言,谢谢您的阅读。

今天的文章React组件的使用总结分享到此就结束了,感谢您的阅读。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/20780.html

(0)
编程小号编程小号

相关推荐

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注