React 組件化開發(fā)

無論是 vue诊笤、React 還是 Angular戴质,主流框架都支持并提倡組件化開發(fā)钱磅,因為組件化開發(fā)不僅可以增強代碼的能動性和復(fù)用性,還能夠加快團(tuán)隊協(xié)作的速度宪潮。組件化開發(fā)就像搭積木溯警,首先把一個個積木(組件)設(shè)計好趣苏,甚至將小積木(容器組件、展示組件)組裝成具備一定功能的積木(比如一個房子)梯轻,最終再將功能化的積木摞成最終的成品(比如一個社區(qū))食磕。
本文簡單介紹 React 中組件的定義,以及容器組件喳挑、展示組件彬伦、高階組件、復(fù)合組件等常見組件的應(yīng)用伊诵,并介紹組件間的通信方式单绑。

1. 如何定義一個組件

1.1 一般組件

React 中組件的定義有兩種方式,一種是使用 Class 關(guān)鍵字以類的形式來定義組件曹宴,另一種是使用函數(shù)方式定義搂橙。比如定義一個網(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è)置多個 key 值,key 值相同時后者覆蓋前者
  // setState 是一個異步函數(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 不會引起組件的重新渲染诀豁。該部分的功能也可以提前到 constructor 中窄刘,因此很少在項目中使用。
      • render舷胜,根據(jù)組件的 propsstate(兩者的重傳遞和重賦值娩践,無論值是否有變化,都可以引起組件重新 render)烹骨,返回?個 React 元素(描述組件翻伺,即UI),不負(fù)責(zé)組件實際渲染?作沮焕,之后由 React ?身根據(jù)此元素去渲染出??DOM吨岭。純函數(shù),返回結(jié)果只依賴于傳入的參數(shù)峦树,執(zhí)行過程中沒有副作用辣辫。不能在該階段執(zhí)行 setState旦事,會造成死循環(huán)。
      • componentDidMount急灭,組件掛載到 DOM 之后調(diào)用姐浮,且只會被調(diào)用一次。
    • 組件的更新:當(dāng) propsstate 被重新賦值時葬馋,無論值是否發(fā)生改變卖鲤,都會觸發(fā)組件的更新。因此有如下兩種情況會觸發(fā)組件的更新:1. 父組件重新 render畴嘶,由于子組件的 props 被傳值蛋逾,觸發(fā)子組件的更新;2. 組件本身調(diào)用 setState掠廓,無論 state 有沒有改變换怖,組件都會更新
      • componentWillReceiveProps(nextProps)props 重傳時被調(diào)用蟀瞧,該函數(shù)中調(diào)用 setState 不會引起組件的二次更新沉颂,因此即便在該函數(shù)中執(zhí)行 this.setState 更新了stateshouldComponentUpdate componentWillUpdate 中的 this.state 依舊是原來的值悦污。
      • shouldComponentUpdate(nextProps, nextState)铸屉,此?法通過?較 nextPropsnextState及當(dāng)前組件的 this.props切端,this.state彻坛,返回 true時當(dāng)前組件將繼續(xù)執(zhí)?更新過程,返回 false 則當(dāng)前組件更新停?踏枣,以此可?來減少組件的不必要渲染昌屉,優(yōu)化組件性能。
      • componentWillUpdate(nextProps, nextState)茵瀑,此?法在調(diào)? render ?法前執(zhí)?间驮,在這邊可執(zhí)??些組件更新發(fā)?前的?作,?般較少?马昨。
      • render :同掛載時的 render竞帽。
      • componentDidUpdate(prevProps, prevState),此?法在組件更新后被調(diào)?鸿捧,可以操作組件更新的 DOM 屹篓,prevPropsprevState 這兩個參數(shù)指的是組件更新前的 propsstate
    • 組件的卸載:
      • componentWillUnmount:此?法在組件被卸載前調(diào)?匙奴,可以在這?執(zhí)??些清理?作堆巧,?如清除組件中使?的定時器, componentDidMount 中?動創(chuàng)建的 DOM 元素等,以避免引起內(nèi)存泄漏恳邀。
    • 【注意】componentWillMount componentWillReceivePropscomponentWillUpdate 在 React 17.x 版本之后將不再支持懦冰,目前使用會提示 warning。在 16.3 之后谣沸,使用 getDerivedStateFromProps 代替上述三個函數(shù)
      • static getDerivedStateFromProps(props, state)刷钢,在組件創(chuàng)建時和更新時的 render ?法之前調(diào)?, 它應(yīng)該返回?個對象來更新狀態(tài)乳附,或者返回 null 來不更新任何內(nèi)容内地。
      • getSnapshotBeforeUpdate,被調(diào)?于render之后赋除,此時可以讀取但還不能操作更新 DOM 阱缓,因此可以按需調(diào)整滾動條等。 返回值(必須有)將作為參數(shù)傳遞給 componentDidUpdate举农。
        引自https://github.com/aermin/blog/issues/55
1.2 組件拆分——容器組件&展示組件

