React 组件化开发

无论是 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 之后调用,且只会被调用一次。
    • 组件的更新:当 propsstate 被重新赋值时,无论值是否发生改变,都会触发组件的更新。因此有如下两种情况会触发组件的更新: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
        引自https://github.com/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
@withTitle
class 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 子传父

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

// 比如:父组件中有个计数值,子组件中的按钮点击之后计数值 +1
function 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>
        )
    }
}

效果:


跨组件通信实例
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,319评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,801评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,567评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,156评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,019评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,090评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,500评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,192评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,474评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,566评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,338评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,212评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,572评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,890评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,169评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,478评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,661评论 2 335

推荐阅读更多精彩内容

  • 作为一个合格的开发者,不要只满足于编写了可以运行的代码。而要了解代码背后的工作原理;不要只满足于自己的程序...
    六个周阅读 8,406评论 1 33
  • 深入JSX date:20170412笔记原文其实JSX是React.createElement(componen...
    gaoer1938阅读 8,026评论 2 35
  • 40、React 什么是React?React 是一个用于构建用户界面的框架(采用的是MVC模式):集中处理VIE...
    萌妹撒阅读 992评论 0 1
  • 其实之所以讲到这里是因为,当我们使用React的组件化开发Web应用的时候,就会遇到这样的问题,很多组件需要某个功...
    井润阅读 383评论 0 0
  • 前言 组件中的state具体是什么?怎么更改state的数据? setState函数分别接收对象以及函数有什么区别...
    itclanCoder阅读 850评论 0 0