React16 新生命週期和 Hooks 介紹

Jad123123發表於2019-03-05

[TOC]

React16 新特性總結

生命週期函式

React16 新生命週期和 Hooks 介紹

不建議使用的生命週期函式

  • componentWillReceiveProps
      React 團隊對於捨棄 componentWillReceiveProps 函式的解釋是開發者錯誤地在該函式中引入 side effect(副作用,呼叫外部函式等),導致元件重複多次執行 Updating。正確的做法應該是將 Updating 中的 side effect 都放在 componentDidUpdate 生命週期函式中進行。   另外我個人覺得 componentWillReceiveProps 的功能沒有一個明確的定位,往往用於實現通過 props 更新 state 的功能。但 componengWillReceiveProps 只在 Updating 中 props 更新時被呼叫,無法覆蓋所有場景,比如 Mounting 時需要在 contructor 中用 props 初始化 state。故需要使用多個生命週期函式才能實現 props 更新 state 這一單一功能。而 React16 中新提出來的 getDerivedStateFromProps 函式覆蓋了所有 state 更新的場景。
  • componentWillUpdate   React 捨棄 componentWillUpdate 函式的解釋與 componentWillReceiveProps 的解釋一致。開發者可以將 componentWillUpdate 中的一些 side effects 放置在 componentDidUpdate中。如果開發者想在元件更新前獲取 DOM 的資訊,可以通過 React16 中新提出來的 getSnapshotBeforeUpdate 函式來實現。
  • componentWillMount   React 團隊 React16 中提供的最核心的 async rendering (非同步渲染)功能(目前暫未釋出)中,元件的渲染可能會被中斷。所以倘若開發者在 componentWillMount 進行了事件繫結,在 componentWillUnmount 中取消事件繫結的話,就可能發生因為非同步渲染取消,因而沒有呼叫 componentWillUnmount 函式,從而導致事件回撥函式物件的記憶體洩漏。
  • 小結   從數量上可以看出,React 對類元件的生命週期函式進行了精簡。主要取消了 render 階段中,容易讓開發者引入 side effects 的生命週期函式。這樣做的主要目的是為了確保 render 階段順利執行,不會因為 side effects 帶來的重複執行 render 階段。從而強迫開發者將 side effects 移入 commit 階段的生命週期函式中。

static getDerivedStateFromProps(props, state)

  新的生命週期函式 getDerivedStateFromProps 位於 Mounting 掛載階段和由 props 更新觸發的 Updating 階段。getDerivedStateFromProps 主要用於替換 componentWillReceiveProps 函式,其功能更加明確,就是根據 props 更新元件的 state。

class Demo extends React.Component{
	state = {
		tmpA: 1,
		tmpB: 'test',
		lastA: null,
		lastB: null			
	};
		
	static getDerivedStateFromProps(props, state){
		if(props.a !== state.lastA){
			return {
				tmpA: props.a,
				lastA: props.a
			}
		}
		return null;
	}

	render(){
		return <div>
			<input 
				value={this.state.tmpA}
				onChange={e => {
					this.setState({tmpA: e.target.value})
				}}
			/>
		</div>
	}
}
複製程式碼

需要注意的幾點是:

  • getDerivedStateFromProps 為類的靜態方法,無法使用 this 關鍵字,此舉也降低了開發者在該函式中引入 side effects 的可能。
  • getDerivedStateFromProps 的引數 props 為將要更新的 props(nextProps)。由於函式中無法使用 this 關鍵字,需要訪問 this.props 的話,可以將 props 中需要用於更新 state 的欄位記錄在 state 中(如程式碼中的 lastA 和 lastB)。
  • getDerivedStateFromProps 返回的物件和跟 state 進行合併,更新後的 state 用於 render。如果 state 不需要更新,則必須返回 null。
  • 其實 React 官方並不提倡使用 derived state,因為這樣會使類元件邏輯變得複雜程式碼臃腫。對於需要根據 props 更新 state 的需求,React 官方建議優化元件的設計來避免。個人總結主要有以下兩種方式:
    • 直接將元件拆分為完全受控元件,去掉元件的 state
    • 對於非受控元件,更新 props 時通過修改元件的 key 值,讓元件重新掛載

