「React」之組件邏輯復用小技巧
編者薦語:
本文將介紹React組件邏輯復用的一些常用模式和技巧。包括一下幾個方面:
什么是高階組件 HOCHOC解決了哪些問題如何封裝一個簡單的高階組件 HOC在項目中常用的一些技巧和方法什么是 Render PropsRender Props的特點和用法Render Props和HOC React Hooks相比,有哪些優(yōu)劣(重要面試題)
HOC高階組件
高階組件(HOC):是React中用于
復用組件邏輯的一種高級技巧。HOC自身不是React API的一部分,它是一種基于React的組合特性而形成的設計模式。
高階組件可以看做React對裝飾器模式的一種實現(xiàn),具體而言,高階組件是參數(shù)作為組件,返回值為新組件的函數(shù)。
HOC解決的問題
抽離公共組件,實現(xiàn)組件代碼復用,常見場景:頁面復用。 條件渲染,控制組件的渲染邏輯(渲染劫持),常見場景:權限控制。 捕獲/劫持被處理組件的生命周期,常見場景:組件渲染性能追蹤、日志打點。
當我們項目中使用高階組件開發(fā)時,能夠讓代碼變得更加優(yōu)雅,同時增強代碼的復用性和靈活性,提升開發(fā)效率
高階組件的基本框架
高階組件的框架:
export default (WrappedComponent) => {
return class NewComponent extends React.Component {
// 可以自定義邏輯
// 比如給 WrappedComponent組件傳遞props和methods
render () {
return <WrappedComponent {...this.props}/>
}
}
}
如果自定義了state和methods可以通過下面方式傳遞到子組件中
export default (WrappedComponent) => {
return class NewComponent extends React.Component {
state = {
markTime: new Date().toLocaleTimeString(); // 獲取組件當前渲染時的時間
}
printTime() {
let myDate = new Date();
let myTime= myDate.toLocaleTimeString();
console.log('當前時間', myTime)
}
render () {
return <WrappedComponent markTime={this.state.markTime} printTime={this.printTime}/>
}
}
}
這樣在WrappedComponent組件中,如果是類組件就可以通過this.props.markTime獲取,函數(shù)組件的話通過props.markTime來獲取,方法獲取和狀態(tài)獲取相同。
HOC可以做什么
屬性代理——可操作所有傳入的props
可以讀取、添加、編輯、刪除傳給 WrappedComponent 的 props(屬性)
「場景描述」: 給Hello組件傳遞show,hide方法,讓其顯示Loading加載框
const loading = message => OldComponent => {
return class extends React.Component {
// 顯示一個 Loading的div
state = {
show: () => {
let div = document.createElement('div');
div.innerHTML = `<p id="loading" style="position: absolute; z-index:10; top: 10; color: red; border: 1px solid #000">${message}</p>`
document.body.appendChild(div);
},
hide: () => {
document.getElementById('loading').remove();
}
}
render() {
return (
<div>
<OldComponent {...this.props} {...this.state}/>
</div>
)
}
}
}
function Hello(props) {
return (
<div>hello
<button onClick={props.show}>show</button>
<button onClick={props.hide}>hide</button>
</div>
)
}
let HightLoadingHello = loading('正在加載')(Hello);
ReactDom.render(<HightLoadingHello/>, document.getElementById('root'));
效果如圖:

