美文网首页Web前端之路
React 组件化开发

React 组件化开发

作者: 林木木road | 来源:发表于2020-04-09 14:18 被阅读0次

    无论是 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>
            )
        }
    }
    

    效果:


    跨组件通信实例

    相关文章

      网友评论

        本文标题:React 组件化开发

        本文链接:https://www.haomeiwen.com/subject/rohcmhtx.html