React 组件化开发

作者 : 开心源码 本文共7573个字,预计阅读时间需要19分钟 发布时间: 2022-05-13 共248人阅读

无论是 vue、React 还是 Angular,主流框架都支持并提倡组件化开发,由于组件化开发不仅可以加强代码的能动性和复用性,还能够加快团队协作的速度。组件化开发就像搭积木,首先把一个个积木(组件)设计好,甚至将小积木(容器组件、展现组件)组装成具有肯定功能的积木(比方一个房子),最终再将功能化的积木摞成最终的成品(比方一个社区)。
本文简单详情 React 中组件的定义,以及容器组件、展现组件、高阶组件、复合组件等常见组件的应用,并详情组件间的通信方式。

1. 如何定义一个组件

1.1 一般组件

React 中组件的定义有两种方式,一种是使用 Class 关键字以类的形式来定义组件,另一种是使用函数方式定义。比方定义一个网站的欢迎提醒组件:

  • 类定义
class WelcomeTip extends React.Component {  render() {    return (        <div>           Welcome to this website!         </div>    )  }}
  • 函数定义
function WelcomeTip(props) {  return (    <div>      Welcome to this website!    </div>  )}

无论使用哪一种方式定义组件,组件的调用都是一致的

<WelcomeTip></WelcomeTip>

但是,组件内状态管理、生命周期却有着很大的不同,本文中主要采用类定义的方式来构建组件,关于函数定义组件的应用可以移步 “React Hook” 的详情。

  • 组件状态