getSnapshotBeforeUpdate(prevProps, prevState)

   getSnapshotBeforeUpdate 函式呼叫於 render 函式之後 componentDidUpdate 函式之前,主要用於獲取更新前的 DOM 元素的資訊。關於該函式的用法,React 的官方示例為:

```javascript
//這是一個帶滾動條的列表元件
class ScrollingList extends React.Component {
  constructor(props) {
	  super(props);
	  this.listRef = React.createRef();
  }

  getSnapshotBeforeUpdate(prevProps, prevState) {
	  //如果此次更新中,列表變長,則記錄更新前滾動的位置
	  //並作為componentDidUpdate函式的第三個引數傳入
    if (prevProps.list.length < this.props.list.length) {
      const list = this.listRef.current;
      return list.scrollHeight - list.scrollTop;
    }
    //預設返回null
    return null;
  }

  componentDidUpdate(prevProps, prevState, snapshot) {
	  //根據更新前的滾動位置,設定更新後列表的滾動長度
    if (snapshot !== null) {
      const list = this.listRef.current;
      list.scrollTop = list.scrollHeight - snapshot;
    }
  }

  render() {
    return (
      <div ref={this.listRef}>{/* ...contents... */}</div>
    );
  }
}
```
複製程式碼

需要注意的幾點:

  • getSnapshotBeforeUpdate 函式需要和 componentDidUpdate 一起使用,否則會報 warning
  • getSnapshotBeforeUpdate 在生命週期中為什麼要在 render 函式之後而不是之前呼叫?我認為主要還是因為 React 想保證 render 階段的無副作用,從而在 commit 階段強行新增一個 pre-commit 階段來獲取更新前的 DOM。可以看出,getSnapshotBeforeUpdate 的引入只是一個折中方案。

小結

  可以看出 React16 中,類元件的一個元件的生命週期被劃分為類 render 和 commit 兩個階段,render 階段主要負責元件渲染相關,包括對渲染資料 state 的更新。為了防止一次更新中 render 階段重複執行,React 將該階段可能引入 side effects 的生命週期函式 componentWillReceivePropscomponentWillUpdatecomponentWillUnmount 等函式移除。
  針對需要通過 props 計算 derived state 的需求,提供靜態函式 getDerivedStateFromProps。針對獲取更新前 DOM 元素的需求,React16 提供了 getSnapshotBeforeUpdate 生命週期函式。

Hooks

簡介

  React 官方文件對 Hooks 的介紹是 Hooks are a new addition in React 16.8. They let you use state and other React features without writing a class,從介紹中我們可知:

  1. Hooks只能在函式元件中使用
  2. Hooks 為函式式元件新增了本來只有類元件才有的一些特性,比如 state。

動機

  • 元件邏輯程式碼複用   相比於 HOC、renderProps 而言,需要提供一個更加靈活輕便並且是原生支援的狀態邏輯複用機制。個人總結 renderProps 和 HOC 的缺點在於:

    • 二者都會引入 wrapper hell,無論 HOC 還是 renderProps,都是將需要複用都邏輯程式碼通過另外一個元件引入進來。這些元件雖然只負責邏輯程式碼的複用,但在 React 元件樹中仍然需要渲染或者作為一個抽象元件(不會渲染成真實的 DOM 元件)存在。當複用較多時,React 元件樹就會因為多層的 HOC wrapper 或者 render props 的引入而變得臃腫。
    • 當對原有程式碼進行重構時,render props 和 HOC 都會去修改原有元件的結構。
    • renderProps 和 HOC 只是在已有機制上的一種設計模式,React 還缺少一個原生機制來支援元件內部邏輯程式碼的複用。
    • renderProps 和 HOC 目前還存在各自的缺點,而 Hooks 可以成功避免這些缺點:
      • HOC 需要藉助 React 的 class 元件來進行實現,而 React 已經在逐步拋棄 class 元件模式
      • renderProps 較多時會導致 render props 的 callback hell
        React16 新生命週期和 Hooks 介紹
  • HOC、render props 和 Hooks 實現邏輯程式碼複用示例
      當我們需要實現一個簡單的顯示隱藏的功能時,一般是在元件的 state 中定義一個控制現實還是隱藏的布林變數,並再新增一個 showhide 方法來控制該變數。如果要將這段邏輯程式碼提取出來複用的話,可以通過高階元件 HOC、render props 或者 Hooks 來實現,以下分別列出這三種方法的實現程式碼以及對應的 React 元件樹結構。可以看出使用 Hooks 複用邏輯程式碼時,由於沒有建立額外的元件,故無論是程式碼還是最後生成的 React 元件樹,都是 Hooks 的實現方式更簡潔。

    • HOC
    const Wrapper = WrappedComponent => class extends React.Component{
        constructor(props) {
            super(props);
            this.state = {
                isDisplayed: defaultTo(props.initialState, false),
            };
            this.hide = this.hide.bind(this);
            this.show = this.show.bind(this);
        }
        hide() {
            this.setState({isDisplayed: false,});
        }
        show() {
            this.setState({isDisplayed: true,});
        }
        render(){
            const newProps = {
                ...this.props,
                ...this.state,
                hide: this.hide,
                show: this.show
            };
            return <WrappedComponent {...newProps}/>
        }
    };
    
    const App = Wrapper(({isDisplayed, hide, show}) => {
            return (
                <div className="App">
                    {
                        isDisplayed &&
                        <p onClick={hide}>Click to hide</p>
                    }
                    <button onClick={show}>Click to display</button>
                </div>
            );
    })
    複製程式碼

    React16 新生命週期和 Hooks 介紹

    • render props
    	class VisibilityHelper extends React.Component {
        constructor(props) {
            super(props);
            this.state = {
                isDisplayed: defaultTo(props.initialState, false),
            };
            this.hide = this.hide.bind(this);
            this.show = this.show.bind(this);
        }
        hide() {
            this.setState({isDisplayed: false});
        }
        show() {
            this.setState({isDisplayed: true});
        }
        render() {
            return this.props.children({
                ...this.state,
                hide: this.hide,
                show: this.show,
            });
        }
    }
    
    const App = () => (
        <div className="App">
            <VisibilityHelper>
               {
                   ({isDisplayed, hide, show}) => (
                        <div>
                            {
                               isDisplayed &&
                               <p onClick={hide}>Click to hide</p>
                            }
                            <button onClick={show}>Click to display</button>
                        </div>
                    )
                }
            </VisibilityHelper>
        </div>
    );
    複製程式碼

    React16 新生命週期和 Hooks 介紹

    • Hooks
    import React, {useState} from 'react';
    
    function useIsDisplayed(initialValue) {
        const [isDisplayed, setDisplayed] = useState(defaultTo(initialValue, false));
        function show(){
            setDisplayed(true);
        }
        function hide() {
            setDisplayed(false);
        }
        return {isDisplayed, show, hide};
    }
    
    const App = () => {
        const {isDisplayed, show, hide} = useIsDisplayed(false);
        return (
            <div className="App">
                {
                    isDisplayed &&
                    <p onClick={hide}>Click to hide</p>
                }
                <button onClick={show}>Click to display</button>
            </div>
        );
    };
    複製程式碼

    React16 新生命週期和 Hooks 介紹

  • 武裝函式式元件   React 現在力推函式式元件,並逐漸棄用 class 元件(具體原因後面章節再討論)。之前的函式式元件由於沒有 state 以及型別 class 元件生命週期函式的機制,往往用來作展示型元件。而通過 useState hook 可以為函式式元件新增 state,通過 useEffect 可以在函式式元件中實現 class 元件生命週期函式的功能。

useState 介紹

   useState 函式用於給函式式元件提供能夠持久儲存並能將變化對映到檢視上的狀態 state hook,類似 class 元件中的 state。在上節給出的 Hooks 實現邏輯程式碼複用的例子中已經展示了 useState 函式的使用。useState 函式的返回值為一個陣列,陣列中第一個元素為 state 物件,第二個元素為一個 dispatch 方法,用於更新該 state。基本使用為:

import React, {useState} from 'react';
function Counter(){
	//宣告一個 count 變數,可以通過 setCount dispatch 一個新的 count 值
	const [count, setCount] = useState(0);
	useState('the second state');
	useState('the third state');
	return <div>
		<p>Count: {count}</p>
		<button onClick={() => setCount(count + 1)}>+</button>
	</div>
}
複製程式碼
  • state 如何儲存   上例中我們可以看到,除了第一次執行元件渲染時呼叫來 useState(0) 生成一個 state 後,之後重新執行元件渲染時,獲取到到 state 都是更改之後都值,故在 React 應該對 state 做了外部儲存。React16 中,一次函式式元件的更新中 Hooks 呼叫邏輯在函式 renderWithHooks 中實現。模組全域性變數 firstCurrentHook 指向當前渲染元件的第一個 Hook
    React16 新生命週期和 Hooks 介紹
    這裡需要注意的幾點是
  1. 同一個元件中的 hooks (state hooks 以及後面的 effect hooks) 都是以連結串列的形式組織的**,每個 hook.next 指向下一個被宣告的 hook。故在示例圖中只用找到第一個 hook (firstWorkInProgressHook) 即可。
  2. 元件的 FiberNode.memoizedState 指向元件的第一個 Hook。下圖是本節例子中元件的 hooks 連結結構。當元件被銷燬的時候,hooks 才會被銷燬,更新過程中的 state 都儲存在 FiberNode.memoizedState 屬性中。另外 useState 方法返回的第二個 dispatch 函式就是 下圖 queue 中的 dispatch 函式
    React16 新生命週期和 Hooks 介紹
  • state hooks 如何觸發元件重新渲染   當我們呼叫 setCount 更新 count 值後,就會觸發 Counter 元件的重新渲染。setCount 就是 state hook count 的 dispatch 函式。在初始化 state hooks 的時候,React 會將元件的 Fiber 繫結到 dispatch 函式上,每次 dispatch 函式執行完後會建立一個 Fiber 的更新任務。
  • 關於 useState 的使用
  1. 建議 useState 宣告的 state 儘量細顆粒,因為每次更新 state 時都是替換整個 state,如果使用 useState 宣告一個大物件的話,不利於 state hooks 的複用
  2. 儘管元件每次更新時都會呼叫 useState 函式,但 useState 作為 state 初始值的引數只有在函式首次渲染時才會被使用。所以不建議在 useState 引數中呼叫 state 的初始化函式對 state 進行初始化,因為在後面每次元件更新中,都會執行一次 state 初始化函式的無效呼叫。正確的做法是,將 state 初始化函式作為引數傳入 useState,因為 state 初始化函式只有在第一次呼叫 useState 函式時才會執行。

useEffect 介紹

  useEffect 函式用於建立一個 effect hook。effect hooks 用於在函式式元件的更新後執行一些 side effects,其之於函式式元件就相當於 componentDidMountcomponentDidUpdate 之於 class 元件。useEffect 的基本用法為:

const App = () => {
    const [text, setText] = useState('');
    useEffect(() => {
        window.addEventListener('keydown', writing, false);
        return () => {
            window.removeEventListener('keydown', writing, false);
        }
    });

    function writing(e){
        setText(text + e.key);
    }

    return (
        <div className="App">
            <p>{text}</p>
        </div>
    );
};
複製程式碼

  上例是一個使用函式式元件和 state hooks 以及 effect hooks 實現的監聽使用者鍵盤輸入並現實輸入字串的功能。可以看到,我們使用 state hooks 來儲存以及更新使用者輸入的字串,用 effect hooks 來監聽以及取消監聽 keydown 事件。

  • useEffect 的輸入是一個函式,該函式在元件渲染完成後執行,主要用於處理一些 side effects
  • useEffect 的輸入函式可以 return 一個函式,該函式在每次元件重新渲染前以及元件 Unmount 前執行,主要用於解除事件監聽
  • 函式式元件重新渲染時,先執行 effect hook 的返回函式,再執行 effect hook。對於示例程式碼而言,就是先執行 window.removeEventListener 再執行 window.addEventListener
  • 預設情況下,每次重新渲染時都會執行 effect hooks,比如上例中,每次輸入字元後,window 都會執行一邊取消監聽以及監聽 keydown 事件。出於效能的考慮,useEffect 方法提供了第二個陣列引數,即 useEffect(() => {}, [])。當傳入第二個引數時,只有當第二個引數中的某個 state 或者 props 改變時,該 effect hook 才會被呼叫。值得注意的是:此時 effect hook 以及在其間的回撥函式只能訪問到 useEffect 陣列引數中的 state 和 props 的最新值,其它 state 和 props 只能獲取到初始值
  • 使用 effect hooks 可以在一個 useEffect 函式中完成一個事件繫結/解除繫結、訂閱/取消訂閱的操作。相比於 class 元件中將事件相關的邏輯分散在 componentDidMountcomponentWillUnmount 兩個函式中,useEffect 程式碼更為緊湊。

useRef 介紹

  和 React16 中提供的 createRef 方法一樣,用於獲取 React 元件的 ref。官方示例:

function TextInputWithFocusButton() {
  const inputEl = useRef(null);
  const onButtonClick = () => {
    //inputEl 的 current 屬性指向 input 元件的 dom 節點
    inputEl.current.focus();
  };
  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  );
}
複製程式碼

除了用於獲取元件 ref 以外,useRef 還可以用於實現類似於 class 元件的例項屬性(this.XXX),直接通過對 useRef 方法返回物件的 current 屬性進行讀寫即可。

useReducer 介紹

  useReducer 實現了 Redux 中的 reducer 功能。當 state 的邏輯比較複雜的時候,可以考慮使用 useReducer 來定義一個 state hook。示例:

const initialState = {count: 0};

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    default:
      throw new Error();
  }
}

function Counter({initialState}) {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
    </>
  );
}
複製程式碼

  上例是 React 官方提供的 useReducer 的使用示例。當然和 Redux 一樣,dispatch 的 action 可以由 actionCreator 來生成。

Hooks 使用規則

  1. 不要在非 React 函式式元件外呼叫 useXXX   在呼叫 useXXX 方法時,會讀取當前的元件的 Fiber,如果 Fiber 不存在則報錯
  2. 不要在條件語句中呼叫 useXXX
      在介紹 useState 的時候我們提到過,同一個元件內的所有 Hooks 是由宣告的先後順序,通過連結串列進行儲存的。
    • 在 React 的 renderWithHooksupdateWorkInProgressHook 函式中會通過 currentHook 來記錄當前執行的 hook
    • currentHook 初始化以及每次執行完元件渲染後都會置為 null。每當執行完 currentHook 後,會將 currentHook 指向 currentHook.next
    • 執行完元件渲染後,如果 currentHook.next 不為 null,說明被渲染元件的 Hooks 連結串列中仍有未執行的 Hooks ,報錯 var didRenderTooFewHooks = currentHook !== null && currentHook.next !== null;

自定義 Hook

  本節直接使用 React 官網提供的示例程式碼,需要注意的是,自定義 Hooks 也需要滿足上節中提到的使用規則。

import React, { useState, useEffect } from 'react';

//useFriendStatus 為根據好友 ID 獲取好友是否線上的自定義 Hook
function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null);

  function handleStatusChange(status) {
    setIsOnline(status.isOnline);
  }

  useEffect(() => {
    ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
    };
  });

  return isOnline;
}

const friendList = [
  { id: 1, name: 'Phoebe' },
  { id: 2, name: 'Rachel' },
  { id: 3, name: 'Ross' },
];

//聊天物件選擇列表元件
function ChatRecipientPicker() {
  const [recipientID, setRecipientID] = useState(1);
  
  //通過使用 useFriendStatus Hook,
  //將獲取當前好友是否線上的邏輯從元件中分離出去 
  const isRecipientOnline = useFriendStatus(recipientID);

  return (
    <>
      <Circle color={isRecipientOnline ? 'green' : 'red'} />
      <select
        value={recipientID}
        onChange={e => setRecipientID(Number(e.target.value))}
      >
        {friendList.map(friend => (
          <option key={friend.id} value={friend.id}>
            {friend.name}
          </option>
        ))}
      </select>
    </>
  );
}
複製程式碼

