React 組件化開發(fā)

無論是 vue、React 還是 Angular,主流框架都支持并提倡組件化開發(fā),因?yàn)榻M件化開發(fā)不僅可以增強(qiáng)代碼的能動(dòng)性和復(fù)用性,還能夠加快團(tuán)隊(duì)協(xié)作的速度。組件化開發(fā)就像搭積木,首先把一個(gè)個(gè)積木(組件)設(shè)計(jì)好,甚至將小積木(容器組件、展示組件)組裝成具備一定功能的積木(比如一個(gè)房子),最終再將功能化的積木摞成最終的成品(比如一個(gè)社區(qū))。
本文簡(jiǎn)單介紹 React 中組件的定義,以及容器組件、展示組件、高階組件、復(fù)合組件等常見組件的應(yīng)用,并介紹組件間的通信方式。

1. 如何定義一個(gè)組件

1.1 一般組件

React 中組件的定義有兩種方式,一種是使用 Class 關(guān)鍵字以類的形式來定義組件,另一種是使用函數(shù)方式定義。比如定義一個(gè)網(wǎng)站的歡迎提示組件:

  • 類定義
class WelcomeTip extends React.Component {
  render() {
    return (
        <div>
           Welcome to this website!   
      </div>
    )
  }
}
  • 函數(shù)定義
function WelcomeTip(props) {
  return (
    <div>
      Welcome to this website!
    </div>
  )
}

無論使用哪一種方式定義組件,組件的調(diào)用都是一致的

<WelcomeTip></WelcomeTip>

但是,組件內(nèi)狀態(tài)管理、生命周期卻有著很大的不同,本文中主要采用類定義的方式來構(gòu)建組件,關(guān)于函數(shù)定義組件的應(yīng)用可以移步 “React Hook” 的介紹。

  • 組件狀態(tài)
