文章目錄:
- 什麼是 Render Props
- Render Props 的應用
- 什麼是 React Hooks
- React Hooks 的應用
- 總結
什麼是 Render Props
簡而言之,只要一個元件中某個屬性的值是函式,那麼就可以說改元件使用了 Render Props 這種技術。聽起來好像就那麼回事兒,那到底 Render Props 有哪些應用場景呢,讓我們還是從簡單的例子講起,假如我們要實現一個打招呼的元件,一開始可能會這麼實現:
const Greeting = props => (
<div>
<h1>{props.text}</h1>
</div>
);
// 然後這麼使用
<Greeting text="Hello ?!" />
複製程式碼
但是如果在打招呼的時候同時還需要傳送一個表情呢,然後可能會這麼實現:
const Greeting = props => (
<div>
<h1>{props.text}</h1>
<p>{props.emoji}</p>
</div>
);
// how to use
<Greeting text="Hello ?!" emoji="?" />
複製程式碼
然後如果還要加上鍊接呢,又要在 Greeting
元件的內部實現傳送連結的邏輯,很明顯這種方式違背了軟體開發六大原則之一的 開閉原則,即每次修改都要到元件內部需修改。
開閉原則:對修改關閉,對擴充開放。
那有什麼方法可以避免這種方式的修改呢,當然有,也就是接下來要講的 Render Props,不過在此之前,我們先來看一個非常簡單的求和函式:
const sumOf = array => {
const sum = array.reduce((prev, current) => {
prev += current;
return prev;
}, 0);
console.log(sum);
}
複製程式碼
這個函式的功能非常簡單,對陣列求和並列印它。但是如果需要把 sum
通過 alert
顯示出來,是不是又要到 sumOf
內部去修改呢,和上面的 Greeting
類似,是的,這兩個函式存在相同的問題,就是當需求有變是,都需要要函式內部去修改。
對於第二個函式,你可能很快就能想出用 回撥函式 去解決:
const sumOf = (array, done) => {
const sum = array.reduce((prev, current) => {
prev += current;
return prev;
}, 0);
done(sum);
}
sumOf([1, 2, 3], sum => {
console.log(sum);
// or
alert(sum);
})
複製程式碼
會發現回撥函式很完美的解決了之前存在的問題,每次修改,我們只需要在 sumOf
函式的回撥函式中去修改,而不需要到 sumOf
內部去修改。
反觀 React 元件 Greeting
,要解決前面遇到的問題,其實和 sumOf
的回撥函式一樣:
const Greeting = props => {
return props.render(props);
};
// how to use
<Greeting
text="Hello ?!"
emoji="?"
link="link here"
render={(props) => (
<div>
<h1>{props.text}</h1>
<p>{props.emoji}</p>
<a href={props.link}></a>
</div>
)}></Greeting>
複製程式碼
類比之前的 sumOf
是不是非常的相似,簡直就是一毛一樣:
sumOf
中通過執行回撥函式done
並把sum
傳入其中,此時只要在sumOf
函式的第二個引數中傳入一個函式即可獲得sum
的值,進而做一寫定製化的需求Greeting
中通過執行回撥函式props.render
並把props
傳入其中,此時只要在Greeting
元件的render
屬性中傳入一個函式即可獲得props
的值並返回你所需要的 UI
值得一提的是,並不是只有在 render
屬性中傳入函式才能叫 Render Props,實際上任何屬性只要它的值是函式,都可稱之為 Render Props,比如上面這個例子把 render
屬性名改成 children
的話使用上其實更為簡便:
const Greeting = props => {
return props.children(props);
};
// how to use
<Greeting text="Hello ?!" emoji="?" link="link here">
{(props) => (
<div>
<h1>{props.text}</h1>
<p>{props.emoji}</p>
<a href={props.link}></a>
</div>
)}
</Greeting>
複製程式碼
這樣就可以直接在 Greeting
標籤內寫函式了,比起之前在 render
中更為直觀。
所以,React 中的 Render Props 你可以把它理解成 JavaScript 中的回撥函式。
Render Props 的應用
上面簡單介紹了什麼是 Render Props,那麼在實際開發中 Render Props 具體有什麼實際應用呢,簡單來講 Render Props 所解決的問題和 高階元件 所解決的問題類似,都是為了 解決程式碼複用的問題。
如果對高階元件不熟悉的話,可以看一下筆者之前寫的 React 中的高階元件及其應用場景。
簡單實現一個「開關」功能的元件:
class Switch extends React.Component {
constructor(props) {
super(props);
this.state = {
on: props.initialState || false,
};
}
toggle() {
this.setState({
on: !this.state.on,
});
}
render() {
return (
<div>{this.props.children({
on,
toggle: this.toggle,
})}</div>
);
}
}
// how to use
const App = () => (
<Switch initialState={false}>{({on, toggle}) => {
<Button onClick={toggle}>Show Modal</Button>
<Modal visible={on} onSure={toggle}></Modal>
}}</Switch>
);
複製程式碼
這是一個簡單的 複用顯隱模態彈窗邏輯 的元件,比如要顯示 OtherModal
就直接替換 Modal
就行了,達到複用「開關」邏輯程式碼的目的。
Render Props 更像是 控制反轉(IoC),它只負責定義介面或資料並通過函式引數傳遞給你,具體怎麼使用這些介面或者資料完全取決於你。
如果對控制反轉不熟悉的話,可以看一下筆者之前寫的 前端中的 IoC 理念
Render Props VS HOC
前面提到過 Render Props 所解決的問題和 高階元件 所解決的問題類似,都是為了 解決程式碼複用的問題,那它們有什麼區別呢,讓我們來簡單分析一下它們各自的特點:
HOC
缺點:
- 由於可能會多次巢狀高階元件,而我們又很難確保每個高階元件中的屬性名是不同的,所以 屬性容易被覆蓋。
- 當在使用高階元件的時候,高階元件相當於一個 黑盒,我們必須去看如何實現才能去使用它:
優點:
- 可以使用
compose
方法合併多個高階元件然後在使用
// 不要這麼使用
const EnhancedComponent = withRouter(connect(commentSelector)(WrappedComponent));
// 可以使用一個 compose 函式組合這些高階元件
// lodash, redux, ramda 等第三方庫都提供了類似 `compose` 功能的函式
const enhance = compose(withRouter, connect(commentSelector));
const EnhancedComponent = enhance(WrappedComponent);
複製程式碼
- 呼叫方便(ES6 + 裝飾器語法)
@withData
class App extends React.Component {}
複製程式碼
Render Props
- 缺點
- 巢狀過深也會形成 地獄回撥
- 優點
- 解決了 HOC 的缺點
Render Props 和 HOC 並不是非此即彼的關係,明白它們各自的優缺點之後,我們就可以在合適的場景下使用適合的方式去實現了。
什麼是 React Hooks
React Hooks 是 React 16.8 版本推出的新特性,讓函式式元件也可以和類風格元件一樣擁有(類似)「生命週期」,進而更好的在函式式元件中發揮 React 的特性。
React 團隊推出 Hooks 的目的和前面提到的 高階元件、Render Props 一樣,都是為了程式碼複用。
在瞭解 React Hooks 之前我們先拿前面 Render Props 的 Switch
例子做個對比:
class Switch extends React.Component {
constructor(props) {
super(props);
this.state = {
on: props.initialState || false,
};
}
toggle() {
this.setState({
on: !this.state.on,
});
}
render() {
return (
<div>{this.props.children({
on,
toggle: this.toggle,
})}</div>
);
}
}
// how to use
const App = () => (
<Switch initialState={false}>{({on, toggle}) => {
<Button onClick={toggle}>Show Modal</Button>
<Modal visible={on} onSure={toggle}></Modal>
}}</Switch>
);
// use hooks
const App = () => {
const [on, setOn] = useState(false);
return (
<div>
<Button onClick={() => setOn(true)}>Show Modal</Button>
<Modal visible={on} onSure={() => setOn(false)}></Modal>
</div>
);
}
複製程式碼
通過對比我們很容易發現 Hooks 版本只用了幾個簡單的 API (useState
, setOn
, on
) 就幹掉了 Switch
類元件 20 行程式碼,極大的減少了程式碼量。
程式碼量減少只是 Hooks 所帶來的好處之一,而它更重要的目的是 狀態邏輯複用。(高階元件和 Render Props 也是一樣,狀態邏輯複用其實就是程式碼複用的更具體的說法)
Hooks 的幾個優點:
- 雖然 Hooks 的目的和高階元件、Render Props 一樣,都是為了程式碼複用,但是 Hooks 較高階元件和 Render Props 更為簡單明瞭,且不會造成巢狀地獄。
- 能更容易的把 UI 和狀態分離
- 一個 Hooks 中可以引用另外的 Hooks
- 解決類元件的痛點
this
指向容易錯誤- 分割在不同宣告週期中的邏輯使得程式碼難以理解和維護
- 程式碼複用成本高(高階元件容易使程式碼量劇增)
React Hooks 的應用
React 官方提供了以下幾個常用的鉤子:
- 基礎鉤子:
useState
、useEffect
、useContext
- 附加鉤子:
useReducer
、useCallback
、useMemo
、useRef
、useImperativeHandle
、useLayoutEffect
、useDebugValue
useEffect
useEffect
鉤子的作用正如其名 —— 為了處理比如 訂閱、資料獲取、DOM 操作 等等一些副作用。它的作用與 componentDidMount
, componentDidUpdate
和 componentWillUnmount
這些生命週期函式類似。
比如我們要監聽輸入框 input
的輸入,用 useEffect
我們可以這麼實現:
function Input() {
const [text, setText] = useState('')
function onChange(event) {
setText(event.target.value)
}
useEffect(() => {
// 類似於 componentDidMount 和 componentDidUpdate 兩個生命週期函式
const input = document.querySelector('input')
input.addEventListener('change', onChange);
return () => {
// 類似於 componentWillUnmount
input.removeEventListener('change', onChange);
}
})
return (
<div>
<input onInput={onChange} />
<p>{text}</p>
</div>
)
}
複製程式碼
useEffect
鉤子的用法就是把函式作為第一個引數傳入 useEffect
中,在該傳入的函式中我們就可以做一些 有副作用 的事情了,比如操作 DOM 等等。
如果傳入 useEffect
方法的函式返回了一個函式,該 返回的函式 會在元件即將解除安裝時呼叫,我們可以在這裡做一些比如清除 timerID 或者取消之前釋出的訂閱等等一些清除操作,下面這麼寫可能比較直觀:
useEffect(function didUpdate() {
// do something effects
return function unmount() {
// cleaning up effects
}
})
複製程式碼
當 useEffect
只傳入一個引數時,每次 render
之後都會執行 useEffect
函式:
useEffect(() => {
// render 一次,執行一次
console.log('useEffect');
})
複製程式碼
當 useEffect
傳入第二個引數是陣列時,只有當陣列的值(依賴)發生變化時,傳入回撥函式才會執行,比如下面這種情況:
雖然 React 的 diff 演算法在 DOM 渲染時只會更新變化的部分,但是卻無法識別到
useEffect
內的變化,所以需要開發者通過第二個引數告訴 React 用到了哪些外部變數。
useEffect(() => {
document.title = title
}, [title])
複製程式碼
因為 useEffect
回撥內部用到了外部的 title
變數,所以如果需要僅當 title
值改變時才執行回撥的話,只需在第二個引數中傳入一個陣列,並把內部所依賴的變數寫在陣列中,此時如果 title
值改變了的話,useEffect
回撥內部就可以通過傳入的依賴判斷是否需要執行回撥。
所以如果給 useEffect
第二個引數傳入一個空陣列的話,useEffect
的回撥函式只會在首次渲染之後執行一次:
useEffect(() => {
// 只會在首次 render 之後執行一次
console.log('useEffect')
}, [])
複製程式碼
useContext
React 中有個 context
的概念,讓我們可以 跨元件共享狀態,無需通過 props
層層傳遞,一個簡單的例子:
redux 就是利用 React 的
context
的特性實現跨元件資料共享的。
const ThemeContext = React.createContext();
function App() {
const theme = {
mode: 'dark',
backgroundColor: '#333',
}
return (
<ThemeContext.Provider value={theme}>
<Display />
</ThemeContext.Provider>
)
}
function Display() {
return (
<ThemeContext.Consumer>
{({backgroundColor}) => <div style={{backgroundColor}}>Hello Hooks.</div>}
</ThemeContext.Consumer>
)
}
複製程式碼
下面是 useContext
版本:
function Display() {
const { backgroundColor } = useContext(ThemeContext);
return (<div style={{backgroundColor}}>Hello Hooks.</div>)
}
複製程式碼
巢狀版 Consumer
:
function Header() {
return (
<CurrentUser.Consumer>
{user =>
<Notifications.Consumer>
{notifications =>
<header>
Hello {user.name}!
You have {notifications.length} notifications.
</header>
}
</Notifications.Consumer>
}
</CurrentUser.Consumer>
);
}
複製程式碼
用 useContext
拍平:
function Header() {
const user = useContext(CurrentUser)
const notifications = useContext(Notifications)
return (
<header>
Hello {user.name}!
You have {notifications.length} notifications.
</header>
)
}
複製程式碼
emm... 這效果有點類似用 async
和 await
改造地獄回撥的感覺。
以上就是 React 的基礎 Hooks。對於其他官方提供的 Hooks 如果感興趣建議直接閱讀文件,本文就不一一介紹了。
使用中需要注意的點
React Hooks 在帶給我們方便的同時,我們也需要遵循它們的一些約定,不然效果只會變得適得其反:
- 避免在 迴圈、條件判斷、巢狀函式 中呼叫 Hooks,保證呼叫順序的穩定;
- 只有 函式定義元件 和 Hooks 可以呼叫 Hooks,避免在 類元件 或者 普通函式 中呼叫;
- 不能在
useEffect
中使用useState
,React 會報錯提示; - 類元件不會被替換或廢棄,不需要強制改造類元件,兩種方式能並存
總結
Render Props 的一個核心思想就是在元件內部通過呼叫 this.props.children()
(當然可以是其他任意值是函式屬性名)把元件內部的一些狀態傳遞出去(參考回撥函式),然後在元件外部對應屬性的函式中通過函式的引數來獲取元件內部的狀態,進而在該函式中處理相應的 UI 或邏輯。而 Hooks 有點像是 Render Props 的 拍平版 (參考前面 useContext
)栗子。
目前為止,介紹了 React 程式碼複用的三種方式:
- Render Props
- Hooks
- HOC
通過對比發現,Hooks 的方式的優勢最大,解決解決了另外兩種方式的一些痛點,所以建議使用。
相關閱讀: