文章目錄:前端
簡而言之,只要一個組件中某個屬性的值是函數,那麼就能夠說改組件使用了 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
組件的內部實現發送連接的邏輯,很明顯這種方式違背了軟件開發六大原則之一的 開閉原則,即每次修改都要到組件內部需修改。redux
開閉原則:對修改關閉,對拓展開放。數組
那有什麼方法能夠避免這種方式的修改呢,固然有,也就是接下來要講的 Render Props,不過在此以前,咱們先來看一個很是簡單的求和函數:app
const sumOf = array => {
const sum = array.reduce((prev, current) => {
prev += current;
return prev;
}, 0);
console.log(sum);
}
複製代碼
這個函數的功能很是簡單,對數組求和並打印它。可是若是須要把 sum
經過 alert
顯示出來,是否是又要到 sumOf
內部去修改呢,和上面的 Greeting
相似,是的,這兩個函數存在相同的問題,就是當需求有變是,都須要要函數內部去修改。async
對於第二個函數,你可能很快就能想出用 回調函數 去解決:ide
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
的回調函數同樣:post
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 所解決的問題和 高階組件 所解決的問題相似,都是爲了 解決代碼複用的問題。
若是對高階組件不熟悉的話,能夠看一下筆者以前寫的 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 所解決的問題和 高階組件 所解決的問題相似,都是爲了 解決代碼複用的問題,那它們有什麼區別呢,讓咱們來簡單分析一下它們各自的特色:
缺點:
優勢:
compose
方法合併多個高階組件而後在使用// 不要這麼使用
const EnhancedComponent = withRouter(connect(commentSelector)(WrappedComponent));
// 可使用一個 compose 函數組合這些高階組件
// lodash, redux, ramda 等第三方庫都提供了相似 `compose` 功能的函數
const enhance = compose(withRouter, connect(commentSelector));
const EnhancedComponent = enhance(WrappedComponent);
複製代碼
@withData
class App extends React.Component {}
複製代碼
Render Props 和 HOC 並非非此即彼的關係,明白它們各自的優缺點以後,咱們就能夠在合適的場景下使用適合的方式去實現了。
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 的幾個優勢:
this
指向容易錯誤React 官方提供瞭如下幾個經常使用的鉤子:
useState
、useEffect
、useContext
useReducer
、useCallback
、useMemo
、useRef
、useImperativeHandle
、useLayoutEffect
、useDebugValue
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')
}, [])
複製代碼
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 在帶給咱們方便的同時,咱們也須要遵循它們的一些約定,否則效果只會變得拔苗助長:
useEffect
中使用 useState
,React 會報錯提示;Render Props 的一個核心思想就是在組件內部經過調用 this.props.children()
(固然能夠是其餘任意值是函數屬性名)把組件內部的一些狀態傳遞出去(參考回調函數),而後在組件外部對應屬性的函數中經過函數的參數來獲取組件內部的狀態,進而在該函數中處理相應的 UI 或邏輯。而 Hooks 有點像是 Render Props 的 拍平版 (參考前面 useContext
)栗子。
目前爲止,介紹了 React 代碼複用的三種方式:
經過對比發現,Hooks 的方式的優點最大,解決解決了另外兩種方式的一些痛點,因此建議使用。
相關閱讀: