首先歡迎你們關注個人Github博客,也算是對個人一點鼓勵,畢竟寫東西無法得到變現,能堅持下去也是靠的是本身的熱情和你們的鼓勵,但願你們多多關注呀!React 16.8中新增了Hooks特性,而且在React官方文檔中新增長了Hooks模塊介紹新特性,可見React對Hooks的重視程度,若是你還不清楚Hooks是什麼,強烈建議你瞭解一下,畢竟這可能真的是React將來的發展方向。 javascript
React一直以來有兩種建立組件的方式: Function Components(函數組件)與Class Components(類組件)。函數組件只是一個普通的JavaScript函數,接受props
對象並返回React Element。在我看來,函數組件更符合React的思想,數據驅動視圖,不含有任何的反作用和狀態。在應用程序中,通常只有很是基礎的組件纔會使用函數組件,而且你會發現隨着業務的增加和變化,組件內部可能必需要包含狀態和其餘反作用,所以你不得不將以前的函數組件改寫爲類組件。但事情每每並無這麼簡單,類組件也沒有咱們想象的那麼美好,除了徒增工做量以外,還存在其餘種種的問題。java
首先類組件共用狀態邏輯很是麻煩。好比咱們借用官方文檔中的一個場景,FriendStatus組件用來顯示朋友列表中該用戶是否在線。react
class FriendStatus extends React.Component {
constructor(props) {
super(props);
this.state = { isOnline: null };
this.handleStatusChange = this.handleStatusChange.bind(this);
}
componentDidMount() {
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentWillUnmount() {
ChatAPI.unsubscribeFromFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
handleStatusChange(status) {
this.setState({
isOnline: status.isOnline
});
}
render() {
if (this.state.isOnline === null) {
return 'Loading...';
}
return this.state.isOnline ? 'Online' : 'Offline';
}
}
複製代碼
上面FriendStatus組件會在建立時主動訂閱用戶狀態,並在卸載時會退訂狀態防止形成內存泄露。假設又出現了一個組件也須要去訂閱用戶在線狀態,若是想用複用該邏輯,咱們通常會使用render props
和高階組件來實現狀態邏輯的複用。git
// 採用render props的方式複用狀態邏輯
class OnlineStatus extends React.Component {
constructor(props) {
super(props);
this.state = { isOnline: null };
this.handleStatusChange = this.handleStatusChange.bind(this);
}
componentDidMount() {
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentWillUnmount() {
ChatAPI.unsubscribeFromFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
handleStatusChange(status) {
this.setState({
isOnline: status.isOnline
});
}
render() {
const {isOnline } = this.state;
return this.props.children({isOnline})
}
}
class FriendStatus extends React.Component{
render(){
return (
<OnlineStatus friend={this.props.friend}> { ({isOnline}) => { if (isOnline === null) { return 'Loading...'; } return isOnline ? 'Online' : 'Offline'; } } </OnlineStatus>
);
}
}
複製代碼
// 採用高階組件的方式複用狀態邏輯
function withSubscription(WrappedComponent) {
return class extends React.Component {
constructor(props) {
super(props);
this.state = { isOnline: null };
this.handleStatusChange = this.handleStatusChange.bind(this);
}
componentDidMount() {
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentWillUnmount() {
ChatAPI.unsubscribeFromFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
handleStatusChange(status) {
this.setState({
isOnline: status.isOnline
});
}
render() {
return <WrappedComponent isOnline={this.state.isOnline}/> } } } const FriendStatus = withSubscription(({isOnline}) => { if (isOnline === null) { return 'Loading...'; } return isOnline ? 'Online' : 'Offline'; }) 複製代碼
上面兩種複用狀態邏輯的方式不只須要費時費力地重構組件,並且Devtools查看組件的層次結構時,會發現組件層級結構變深,當複用的狀態邏輯過多時,也會陷入組件嵌套地獄(wrapper hell)的狀況。可見上述兩種方式並不能完美解決狀態邏輯複用的問題。github
不只如此,隨着類組件中業務邏輯逐漸複雜,維護難度也會逐步提高,由於狀態邏輯會被分割到不一樣的生命週期函數中,例如訂閱狀態邏輯位於componentDidMount
,取消訂閱邏輯位於componentWillUnmount
中,相關邏輯的代碼相互割裂,而邏輯不相關的代碼反而有可能集中在一塊兒,總體都是不利於維護的。而且相好比函數式組件,類組件學習更爲複雜,你須要時刻提防this
在組件中的陷阱,永遠不能忘了爲事件處理程序綁定this
。如此種種,看來函數組件仍是有特有的優點的。數組
函數式組件一直以來都缺少類組件諸如狀態、生命週期等種種特性,而Hooks的出現就是讓函數式組件擁有類組件的特性。官方定義:緩存
Hooks are functions that let you 「hook into」 React state and lifecycle features from function components.app
要讓函數組件擁有類組件的特性,首先就要實現狀態state
的邏輯。ide
useState
就是React提供最基礎、最經常使用的Hook,主要用來定義本地狀態,咱們以一個最簡單的計數器爲例:函數
import React, { useState } from 'react'
function Example() {
const [count, setCount] = useState(0);
return (
<div> <span>{count}</span> <button onClick={()=> setCount(count + 1)}>+</button> <button onClick={() => setCount((count) => count - 1)}>-</button> </div>
);
}
複製代碼
useState
能夠用來定義一個狀態,與state不一樣的是,狀態不只僅能夠是對象,並且能夠是基礎類型值,例如上面的Number類型的變量。useState
返回的是一個數組,第一個是當前狀態的實際值,第二個用於更改該狀態的函數,相似於setState
。更新函數與setState
相同的是均可以接受值和函數兩種類型的參數,與useState
不一樣的是,更新函數會將狀態替換(replace)而不是合併(merge)。
函數組件中若是存在多個狀態,既能夠經過一個useState
聲明對象類型的狀態,也能夠經過useState
屢次聲明狀態。
// 聲明對象類型的狀態
const [count, setCount] = useState({
count1: 0,
count2: 0
});
// 屢次聲明
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);
複製代碼
相比於聲明對象類型的狀態,明顯屢次聲明狀態的方式更加方便,主要是由於更新函數是採用的替換的方式,所以你必須給參數中添加未變化的屬性,很是的麻煩。須要注意的是,React是經過Hook調用的次序來記錄各個內部狀態的,所以Hook不能在條件語句(如if)或者循環語句中調用,並在須要注意的是,咱們僅能夠在函數組件中調用Hook,不能在組件和普通函數中(除自定義Hook)調用Hook。
當咱們要在函數組件中處理複雜多層數據邏輯時,使用useState就開始力不從心,值得慶幸的是,React爲咱們提供了useReducer來處理函數組件中複雜狀態邏輯。若是你使用過Redux,那麼useReducer可謂是很是的親切,讓咱們用useReducer重寫以前的計數器例子:
import React, { useReducer } from 'react'
const reducer = function (state, action) {
switch (action.type) {
case "increment":
return { count : state.count + 1};
case "decrement":
return { count: state.count - 1};
default:
return { count: state.count }
}
}
function Example() {
const [state, dispatch] = useReducer(reducer, { count: 0 });
const {count} = state;
return (
<div> <span>{count}</span> <button onClick={()=> dispatch({ type: "increment"})}>+</button> <button onClick={() => dispatch({ type: "decrement"})}>-</button> </div>
);
}
複製代碼
useReducer接受兩個參數: reducer函數和默認值,並返回當前狀態state和dispatch函數的數組,其邏輯與Redux基本一致。useReducer和Redux的區別在於默認值,Redux的默認值是經過給reducer函數賦值默認參數的方式給定,例如:
// Redux的默認值邏輯
const reducer = function (state = { count: 0 }, action) {
switch (action.type) {
case "increment":
return { count : state.count + 1};
case "decrement":
return { count: state.count - 1};
default:
return { count: state.count }
}
}
複製代碼
useReducer之因此沒有采用Redux的邏輯是由於React認爲state的默認值多是來自於函數組件的props,例如:
function Example({initialState = 0}) {
const [state, dispatch] = useReducer(reducer, { count: initialState });
// 省略...
}
複製代碼
這樣就能實現經過傳遞props來決定state的默認值,固然React雖然不推薦Redux的默認值方式,但也容許你相似Redux的方式去賦值默認值。這就要接觸useReducer的第三個參數: initialization。
顧名思義,第三個參數initialization是用來初始化狀態,當useReducer初始化狀態時,會將第二個參數initialState傳遞initialization函數,initialState函數返回的值就是state的初始狀態,這也就容許在reducer外抽象出一個函數專門負責計算state的初始狀態。例如:
const initialization = (initialState) => ({ count: initialState })
function Example({initialState = 0}) {
const [state, dispatch] = useReducer(reducer, initialState, initialization);
// 省略...
}
複製代碼
因此藉助於initialization函數,咱們就能夠模擬Redux的初始值方式:
import React, { useReducer } from 'react'
const reducer = function (state = {count: 0}, action) {
// 省略...
}
function Example({initialState = 0}) {
const [state, dispatch] = useReducer(reducer, undefined, reducer());
// 省略...
}
複製代碼
解決了函數組件中內部狀態的定義,接下來亟待解決的函數組件中生命週期函數的問題。在函數式思想的React中,生命週期函數是溝通函數式和命令式的橋樑,你能夠在生命週期中執行相關的反作用(Side Effects),例如: 請求數據、操做DOM等。React提供了useEffect來處理反作用。例如:
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`
return () => {
console.log('clean up!')
}
});
return (
<div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}> Click me </button> </div>
);
}
複製代碼
在上面的例子中咱們給useEffect
傳入了一個函數,並在函數內根據count值更新網頁標題。咱們會發現每次組件更新時,useEffect中的回調函數都會被調用。所以咱們能夠認爲useEffect是componentDidMount和componentDidUpdate結合體。當組件安裝(Mounted)和更新(Updated)時,回調函數都會被調用。觀察上面的例中,回調函數返回了一個函數,這個函數就是專門用來清除反作用,咱們知道相似監聽事件的反作用在組件卸載時應該及時被清除,不然會形成內存泄露。清除函數會在每次組件從新渲染前調用,所以執行順序是:
render -> effect callback -> re-render -> clean callback -> effect callback
所以咱們可使用useEffect
模擬componentDidMount、componentDidUpdate、componentWillUnmount行爲。以前咱們提到過,正是由於生命週期函數,咱們無可奈何將相關的代碼拆分到不一樣的生命週期函數,反而將不相關的代碼放置在同一個生命週期函數,之因此會出現這個狀況,主要問題在於咱們並非依據於業務邏輯書寫代碼,而是經過執行時間編碼。爲了解決這個問題,咱們能夠經過建立多個Hook,將相關邏輯代碼放置在同一個Hook來解決上述問題:
import React, { useState, useEffect } from 'react';
function Example() {
useEffect(() => {
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return function cleanup() {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
useEffect(() => {
otherAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return function cleanup() {
otherAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
// 省略...
}
複製代碼
咱們經過多個Hook來集中邏輯關注點,避免不相關的代碼糅雜而出現的邏輯混亂。可是隨之而來就遇到一個問題,假設咱們的某個行爲肯定是要在區分componentDidUpdate
或者componentDidMount
時才執行,useEffect
是否能區分。好在useEffect
爲咱們提供了第二個參數,若是第二個參數傳入一個數組,僅當從新渲染時數組中的值發生改變時,useEffect
中的回調函數纔會執行。所以若是咱們向其傳入一個空數組,則能夠模擬生命週期componentDidMount
。可是若是你想僅模擬componentDidUpdate
,目前暫時未發現什麼好的方法。
useEffect
與類組件生命週期不一樣的是,componentDidUpdate
和componentDidMount
都是在DOM更新後同步執行的,但useEffect
並不會在DOM更新後同步執行,也不會阻塞更新界面。若是須要模擬生命週期同步效果,則須要使用useLayoutEffect
,其使用方法和useEffect
相同,區域只在於執行時間上。
藉助Hook:useContext
,咱們也能夠在函數組件中使用context
。相比於在類組件中須要經過render props的方式使用,useContext
的使用則至關方便。
import { createContext } from 'react'
const ThemeContext = createContext({ color: 'color', background: 'black'});
function Example() {
const theme = useContext(Conext);
return (
<p style={{color: theme.color}}>Hello World!</p>
);
}
class App extends Component {
state = {
color: "red",
background: "black"
};
render() {
return (
<Context.Provider value={{ color: this.state.color, background: this.state.background}}> <Example/> <button onClick={() => this.setState({color: 'blue'})}>color</button> <button onClick={() => this.setState({background: 'blue'})}>backgroud</button> </Context.Provider> ); } } 複製代碼
useContext
接受函數React.createContext
返回的context對象做爲參數,返回當前context中值。每當Provider
中的值發生改變時,函數組件就會從新渲染,須要注意的是,即便的context的未使用的值發生改變時,函數組件也會從新渲染,正如上面的例子,Example
組件中即便沒有使用過background
,但background
發生改變時,Example
也會從新渲染。所以必要時,若是Example
組件還含有子組件,你可能須要添加shouldComponentUpdate
防止沒必要要的渲染浪費性能。
useRef
經常使用在訪問子元素的實例:
function Example() {
const inputEl = useRef();
const onButtonClick = () => {
inputEl.current.focus();
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}
複製代碼
上面咱們說了useRef
經常使用在ref
屬性上,實際上useRef
的做用不止於此
const refContainer = useRef(initialValue)
useRef
能夠接受一個默認值,並返回一個含有current
屬性的可變對象,該可變對象會將持續整個組件的生命週期。所以能夠將其當作類組件的屬性同樣使用。
useImperativeHandle
用於自定義暴露給父組件的ref
屬性。須要配合forwardRef
一塊兒使用。
function Example(props, ref) {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
}
}));
return <input ref={inputRef} />; } export default forwardRef(Example); 複製代碼
class App extends Component {
constructor(props){
super(props);
this.inputRef = createRef()
}
render() {
return (
<>
<Example ref={this.inputRef}/>
<button onClick={() => {this.inputRef.current.focus()}}>Click</button>
</>
);
}
}
複製代碼
熟悉React的同窗見過相似的場景:
class Example extends React.PureComponent{
render(){
// ......
}
}
class App extends Component{
render(){
return <Example onChange={() => this.setState()}/> } } 複製代碼
其實在這種場景下,雖然Example
繼承了PureComponent
,但實際上並不可以優化性能,緣由在於每次App
組件傳入的onChange
屬性都是一個新的函數實例,所以每次Example
都會從新渲染。通常咱們爲了解決這個狀況,通常會採用下面的方法:
class App extends Component{
constructor(props){
super(props);
this.onChange = this.onChange.bind(this);
}
render(){
return <Example onChange={this.onChange}/> } onChange(){ // ... } } 複製代碼
經過上面的方法一併解決了兩個問題,首先保證了每次渲染時傳給Example
組件的onChange
屬性都是同一個函數實例,而且解決了回調函數this
的綁定。那麼如何解決函數組件中存在的該問題呢?React提供useCallback
函數,對事件句柄進行緩存。
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
複製代碼
useCallback接受函數和一個數組輸入,並返回的一個緩存版本的回調函數,僅當從新渲染時數組中的值發生改變時,纔會返回新的函數實例,這也就解決咱們上面提到的優化子組件性能的問題,而且也不會有上面繁瑣的步驟。
與useCallback
相似的是,useMemo
返回的是一個緩存的值。
const memoizedValue = useMemo(
() => complexComputed(),
[a, b],
);
複製代碼
也就是僅當從新渲染時數組中的值發生改變時,回調函數纔會從新計算緩存數據,這可使得咱們避免在每次從新渲染時都進行復雜的數據計算。所以咱們能夠認爲:
useCallback(fn, input)
等同於useMemo(() => fn, input)
若是沒有給useMemo
傳入第二個參數,則useMemo
僅會在收到新的函數實例時,才從新計算,須要注意的是,React官方文檔提示咱們,useMemo
僅能夠做爲一種優化性能的手段,不能當作語義上的保證,這就是說,也會React在某些狀況下,即便數組中的數據未發生改變,也會從新執行。
咱們前面講過,Hook只能在函數組件的頂部調用,不能再循環、條件、普通函數中使用。咱們前面講過,類組件想要共享狀態邏輯很是麻煩,必需要藉助於render props和HOC,很是的繁瑣。相比於次,React容許咱們建立自定義Hook來封裝共享狀態邏輯。所謂的自定義Hook是指以函數名以use
開頭並調用其餘Hook的函數。咱們用自定義Hook來重寫剛開始的訂閱用戶狀態的例子:
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null);
function handleStatusChange(isOnline) {
setIsOnline(isOnline);
}
useEffect(() => {
ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
};
});
return isOnline;
}
function FriendStatus() {
const isOnline = useFriendStatus();
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
複製代碼
咱們用自定義Hook重寫了以前的訂閱用戶在線狀態的例子,相比於render prop和HOC複雜的邏輯,自定義Hook更加的簡潔,不只於此,自定義Hook也不會引發以前咱們說提到過的組件嵌套地獄(wrapper hell)的狀況。優雅的解決了以前類組件複用狀態邏輯困難的狀況。
藉助於Hooks,函數組件已經能基本實現絕大部分的類組件的功能,不只於此,Hooks在共享狀態邏輯、提升組件可維護性上有具備必定的優點。能夠預見的是,Hooks頗有多是React可預見將來大的方向。React官方對Hook採用的是逐步採用策略(Gradual Adoption Strategy),並表示目前沒有計劃會將class從React中剔除,可見Hooks會很長時間內和咱們的現有代碼並行工做,React並不建議咱們所有用Hooks重寫以前的類組件,而是建議咱們在新的組件或者非關鍵性組件中使用Hooks。 若有表述不周之處,虛心接受批評指教。願你們一同進步!