使用 Hooks 代替生命週期函式

  • constructor class 元件中 constructor 函式一般實現:建立 this繫結事件處理函式以及初始化 state 等功能。在 FC 中不需要 this 關鍵字,另外可以通過傳入 state 初始化函式到 useState 第二個引數來初始化 state
  • render FC 元件函式本身就是 render 函式
  • componentDidMount 使用 *useEffect(() => {}, []) 代替 componentDidMount。由於 useEffect 第二個引數為空陣列,故第一個引數中的函式只有在元件掛載和解除安裝時呼叫
  • componentWillUnmountcomponentDidMount 一樣,使用第二個引數為空陣列的 useEffect 代替。當元件解除安裝時,useEffect 第一個函式引數的返回函式會被呼叫。componentWillUnmount 裡面的邏輯可以解除安裝該函式中
  • componentDidUpdate 使用 useEffect 代替 componentDidUpdate,首次渲染觸發的 effect hook 中新增判斷跳過即可。
  • getDerivedStateFromProps 函式式元件中,要實現 class 元件的 getDerivedStateFromProps 函式需要考慮亮點:一是儲存上一次的 props 用於和當前要更新的 props 進行比較;二是當 props 變化後觸發相應的 derived state 改變。前者可以通過定一個 state hook (稱作 prevPropsHook)來記錄最近一次更新的 props。後者可以在元件函式呼叫時通過對比 prevPropsHook 的值與 props 是否相同,若不同則改變相應 state hook 的值。
  • shouldComponentUpdate 在 class 元件中,props 和 state 的更新都會觸發 shouldComponentUpdate 函式。
    • 在函式式元件中,通過高階元件 React.memo 實現 props 更新判斷時候需要重新渲染元件。React.memo 使用方式為
    function F({count = 0, isPure = false}){
        console.log(`render ${isPure ? 'pure FC' : 'FC'} ${count} times`);
        return <h2>{count}</h2>
    }
    
    const PureFC = React.memo(F, /*areEqual(prevProps, nextProps)*/);
    
    class App extends React.Component{
        state = {
            count: 0,
            fakeState: false
        };
    
        render(){
            const {count, fakeState} = this.state;
            return <div>
                <F count={count} isPure={false}/>
                <PureFC count={count} isPure={true}/>
                <button onClick={() => {
    	            this.setState({count: count + 1})}
    	          }>increase</button>
                <button onClick={() => {this.setState({fakeState: !fakeState})}}>Click2</button>
            </div>;
        }
    }
    //*click increase button
    //render FC 1 times
    //render pure FC 1 times
    //*click Click2 button
    //render FC 1 times
    複製程式碼
      上例說明,即使 FC(函式式元件) 的 props 沒有變化,當父元件更新時,還是會重新渲染 FC。但用 React.memo 高階元件包裹的 FC 卻可以跳過 props 沒有變化的更新。為了支援更加靈活的 props 對比,React.memo 還支援傳入第二個函式引數 areEqual(prevProps, nextProps)。該函式返回 true 時不更新所包裹的 FC,反之更新 FC,這點與 shouldComponentUpdate 函式相反。
    • 由於 FC 通過 state hooks 模擬了 class 元件的 state,所以當 state hooks 更新時也需要一個機制模擬 shouldComponentUpdate 函式的跳過 state 更新的功能。React 官方提供的方法比較彆扭,即用 useMemo 包裹渲染了 state 的子元件來實現。useMemo 只會在第二個陣列引數中的某個元素變化時,才會觸發 第一個函式函式的重新執行。官方示例為:
    function Parent({ a, b }) {
      // Only re-rendered if `a` changes:
      const child1 = useMemo(() => <Child1 a={a} />, [a]);
      // Only re-rendered if `b` changes:
      const child2 = useMemo(() => <Child2 b={b} />, [b]);
      return (
        <>
          {child1}
          {child2}
        </>
      )
    }
    複製程式碼
  • 其它生命週期函式 諸如 getSnapshotBeforeUpdategetStateFromErrorcomponentDidCatch 函式在 hooks 中還未找到替代方法。

Context

   Context 主要解決了 React 元件樹非父子元件的狀態共享問題,以及子元件與祖先元件之前多層 props 傳遞繁瑣的問題。官方示例:

const ThemeContext = React.createContext('light');

class ThemeProvider extends React.Component {
  state = {theme: 'light'};

  render() {
    return (
      <ThemeContext.Provider value={this.state.theme}>
        {this.props.children}
      </ThemeContext.Provider>
    );
  }
}