抽離公共組件,最大化實現(xiàn)復用
「場景描述」: 統(tǒng)計每個組件的渲染時間
class CalTimeComponent extends React.Component {
componentWillMount() {
this.start = Date.now(); // 初始渲染節(jié)點
}
componentDidMount() {
console.log((Date.now() - this.start) + 'ms');
}
render() {
return <div>calTimeComponent</div>
}
}
ReactDom.render(<CalTimeComponent/>, document.getElementById('root'));
這樣僅僅能計算當前組件的渲染時間,假如現(xiàn)在有這樣一個需求,需要統(tǒng)計每個組件的渲染時間呢?
就應該想到把它抽離出去,比如:
// CalTimeComponent.js
export default function CalTimeComponent(OldComponent) {
return class extends React.Component {
state = {
markTime: new Date().toLocaleTimeString()
}
componentWillMount() {
this.start = Date.now();
}
componentDidMount() {
console.log((Date.now() - this.start) + 'ms');
}
printTime() {
let myDate = new Date();
let myTime= myDate.toLocaleTimeString();
console.log('當前時間', myTime)
}
render() {
return <OldComponent markTime={this.state.markTime} printTime={this.printTime}/>
}
}
}
// HelloComponent.js
import withTracker from '../../Components/CalTimeComponent.js';
class HelloComponent extends React.Component{
render() {
console.log(this.props);
this.props.printTime()
return <div>hello</div>
}
}
let HighHelloComponent = CalTimeComponent(HelloComponent);
ReactDom.render(<HighHelloComponent/>, document.getElementById('root'));
這樣就能最大化的實現(xiàn)CalTimeComponent組件復用了,把它引入到想要計算時間的組件里,并傳入當前組件就好了。
Render Props
特點1:render props指在一種React組件之間使用一個值為函數(shù)的props共享代碼的簡單技術。
特點2:具有render props的組件接收一個函數(shù),該函數(shù)返回一個React元素并調(diào)用它而不是實現(xiàn)一個自己的渲染邏輯。
特點3:render props是一個用于告知組件需要渲染什么內(nèi)容的函數(shù)(props)
特點4:也是組件邏輯復用的一種實現(xiàn)方式
接下來,我通過一個例子帶大家分別認識上面的四種特點
「場景描述」: 在多個組件內(nèi)實時獲取鼠標的x、y坐標
原生實現(xiàn):不復用邏輯
class MouseTracker extends React.Component {
constructor(props) {
super(props);
this.state = {
x: 0,
y: 0,
}
}
handleMouseMove = (event) => {
this.setState({
x: event.clientX,
y: event.clientY
});
}
render() {
return (
<div onMouseMove={this.handleMouseMove}>
<h1>請移動鼠標</h1>
<p>當前鼠標的位置是:x:{this.state.x} y:{this.state.y}</p>
</div>
)
}
}
ReactDom.render(<MouseTracker/>, document.getElementById('root'));
上面,這是在一個組件內(nèi)完成的,假如現(xiàn)在要在多個div內(nèi)完成上面的邏輯該怎么辦,就該想到復用了,看看render prop是怎么幫我們完成的?
Render Props
class MouseTracker extends React.Component {
constructor(props) {
super(props);
this.state = {
x: 0,
y: 0,
}
}
handleMouseMove = (event) => {
this.setState({
x: event.clientX,
y: event.clientY
});
}
render() {
console.log(this.props)
return (
<div onMouseMove={this.handleMouseMove}>
{this.props.render(this.state)}
</div>
)
}
}
ReactDom.render(
<MouseTracker render={
props => (
<React.Fragment>
<h1>請移動鼠標</h1>
<p>當前鼠標的位置是: x:{props.x} y:{props.y}</p>
</React.Fragment>
)
}></MouseTracker>, document.getElementById('root'));
注意:render props 是因為模式才被稱為 render props ,你不一定要用名為 render 的 props 來使用這種模式。render props 是一個用于告知組件需要渲染什么內(nèi)容的函數(shù) `prop
那如果改寫成高階組件呢?
高階組件寫法
改寫成高階組件,并將公共組件抽離出去, ShowPosition子組件中可以拿到withTracker父組件中傳遞的x、y坐標值
// withTracker.js
export default function withTracker (OldComponent) {
return class MouseTracker extends React.Component {
constructor(props) {
super(props);
this.state = {
x: 0,
y: 0,
}
}
handleMouseMove = (event) => {
this.setState({
x: event.clientX,
y: event.clientY
});
}
render() {
console.log(this.props)
return (
<div onMouseMove={this.handleMouseMove}>
<OldComponent {...this.state}/>
</div>
)
}
}
}
// ShowPosition.js
import withTracker from '../../Components/withTracker.js';
function ShowPosition(props) {
return (
<React.Fragment>
<h1>請移動鼠標</h1>
<p>當前鼠標的位置是: x:{props.x} y:{props.y}</p>
</React.Fragment>
)
}
// 在 ShowPosition 組件中 可以拿到 withTracker 傳遞過來的坐標值
let HightShowPosition = withTracker(ShowPosition);
ReactDom.render(<HightShowPosition/>, document.getElementById('root'));
hoc、render props、react-hooks的優(yōu)劣如何?
HOC的優(yōu)勢:
抽離公共組件,實現(xiàn)組件代碼復用,常見場景:頁面復用。 條件渲染,控制組件的渲染邏輯(渲染劫持),常見場景:權限控制。 捕獲/劫持被處理組件的生命周期,常見場景:組件渲染性能追蹤、日志打點。 屬性代理,可以給一些子組件傳遞層次比較遠的屬性,并按需求操作他們
HOC的缺陷:
擴展性限制: HOC無法從外部訪問子組件(被包裹組件WrappedComponent)的state,因此無法通過shouldComponentUpdate過濾掉不必要的更新(React支持ES6之后,提供了React.pureComponent來解決這個問題)Ref傳遞問題: Ref由于組件被高階組件包裹,導致被隔斷,需要后來的React.forwardRef來解決這個問題層級嵌套: HOC可能出現(xiàn)多層包裹組件的情況(一般不超過兩層,否則不好維護)多層抽象增加了復雜度和理解成本命名沖突:如果高階組件多次嵌套,沒有使用 命名空間的話會產(chǎn)生沖突,覆蓋老屬性
Render Props優(yōu)點:
上述HOC的缺點,Render Props都可以解決
Render Props缺陷:
使用繁瑣: HOC只需要借助裝飾器/高階函數(shù)的特點就可以進行復用,而Render Props需要借助回調(diào)嵌套嵌套過深: Render Props雖然擺脫了組件多層嵌套的問題,但是轉化為了函數(shù)回調(diào)的嵌套
React Hooks的優(yōu)點(重點):
簡潔: React Hooks解決了HOC和Render Props的嵌套問題,更加簡潔解耦: React Hooks可以更方便地把 UI 和狀態(tài)分離,做到更徹底的解耦無影響復用組件邏輯: Hook使你在無需修改組件結構的情況下復用狀態(tài)邏輯函數(shù)友好:React Hooks為函數(shù)組件而生,從而解決了類組件的幾大問題 this 指向容易錯誤 分割在不同聲明周期中的邏輯使得代碼難以理解和維護 代碼復用成本高(高階組件容易使代碼量劇增)
React Hooks的缺陷(重點):
額外的學習成本(Functional Component 與 Class Component 之間的困惑) 寫法上有限制(不能出現(xiàn)在條件、循環(huán)中,只能在最外層調(diào)用 Hooks),并且寫法限制增加了重構成本破壞了 PureComponent、React.memo淺比較的性能優(yōu)化效果(為了取最新的props和state,每次render()都要重新創(chuàng)建事件處函數(shù))(依賴項不變,可解決該問題)使用不當,可能會造成 閉包陷阱問題React.memo并不能完全替代shouldComponentUpdate(因為拿不到 state change,只針對 props change)
關于react-hooks的評價來源于官方react-hooks RFC
感謝大家
如果你覺得這篇內(nèi)容對你挺有啟發(fā),我想邀請你幫我三個小忙:
點個「在看」,讓更多的人也能看到這篇內(nèi)容(喜歡不點在看,都是耍流氓 -_-) 歡迎加我微信「TH0000666」一起交流學習... 關注公眾號「前端Sharing」,持續(xù)為你推送精選好文。