class Counter extends React.Component {
  // 寫了 constructor 就要調(diào)用 super
  constructor(props) {
    super(props)
    // 狀態(tài)聲明
    this.state = {
      count: 0
    }
  }
  // state 的調(diào)用:this.state.xxx
  // state 的修改:this.setState({count: 1}) 
  // 或者 this.setState(state => ({count: 1}))
  // 支持同時(shí)設(shè)置多個(gè) key 值,key 值相同時(shí)后者覆蓋前者
  // setState 是一個(gè)異步函數(shù)
  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 的初始內(nèi)容、定義組件內(nèi)部變量等
    • 組件的掛載:
      • componentWillMount,發(fā)生在組件掛載到 DOM 之前,此處修改 state 不會(huì)引起組件的重新渲染。該部分的功能也可以提前到 constructor 中,因此很少在項(xiàng)目中使用。
      • render,根據(jù)組件的 propsstate(兩者的重傳遞和重賦值,無論值是否有變化,都可以引起組件重新 render),返回?個(gè) React 元素(描述組件,即UI),不負(fù)責(zé)組件實(shí)際渲染?作,之后由 React ?身根據(jù)此元素去渲染出??DOM。純函數(shù),返回結(jié)果只依賴于傳入的參數(shù),執(zhí)行過程中沒有副作用。不能在該階段執(zhí)行 setState,會(huì)造成死循環(huán)。
      • componentDidMount,組件掛載到 DOM 之后調(diào)用,且只會(huì)被調(diào)用一次。
    • 組件的更新:當(dāng) propsstate 被重新賦值時(shí),無論值是否發(fā)生改變,都會(huì)觸發(fā)組件的更新。因此有如下兩種情況會(huì)觸發(fā)組件的更新:1. 父組件重新 render,由于子組件的 props 被傳值,觸發(fā)子組件的更新;2. 組件本身調(diào)用 setState,無論 state 有沒有改變,組件都會(huì)更新
      • componentWillReceiveProps(nextProps),props 重傳時(shí)被調(diào)用,該函數(shù)中調(diào)用 setState 不會(huì)引起組件的二次更新,因此即便在該函數(shù)中執(zhí)行 this.setState 更新了state,shouldComponentUpdate componentWillUpdate 中的 this.state 依舊是原來的值。
      • shouldComponentUpdate(nextProps, nextState),此?法通過?較 nextProps,nextState及當(dāng)前組件的 this.props,this.state,返回 true時(shí)當(dāng)前組件將繼續(xù)執(zhí)?更新過程,返回 false 則當(dāng)前組件更新停?,以此可?來減少組件的不必要渲染,優(yōu)化組件性能。
      • componentWillUpdate(nextProps, nextState),此?法在調(diào)? render ?法前執(zhí)?,在這邊可執(zhí)??些組件更新發(fā)?前的?作,?般較少?。
      • render :同掛載時(shí)的 render。
      • componentDidUpdate(prevProps, prevState),此?法在組件更新后被調(diào)?,可以操作組件更新的 DOM ,prevPropsprevState 這兩個(gè)參數(shù)指的是組件更新前的 propsstate 。
    • 組件的卸載:
      • componentWillUnmount:此?法在組件被卸載前調(diào)?,可以在這?執(zhí)??些清理?作,?如清除組件中使?的定時(shí)器, componentDidMount 中?動(dòng)創(chuàng)建的 DOM 元素等,以避免引起內(nèi)存泄漏。
    • 【注意】componentWillMount componentWillReceivePropscomponentWillUpdate 在 React 17.x 版本之后將不再支持,目前使用會(huì)提示 warning。在 16.3 之后,使用 getDerivedStateFromProps 代替上述三個(gè)函數(shù)
      • static getDerivedStateFromProps(props, state),在組件創(chuàng)建時(shí)和更新時(shí)的 render ?法之前調(diào)?, 它應(yīng)該返回?個(gè)對(duì)象來更新狀態(tài),或者返回 null 來不更新任何內(nèi)容。
      • getSnapshotBeforeUpdate,被調(diào)?于render之后,此時(shí)可以讀取但還不能操作更新 DOM ,因此可以按需調(diào)整滾動(dòng)條等。 返回值(必須有)將作為參數(shù)傳遞給 componentDidUpdate。
        引自https://github.com/aermin/blog/issues/55
1.2 組件拆分——容器組件&展示組件

在涉及復(fù)雜的數(shù)據(jù)預(yù)處理時(shí),可以考慮將組件拆分成容器組件和展示組件。其中容器組件負(fù)責(zé)請(qǐng)求并處理數(shù)據(jù),展示組件負(fù)責(zé)根據(jù) Props 顯示信息。如此可以減小組件的體積,使開發(fā)人員可以跟專注于某一功能開發(fā),并提高組件的重用性和可用性,同時(shí)易于測(cè)試和提高系統(tǒng)性能。

// 容器組件
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

在組件生命周期中組件更新過程中,提及只要發(fā)生重新掛載,無論 props state 是否變化,都會(huì)出發(fā)更新。純組件就是定制了 shouldComponentUpdate 后的Component,僅有依賴的數(shù)據(jù)發(fā)生變化時(shí)才進(jìn)行更新。 該比較過程數(shù)據(jù)淺比較,因此對(duì)象屬性或數(shù)組中元素并不適用于該特性。

// 假設(shè)父組件有 count 和 name 兩個(gè)狀態(tài)
// 子組件僅依賴父組件的 count
// 如果子組件繼承的是 React.Component,那么父組件 name 值發(fā)生變更時(shí),子組件依舊會(huì)重新 render
// 繼承的是 React.PureComponent 時(shí),則僅有父組件的 count 值變化時(shí),子組件才會(huì)重新調(diào)用 render 
class Child extends React.PureComponent {
  render() {
    return <div>{this.props.count}</div>
  }
}

React 16.6.0 之后,使用 React.memo 讓函數(shù)式的組件也有 PureComponent 的功能

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

2. 高階組件是什么

2.1 高階組件與一般組件有什么不同

高階組件是 React 中重用組件邏輯的高級(jí)技術(shù),它不是 React 的 api ,而是一種組件增強(qiáng)模式。高階組件是一個(gè)函數(shù),它返回另外一個(gè)組件,產(chǎn)生新的組件可以對(duì)被包裝組件屬性進(jìn)行包裝,也可以重寫部分生命周期。

高階組件可以為組件添加某一特殊功能,也可以多層嵌套,賦予被包裝組件多個(gè)功能。比如打印日志功能、添加標(biāo)題功能等。

// 包裝后的組件具備日志打印功能
const withLog = Component => { 
    class newComponent extends React.Component {
        componentDidMount() {
            console.log(`${Date.now()}:組件已掛載`)
        }
        render() {
            return <Component {...this.props} />
        }
    }
    return newComponent
}

// 包裝后的組件都帶有一個(gè)標(biāo)題
const withTitle = Component => {
    const newComponent = props => {
        return (<Fragment>
            <h3>這是一個(gè)標(biāo)題</h3>
            <hr />
            <Component {...props} />
        </Fragment>)
    }
    return newComponent
}
2.2 高階組件怎么使用
  1. 鏈?zhǔn)秸{(diào)用

高階組件本質(zhì)上就是一個(gè)函數(shù),因此可以采用鏈?zhǔn)秸{(diào)用的形式,將待包裝的組件作為參數(shù)傳入,并 export 出去即可。同時(shí)也可以多個(gè)高階組件嵌套,一層層包裝單一組件。

export default withLog(withTitle(CommentList))
  1. 裝飾者模式

ES7 中提供了裝飾者模式的寫法,可以使代碼更加簡(jiǎn)潔,但需要進(jìn)行相關(guān)配置:

  • 暴露項(xiàng)目的所有配置項(xiàng):npm run eject

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

  • 配置 package.json 文件中 babel 配置項(xiàng)

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

如此,上述鏈?zhǔn)秸{(diào)用可以修改為:

export default 
@withLog
@withTitle
class CommentList extends React.Component {
  ...
}

3. 復(fù)合組件

復(fù)合組件可以讓開發(fā)者以更便捷地創(chuàng)建組件的外觀和行為,相比繼承更加直觀和安全。

// 容器不關(guān)心內(nèi)容與邏輯
// 3. 容器中可以使用 children,但由于傳入的是 vdom 數(shù)組,故而不能修改
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>)
}
// 通過復(fù)合提供內(nèi)容
function HelloDialog(props) {
  // 1. 參數(shù)可以使用 props 傳入
  // 2. 可以傳入任何表達(dá)式
  return (<Dialog color='blue' footer={<p>版權(quán)歸 road 所有</p>}>
    <h3>你好啊,{props.name}</h3>  
    <p>感謝訪問本網(wǎng)站</p>
  </Dialog>)
}