const ThemedButton = () => {
  render() {
    return (
      <ThemeContext.Consumer>
        {theme => <Button theme={theme} />}
      </ThemeContext.Consumer>
    );
  }
}
複製程式碼
  • React.createContext   用於建立一個 Context 物件,該物件支援訂閱釋出功能。元件可以通過 Context.Provider 釋出一個新的值,其所有訂閱了該 Context 物件的子元件就可以即使獲取到更新後的值。上例中 createContext 方法的引數作為 Context 物件 ThemeContext 的預設值,訂閱了 ThemeContext 的元件在獲取 Context 值的時候會沿著副元件向上搜尋 Context.Provider,如果沒有搜尋到 Context.Provider,則會使用該預設值。
  • Context.Provider   用於釋出 Context,Provider 的子元件會從 Context.Provider 元件的 value 屬性獲取 Context 的值。
  • Context.Consumer   可以監聽 Context 的變化。當 Context.Provider 的 value 屬性變化後,該 Provider 元件下的所有 Consumer 元件都會重新渲染,並且不受 shouldComponentUpdate 返回值的影響。Consumer 採用了 render props 模式, 其子元件為一個函式,函式引數為其監聽的 Context 的值。

###Error Boundary   React16 中如果元件生命週期中丟擲了未經捕獲的異常,會導致整個元件樹解除安裝。React16 提供了兩個生命週期函式用於捕獲子元件中在生命週期中丟擲的異常。一個是 static getDerivedStateFromError(error) 在渲染階段 render 函式前呼叫,,另一個是 componentDidCatch 在 commit 階段即完成渲染後呼叫。關於這兩個函式,React 官方示例為:

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    //主要用於根據額 error 更新 state.
    //和 getDerivedStateFromProps 函式類似,
    //返回的物件會更新到 state
    return { hasError: true };
  }

  componentDidCatch(error, info) {
    //主要用於在 commit 階段處理錯誤相關的 side effects
    //比如此處的傳送錯誤資訊到伺服器
    logErrorToMyService(error, info);
  }

  render() {
    if (this.state.hasError) {
      return <h1>Something went wrong.</h1>;
    }
    return this.props.children; 
  }
}
複製程式碼

  關於 Error Boundary 需要注意的幾點是:

  1. 新增了 getDerivedStateFromError 或者 componentDidCatch 函式的元件可以作為 ErrorBoundary。ErrorBoundary 只會捕獲其子元件中丟擲的異常,無法捕獲自身丟擲的異常
  2. ErrorBoundary 只能捕獲子元件生命週期中丟擲的異常。其它的,比如回撥函式以及事件處理函式中丟擲的異常就無法捕獲。
  3. 事件處理函式中建議使用 JavaScript 的 try/catch 進行異常捕獲

其它

Portal

  在使用 React16 時,如果我們在渲染元件時需要渲染一個脫離於當前元件樹之外的元件(如對話方塊、tooltip等),可以通過 ReactDOM.createPortal(Child, mountDom)* 函式建立一個 Portal,將 React 元件 Child 掛載到真實 DOM 元素 mountDom 上。示例程式碼:

//html
<body>
    <div id="root"></div>
    <div id="modal"></div>
</body>

//js
const modalDom = document.querySelector('#modal');

function Child(){
    function handleClick(){
        console.log('click child');
    }
    return <button onClick={handleClick}>Child Button</button>
}

function App(){
    const [count, setCount] = useState(0);

    return <div onClick={() => {setCount(count + 1)}}>
        <h1>{count}</h1>
        {
            ReactDOM.createPortal(
                <Child/>,
                modalDom //將 Child 掛載到 id=modal 的 div 元素下
            )
        }
    </div>
}
//將 App 掛載到 id=root 的 div 元素下
ReactDOM.render(<App />, document.getElementById('root'));
複製程式碼

  上例中,雖然 Child 元件的真實 DOM 節點掛載在 modal 下,而 App 元件的真實 DOM 節點掛載在 root 下。但 Child 元件中的 Click 事件仍然會冒泡到 App 元件。故我們點選 button 時,會依次觸發 Child 元件的 handleClick 函式,以及 App 元件的 setCount 操作。

React.Fragment 元件

  React16 中可以通過 React.Fragment 元件來組合一列元件,而不需要為了返回一列元件專門引入一個 DIV 元件。其中 <></><React.Fragment></React.Fragment>的簡寫。

React16 新生命週期和 Hooks 介紹
React16 新生命週期和 Hooks 介紹

相關文章