在涉及復(fù)雜的數(shù)據(jù)預(yù)處理時荆针,可以考慮將組件拆分成容器組件和展示組件。其中容器組件負(fù)責(zé)請求并處理數(shù)據(jù)颁糟,展示組件負(fù)責(zé)根據(jù) Props 顯示信息航背。如此可以減小組件的體積,使開發(fā)人員可以跟專注于某一功能開發(fā)棱貌,并提高組件的重用性和可用性玖媚,同時易于測試和提高系統(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 是否變化,都會出發(fā)更新障贸。純組件就是定制了 shouldComponentUpdate 后的Component错森,僅有依賴的數(shù)據(jù)發(fā)生變化時才進(jìn)行更新。 該比較過程數(shù)據(jù)淺比較篮洁,因此對象屬性或數(shù)組中元素并不適用于該特性问词。

// 假設(shè)父組件有 count 和 name 兩個狀態(tài)
// 子組件僅依賴父組件的 count
// 如果子組件繼承的是 React.Component,那么父組件 name 值發(fā)生變更時嘀粱,子組件依舊會重新 render
// 繼承的是 React.PureComponent 時,則僅有父組件的 count 值變化時辰狡,子組件才會重新調(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 中重用組件邏輯的高級技術(shù),它不是 React 的 api 宛篇,而是一種組件增強模式娃磺。高階組件是一個函數(shù),它返回另外一個組件叫倍,產(chǎn)生新的組件可以對被包裝組件屬性進(jìn)行包裝偷卧,也可以重寫部分生命周期豺瘤。

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

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

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

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

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

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

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

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

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

      "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. 組件間如何實現(xiàn)通信

4.1 父傳子

通過 props 將參數(shù)傳遞給子組件鸵鸥,使用 class 關(guān)鍵字以類方式定義組件時奠滑,使用 this.props 即可以父組件傳遞的所有參數(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 子傳父

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

// 比如:父組件中有個計數(shù)值矮台,子組件中的按鈕點擊之后計數(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>
                計數(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】通信時,就需要將信息一層層冒到祖先組件中确虱,再通過祖先組件派發(fā)給【父組件B】含友。

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

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

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

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

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)部接收一個函數(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 對象的方法傳遞*/}
                <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>
        )
    }
}

效果:


跨組件通信實例
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市使套,隨后出現(xiàn)的幾起案子罐呼,更是在濱河造成了極大的恐慌,老刑警劉巖侦高,帶你破解...
    沈念sama閱讀 222,104評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件嫉柴,死亡現(xiàn)場離奇詭異,居然都是意外死亡奉呛,警方通過查閱死者的電腦和手機计螺,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,816評論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來瞧壮,“玉大人登馒,你說我怎么就攤上這事∨夭郏” “怎么了陈轿?”我有些...
    開封第一講書人閱讀 168,697評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長秦忿。 經(jīng)常有香客問我麦射,道長,這世上最難降的妖魔是什么灯谣? 我笑而不...
    開封第一講書人閱讀 59,836評論 1 298
  • 正文 為了忘掉前任法褥,我火速辦了婚禮,結(jié)果婚禮上酬屉,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好呐萨,可當(dāng)我...
    茶點故事閱讀 68,851評論 6 397
  • 文/花漫 我一把揭開白布杀饵。 她就那樣靜靜地躺著,像睡著了一般谬擦。 火紅的嫁衣襯著肌膚如雪切距。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,441評論 1 310
  • 那天惨远,我揣著相機與錄音谜悟,去河邊找鬼。 笑死北秽,一個胖子當(dāng)著我的面吹牛葡幸,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播贺氓,決...
    沈念sama閱讀 40,992評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼蔚叨,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了辙培?” 一聲冷哼從身側(cè)響起蔑水,我...
    開封第一講書人閱讀 39,899評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎扬蕊,沒想到半個月后搀别,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,457評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡尾抑,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,529評論 3 341
  • 正文 我和宋清朗相戀三年歇父,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蛮穿。...
    茶點故事閱讀 40,664評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡庶骄,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出践磅,到底是詐尸還是另有隱情单刁,我是刑警寧澤,帶...
    沈念sama閱讀 36,346評論 5 350
  • 正文 年R本政府宣布府适,位于F島的核電站羔飞,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏檐春。R本人自食惡果不足惜逻淌,卻給世界環(huán)境...
    茶點故事閱讀 42,025評論 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望疟暖。 院中可真熱鬧卡儒,春花似錦田柔、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,511評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至擎鸠,卻和暖如春缀磕,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背劣光。 一陣腳步聲響...
    開封第一講書人閱讀 33,611評論 1 272
  • 我被黑心中介騙來泰國打工袜蚕, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人绢涡。 一個月前我還...
    沈念sama閱讀 49,081評論 3 377
  • 正文 我出身青樓牲剃,卻偏偏與公主長得像,于是被迫代替她去往敵國和親垂寥。 傳聞我的和親對象是個殘疾皇子颠黎,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,675評論 2 359

推薦閱讀更多精彩內(nèi)容