class Counter extends React.Component {  // 写了 constructor 就要调用 super  constructor(props) {    super(props)    // 状态公告    this.state = {      count: 0    }  }  // state 的调用:this.state.xxx  // state 的修改:this.setState({count: 1})   // 或者者 this.setState(state => ({count: 1}))  // 支持同时设置多个 key 值,key 值相同时后者覆盖前者  // setState 是一个异步函数  render() {    return (        <div>        <p>Welcome, {this.props.name}! You have click {this.state.count} times!</p>        <button           onClick={() => this.setState(state => {count: state.count + 1})}         >Click</button>      </div>    )  }}
  • 组件的生命周期
    • 初始化:constructor ,用于完成组件的初始化工作,如定义state 的初始内容、定义组件内部变量等
    • 组件的挂载:
      • componentWillMount,发生在组件挂载到 DOM 之前,此处修改 state 不会引起组件的重新渲染。该部分的功能也可以提前到 constructor 中,因而很少在项目中使用。
      • render,根据组件的 propsstate(两者的重传递和重赋值,无论值能否有变化,都可以引起组件重新 render),返回?个 React 元素(形容组件,即UI),不负责组件实际渲染?作,之后由 React ?身根据此元素去渲染出??DOM。纯函数,返回结果只依赖于传入的参数,执行过程中没有反作用。不能在该阶段执行 setState,会造成死循环。
      • componentDidMount,组件挂载到 DOM 之后调用,且只会被调用一次。
    • 组件的升级:当 props 或者 state 被重新赋值时,无论值能否发生改变,都会触发组件的升级。因而有如下两种情况会触发组件的升级:1. 父组件重新 render,因为子组件的 props 被传值,触发子组件的升级;2. 组件本身调用 setState,无论 state 有没有改变,组件都会升级
      • componentWillReceiveProps(nextProps)props 重传时被调用,该函数中调用 setState 不会引起组件的二次升级,因而即使在该函数中执行 this.setState 升级了stateshouldComponentUpdate componentWillUpdate 中的 this.state 仍旧是原来的值
      • shouldComponentUpdate(nextProps, nextState),此?法通过?较 nextPropsnextState及当前组件的 this.propsthis.state,返回 true时当前组件将继续执?升级过程,返回 false 则当前组件升级停?,以此可?来减少组件的不必要渲染,优化组件性能。
      • componentWillUpdate(nextProps, nextState),此?法在调? render ?法前执?,在这边可执??些组件升级发?前的?作,?般较少?。
      • render :同挂载时的 render。
      • componentDidUpdate(prevProps, prevState),此?法在组件升级后被调?,可以操作组件升级的 DOM ,prevPropsprevState 这两个参数指的是组件升级前的 propsstate
    • 组件的卸载:
      • componentWillUnmount:此?法在组件被卸载前调?,可以在这?执??些清除?作,?如清理组件中使?的定时器, componentDidMount 中?动创立的 DOM 元素等,以避免引起内存泄漏。
    • 【注意】componentWillMount componentWillReceivePropscomponentWillUpdate 在 React 17.x 版本之后将不再支持,目前使用会提醒 warning。在 16.3 之后,使用 getDerivedStateFromProps 代替上述三个函数
      • static getDerivedStateFromProps(props, state),在组件创立时和升级时的 render ?法之前调?, 它应该返回?个对象来升级状态,或者者返回 null 来不升级任何内容。
      • getSnapshotBeforeUpdate,被调?于render之后,此时可以读取但还不能操作升级 DOM ,因而可以按需调整滚动条等。 返回值(必需有)将作为参数传递给 componentDidUpdate
        引自 aermin/blog/issues/55
1.2 组件拆分——容器组件&展现组件

在涉及复杂的数据预解决时,可以考虑将组件拆分成容器组件和展现组件。其中容器组件负责请求并解决数据,展现组件负责根据 Props 显示信息。如此可以减小组件的体积,使开发人员可以跟专注于某一功能开发,并提高组件的重用性和可用性,同时易于测试和提高系统性能。

// 容器组件class CommentList extends React.Component {    state = {        list: []    }    componentDidMount() {        setTimeout(() => {            this.setState({                list: [                    {id: 1, text: '我喜欢苹果', author: '小A'},                    {id: 2, text: '我喜欢橙子', author: '小B'},                    {id: 3, text: '我喜欢西瓜', author: '小C'},                ]            })        })    }    render() {        return (            <div>                {this.state.list.map(l => {                    return <Item key={l.id} text={l.text} author={l.author}/>                })}            </div>        )    }}// 展现组件function Item({text, author}) {    return (<div>        {text} -- <span style={{color: 'blue'}}>{author}</span>    </div>)}
1.3 PureComponent

在组件生命周期中组件升级过程中,提及只需发生重新挂载,无论 props state 能否变化,都会出发升级。纯组件就是定制了 shouldComponentUpdate 后的Component,仅有依赖的数据发生变化时才进行升级。 该比较过程数据浅比较,因而对象属性或者数组中元素并不适用于该特性。

// 假设父组件有 count 和 name 两个状态// 子组件仅依赖父组件的 count// 假如子组件继承的是 React.Component,那么父组件 name 值发生变更时,子组件仍旧会重新 render// 继承的是 React.PureComponent 时,则仅有父组件的 count 值变化时,子组件才会重新调用 render class Child extends React.PureComponent {  render() {    return <div>{this.props.count}</div>  }}

React 16.6.0 之后,使用 React.memo 让函数式的组件也有 PureComponent 的功能

const Child = React.memo(() => {  return <div>{this.props.count}</div>})

2. 高阶组件是什么

2.1 高阶组件与一般组件有什么不同

高阶组件是 React 中重用组件逻辑的高级技术,它不是 React 的 api ,而是一种组件加强模式。高阶组件是一个函数,它返回另外一个组件,产生新的组件可以对被包装组件属性进行包装,也可以重写部分生命周期。

高阶组件可以为组件增加某一特殊功能,也可以多层嵌套,赋予被包装组件多个功能。比方打印日志功能、增加标题功能等。

// 包装后的组件具有日志打印功能const withLog = Component => {     class newComponent extends React.Component {        componentDidMount() {            console.log(`${Date.now()}:组件已挂载`)        }        render() {            return <Component {...this.props} />        }    }    return newComponent}// 包装后的组件都带有一个标题const withTitle = Component => {    const newComponent = props => {        return (<Fragment>            <h3>这是一个标题</h3>            <hr />            <Component {...props} />        </Fragment>)    }    return newComponent}
2.2 高阶组件怎样使用
  1. 链式调用

高阶组件本质上就是一个函数,因而可以采用链式调用的形式,将待包装的组件作为参数传入,并 export 出去就可。同时也可以多个高阶组件嵌套,一层层包装单一组件。

export default withLog(withTitle(CommentList))
  1. 装饰者模式

ES7 中提供了装饰者模式的写法,可以使代码更加简洁,但需要进行相关配置:

  • 暴露项目的所有配置项:npm run eject

  • 安装:npm install -D @babel/plugin-proposal-decorators

  • 配置 package.json 文件中 babel 配置项