4. 組件間如何實(shí)現(xiàn)通信

4.1 父?jìng)髯?/h5>

通過 props 將參數(shù)傳遞給子組件,使用 class 關(guān)鍵字以類方式定義組件時(shí),使用 this.props 即可以父組件傳遞的所有參數(shù),函數(shù)方式定義時(shí)則需要在聲明時(shí)添加 props 參數(shù),或解構(gòu)參數(shù)。

// 類方式定義
class Child extends React.Component {
  render() {
    return (<div>
        子組件:{this.props.name}
    </div>)
  }
}

// 函數(shù)方式定義
function Child(props) {
  return (<div>
        子組件:{props.name}
  </div>)
}

// 函數(shù)方式
function Child({name}) {
  return (<div>
        子組件:{name}
  </div>)
}

父組件傳參:

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

父組件中聲明一個(gè)相關(guān)方法,并作為參數(shù)傳遞給子組件。子組件通過調(diào)用父組件傳遞過來的方法,修改父組件中的數(shù)據(jù)。

// 比如:父組件中有個(gè)計(jì)數(shù)值,子組件中的按鈕點(diǎn)擊之后計(jì)數(shù)值 +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>
                計(jì)數(shù)值為 {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 跨組件通信

跨組件通信有兄弟組件通信、父組件與孫組件的通信等,從上到下的數(shù)據(jù)傳遞可以通過 props 一層層傳遞,但從下到上的數(shù)據(jù)傳遞則十分麻煩。例如下圖中【子組件1】相與【父組件B】通信時(shí),就需要將信息一層層冒到祖先組件中,再通過祖先組件派發(fā)給【父組件B】。

多層組件結(jié)構(gòu)

因此如果項(xiàng)目較為龐大時(shí),可以引入 redux 進(jìn)行全局狀態(tài)管理(可參考 redux 使用實(shí)例)。當(dāng)項(xiàng)目量級(jí)較小時(shí),則使用 React 中的 Context 來進(jìn)行公共狀態(tài)的管理,該模式包括兩個(gè)角色:

  • Provider:外層提供數(shù)據(jù)的組件,內(nèi)部組件都可以訪問到來自 provider 的數(shù)據(jù)

  • Consumer :內(nèi)層獲取數(shù)據(jù)的組件,沿上追溯到最近的 provider,消費(fèi)其數(shù)據(jù)。接收一個(gè)函數(shù)作為子節(jié)點(diǎn),返回 react 節(jié)點(diǎn)。

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 內(nèi)部接收一個(gè)函數(shù),參數(shù) 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. 調(diào)用操作方法,也即 Survey 組件中的 changeState 方法,修改 provider 中的數(shù)據(jù)
                                value.change(keyName, this.state.val)
                            }
                        }}
                    />
                </div>)
            }}
        </SurveyContext.Consumer>)
    }
}

// 2. 中間組件不需要傳遞數(shù)據(jù)和方法
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 方法觸發(fā)組件更新,重新 render
    render() {
        return (
            <div>
                {/* 1. provider 提供 value 給 consumer,可以將修改 state 的方法也作為 value 對(duì)象的方法傳遞*/}
                <SurveyContext.Provider 
                    value={{
                        ...this.state, 
                        change: this.changeState.bind(this)
                    }}
                >
                    <Form></Form>        
                </SurveyContext.Provider>
                <hr />
                <Display title='問卷調(diào)查' name={this.state.name} email={this.state.email}></Display> 
            </div>
        )
    }
}

效果:


跨組件通信實(shí)例
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容