      "babel": {    "presets": [      "react-app"    ],    "plugins": [      ["@babel/plugin-proposal-decorators", {"legacy": true}]    ]  }

如此,上述链式调用可以修改为:

export default @withLog@withTitleclass CommentList extends React.Component {  ...}

3. 复合组件

复合组件可以让开发者以更便捷地创立组件的外观和行为,相比继承更加直观和安全。

// 容器不关心内容与逻辑// 3. 容器中可以使用 children,但因为传入的是 vdom 数组,故而不能修改function Dialog(props) {  return (<div style={{border: `1px solid ${props.color || '#ccc'}`}}>    {React.Children.map(props.children, child => child.type === 'p' ? child : null)}    {props.footer}  </div>)}// 通过复合提供内容function HelloDialog(props) {  // 1. 参数可以使用 props 传入  // 2. 可以传入任何表达式  return (<Dialog color='blue' footer={<p>版权归 road 所有</p>}>    <h3>你好啊,{props.name}</h3>      <p>感谢访问本网站</p>  </Dialog>)}

4. 组件间如何实现通信

4.1 父传子

通过 props 将参数传递给子组件,使用 class 关键字以类方式定义组件时,使用 this.props 就可以父组件传递的所有参数,函数方式定义时则需要在公告时增加 props 参数,或者解构参数。

// 类方式定义class Child extends React.Component {  render() {    return (<div>        子组件:{this.props.name}    </div>)  }}// 函数方式定义function Child(props) {  return (<div>        子组件:{props.name}  </div>)}// 函数方式function Child({name}) {  return (<div>        子组件:{name}  </div>)}

父组件传参:

<Child name='road'></Child>
4.2 子传父

父组件中公告一个相关方法,并作为参数传递给子组件。子组件通过调用父组件传递过来的方法,修改父组件中的数据。

// 比方:父组件中有个计数值,子组件中的按钮点击之后计数值 +1function Child({increase, step}) {    return (        <div>            <button onClick={() => increase(step)}>+{step}</button>          </div>    );}export class Parent extends Component {    state = {        count: 0    }    add(step) {        this.setState(state => ({count: state.count + step}))    }    render() {        return (            <div>                计数值为 {this.state.count}                    {/* 注意方法传递过程中 this 的指向变更 */}                <Child increase={this.add.bind(this)} step={1}></Child>                <Child increase={this.add.bind(this)} step={2}></Child>            </div>        );    }}
4.3 跨组件通信

跨组件通信有兄弟组件通信、父组件与孙组件的通信等,从上到下的数据传递可以通过 props 一层层传递,但从下到上的数据传递则十分麻烦。例如下图中【子组件1】相与【父组件B】通信时,就需要将信息一层层冒到祖先组件中,再通过祖先组件派发给【父组件B】。

多层组件结构

因而假如项目较为庞大时,可以引入 redux 进行全局状态管理(可参考 redux 使用实例)。当项目量级较小时,则使用 React 中的 Context 来进行公共状态的管理,该模式包括两个角色:

  • Provider:外层提供数据的组件,内部组件都可以访问到来自 provider 的数据

  • Consumer :内层获取数据的组件,沿上追溯到最近的 provider,消费其数据。接收一个函数作为子节点,返回 react 节点。

function Display(props) {    // 6. props 重新赋值,组件升级    return (        <div>            <h2>{props.title}</h2>            <p>你的名字是:{props.name}</p>            <p>你的邮箱是:{props.email}</p>        </div>    )}class FormItem extends Component {    state = {        val: ''    }    render() {        const {keyName, label, type} = this.props        // 3. consumer 内部接收一个函数,参数 value 来源于最近的 provider        return (<SurveyContext.Consumer>            {(value, _this) => {                return (<div>                    <label htmlFor={keyName}>{label}</label>                    <input                         id = {keyName}                         type = {type}                         placeholder={value[keyName]}                        onChange = {e => {this.setState({val: e.target.value})}}                        onKeyDown = {e => {                            if( 13 === e.keyCode ) {                                // 4. 调用操作方法,也即 Survey 组件中的 changeState 方法,修改 provider 中的数据                                value.change(keyName, this.state.val)                            }                        }}                    />                </div>)            }}        </SurveyContext.Consumer>)    }}// 2. 中间组件不需要传递数据和方法class Form extends Component {    render() {        return (            <div>                <FormItem keyName='name' label='名字' type='text'/>                <FormItem keyName='email' label='邮箱' type='text'/>            </div>        );    }}const SurveyContext = React.createContext()export default class Survey extends Component {    state = {        name: 'abc',        email: '123@163.com'    }    changeState(key, val) {        this.setState({[key]: val})    }    // 5. setState 方法触发组件升级,重新 render    render() {        return (            <div>                {/* 1. provider 提供 value 给 consumer,可以将修改 state 的方法也作为 value 对象的方法传递*/}                <SurveyContext.Provider                     value={{                        ...this.state,                         change: this.changeState.bind(this)                    }}                >                    <Form></Form>                        </SurveyContext.Provider>                <hr />                <Display title='问卷调查' name={this.state.name} email={this.state.email}></Display>             </div>        )    }}

效果:

跨组件通信实例

说明
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » React 组件化开发

发表回复