React 16.0之後的改動還是很大的,除了新增加了很多新特性之外,還明確表示未來會增加async render,增加async render之後,將會在17.0的版本完全廢除當前版本的三個生命週期,對於已經習慣現在寫法的小夥伴來說感覺有點方(至少我有點方),所以還是提前熟悉一下,做好升級的準備吧~
個人覺得升級是必然的事情,所以,還是提前準備一下,做好升級準備!
我技術沒有大牛的水平,所以我寫文章並不是為了吸引人,一方面是記錄自己新學的東西,寫出來覺得自己的理解也會加深;另一方面是讓比我還入門的人找到個非常合適的入門文章。我喜歡配上一些Demo,這樣不太明白的人才能看懂,受教人群不一樣,大牛可以去看官方文件說明,小白可以看看demo感受一下新特性~ Demo地址 Demo大概長這個樣子:
V16.0
16.0算是一個大版本,增加了很多新特性,解決了很多痛點問題~比如,可以render字串和陣列,比如增加了Fragment,這些在使用中都有效減少了dom節點的數量;還有可以使用portals將新節點插入在任何其他非父節點的dom節點下,對於modal,message等外掛是福音;還有增加了error boundary,如果使用的好你再也不會在專案裡看到滿屏紅色或者崩潰了,哈哈~
render多型別
16.0以後,react的render函式增加了幾種型別,包括字串和陣列型別。
render() {
//不需要再把所有的元素繫結到一個單獨的元素中了
return [
// 別忘記加上key值
<li key="A"/>First itemli>,
<li key="B"/>Second itemli>,
<li key="C"/>Third itemli>,
];
}
// 也可以用下面這種寫法
// 不需要再把所有的元素繫結到一個單獨的元素中了
render() {
const arr = ['Adams', 'Bill', 'Charlie'];
const Arr = () => (arr.map((item, index) => <p key={index}>{item}</p>));
return <Arr />
}
複製程式碼
從上圖可以看出,解決了以往必須在外層包一個父元素div的限制,有效的減少了不必要的dom元素。
React.Fragment
解決的痛點問題與上面陣列是相同的,不過個人感覺更加優雅,首先不需要加上key,其次就是增加一個不渲染的空標籤看起來更加的整體,因為以前已經習慣了JSX語法需要一個父標籤,這種寫法更符合習慣。但是在16.0裡提到了Fragment,而更詳細的介紹是在16.2版本里,之所以放在這裡說因為和返回陣列解決的痛點是類似的~ 下面例子來自官網:
// 一個Table元件,裡面巢狀了columns元件
class Table extends React.Component {
render() {
return (
<table>
<tr>
<Columns />
</tr>
</table>
);
}
}
// columns元件
class Columns extends React.Component {
render() {
return (
<div>
<td>Hello</td>
<td>World</td>
</div>
);
}
}
複製程式碼
上面設計符合react,元件式劃分,但是最後渲染出來卻不是最佳的,因為columns的最外層巢狀了一層沒用的div標籤。這個問題存在於16.0之前。
有了Fragment以後,很好的解決問題:
import React, { Fragment } from 'react';
class Columns extends React.Component {
render() {
return (
<Fragment>
<td>Hello</td>
<td>World</td>
</Fragment>
);
}
}
// Fragment的語法糖
<>
<td>Hello</td>
<td>World</td>
</>
兩個空標籤
複製程式碼
這塊糖有點苦,官方明明說的是語法糖,但是我試了,編譯通不過,並且官方也特意說明了可能使用該語法糖會出現問題,但是給出的解決辦法我都試了,還是不成功,可能配置的不對吧,有誰配置好了可以留言告訴我一下,不過無傷大雅,我倒是覺得語法糖也不一定必須使用。
Error Boundary
什麼是Error Boundary?
單一元件內部錯誤,不應該導致整個應用報錯並顯示空白頁,而Error Boundaries解決的就是這個問題。
在以前的React版本中,如果某一個元件內部出現異常錯誤,會導致整個專案崩潰直接顯示空白頁或者error紅頁,很不友好。error boundary就是解決這個問題的。
Error Boundary本質上是一個元件
按照我的個人理解,error boundary本質上就是一個元件,只不過元件內部多出現了一個生命週期,componentDidCatch,在這個生命週期裡面,它會捕捉本元件下的所有子元件丟擲的異常錯誤,包括堆疊資訊。
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
componentDidCatch(error, info) {
// Display fallback UI
this.setState({ hasError: true });
// You can also log the error to an error reporting service
logErrorToMyService(error, info);
}
render() {
if (this.state.hasError) {
// You can render any custom fallback UI
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
// 使用起來就跟普通元件一樣
<ErrorBoundary>
<ChildCompA />
<ChildCompB />
...
</ErrorBoundary>
複製程式碼
上面程式碼是官網給出的例子,在ErrorBoundary元件內,定義state={ hasError: false },在componentDidCatch內部捕捉到error,然後動態渲染元件,如果出現異常使用提前定義好的替換元件代替發生異常的元件,這樣整個頁面只有發生異常的部分被替換不影響其他內容的展示。
Portals
有些元素需要被掛載在更高層級的位置。最典型的應用場景:當父元件具有overflow: hidden或者z-index的樣式設定時,元件有可能被其他元素遮擋,這個時候你就可以考慮要不要使用Portal使元件的掛載脫離父元件。
Portals 提供了一種很好的將子節點渲染到父元件以外的 DOM 節點的方式。
render() {
// React does *not* create a new div. It renders the children into `domNode`.
// `domNode` is any valid DOM node, regardless of its location in the DOM.
return ReactDOM.createPortal(
this.props.children,
domNode,
);
}
複製程式碼
一般而言,元件在裝載的時候會就近裝載在該元件最近的父元素下,而現在你可以使用Portal將元件渲染到任意一個已存在的dom元素下,這個dom元素並不一定必須是元件的父元件。
Portals的應用 —— Modal,message等訊息提示
Portals的事件冒泡
從上圖可以看出來,彈窗的父元件應該是掛載在#app這個dom下面的,通過portals,我們將modal框掛載在#portal_modal這個dom下了。雖然最後的modal元件沒有掛載在整個應用所在的#app下,但是portals建立的元件裡面的事件依然會冒泡給它自身的父元件,父元件可以捕獲到被掛載在#portal_modal節點下面的modal的點選事件。
class PortalsComp extends Component {
constructor(props) {
super(props);
this.state = { showModal: false, clickTime: 0 };
}
handleShow = () => {
this.setState({ showModal: true });
}
handleHide = () => {
this.setState({ showModal: false });
}
handleClick = () => {
let { clickTime } = this.state;
clickTime += 1;
this.setState({ clickTime });
}
render() {
const protalModal = this.state.showModal ? (
<PortalModal>
<ModalContent hideModal={this.handleHide} />
</PortalModal>
) : null;
return (
<div className={s.portalContainer} onClick={this.handleClick}>
<div>該元件被點選了: {this.state.clickTime}次</div>
<Button onClick={this.handleShow} type='primary'>點我彈出Modal</Button>
{protalModal}
</div>
);
}
}
export default PortalsComp;
複製程式碼
從上圖可以看出來,portals的元件雖然掛載在其他dom下,但是父元件依然可以捕獲到modal的冒泡事件,開啟和關閉,父元件顯示點選次數為2。
V16.3
廢棄的幾個生命週期
- componentWillMount
- componentWillReceiveProps
- componentWillUpdate
這三個生命週期之中,componentWillReceiveProps平時用的頻率還是特別多的,所以對於以前的專案,可能升級會是一種麻煩事,但是說是廢棄,但是其實在整個V16版本,還都是可以使用的,只不過會丟擲警告,而且官方會建議使用的時候加上字首UNSAFE_。
componentWillReceiveProps ---> UNSAFE_componentWillReceiveProps
為什麼要廢棄這三個生命週期
React16.0之前的生命週期設計如下圖:
可以看到從開始到結束,這些生命週期的設計可以捕捉到元件的每一個state和props的改變,並沒有任何邏輯上的問題,而且對於我們來說寫法已經形成習慣,如果廢棄肯定是費力不討好的事情。那麼為啥官方還是要皮這麼一下呢? 雖然我英文不好,但是還是大致看了一下,意思呢,首先就是說這三個API經常被濫用和誤用,再者就是在未來版本中,要引入async render(非同步渲染),而在非同步渲染的場景下,這些生命週期裡面的程式碼會在未來的React版本里存在缺陷,因此就拋棄了。 這三個API存在的問題:React v16.3 版本新生命週期函式淺析及升級方案這裡講的很清楚。為了彌補不足新增了兩個生命週期
static getDerivedStateFromProps
觸發時間:在元件構建之後(虛擬dom之後,實際dom掛載之前) ,以及每次獲取新的props之後。
每次接收新的props之後都會返回一個物件作為新的state,返回null則說明不需要更新state. 配合componentDidUpdate,可以覆蓋componentWillReceiveProps的所有用法。
// before
componentWillReceiveProps(nextProps) {
if (nextProps.flag !== this.props.flag) {
this.setState({ flag: nextProps.flag }, () => {
if (nextProps.flag) {
this.doSmething();
}
});
}
}
// 在16.3之後的版本使用,react推薦下面這種寫法,否則eslint可能會提示警告
UNSAFE_componentWillReceiveProps(nextProps) {
// your code
}
// after
static getDrivedStateFromProps(nextProps, prevState) {
if (nextProps.flag !== prevState.flag) {
// 更新state
return {
flag: nextProps.flag
}
}
// 不更新state
return null;
}
// state更新過後需要做的事放在componentDidUpdate裡
componentDidUpdate(prevProps, prevState) {
if (prevState.flag !== this.props.flag) {
this.doSomething();
}
}
複製程式碼
寫法與之前相比要麻煩了一些,但是處理邏輯上應該是更清晰了。在 componentWillReceiveProps 中,一般會進行兩件事,第一、判斷this.props與nextProps的異同,然後更新元件state;第二、根據state的變化更新元件或者執行一些回撥函式。在以前的寫法裡,這兩件事我們都需要在 componentWillReceiveProps 中去做。而在新版本中,官方將兩件事分配到了兩個不同的生命週期 getDerivedStateFromProps 與 componentDidUpdate 中去做,使得元件整體的更新邏輯更為清晰,getDerivedStateFromProps裡面進行state的更新,componentDidUpdate裡做更新之後的各種回撥。而且在 getDerivedStateFromProps 中還禁止了元件去訪問 this.props(static方法,獲取不到元件的this),強制讓開發者去比較 nextProps 與 prevState 中的值,以確保當開發者用到 getDerivedStateFromProps 這個生命週期函式時,就是在根據當前的 props 來更新元件的 state,而不是去做其他一些讓元件自身狀態變得更加不可預測的事情。
還是需要適應,雖然習慣了以前的寫法,但是現在這種效能要更好。而且畢竟以後會廢棄。
這裡解決了我的很久一個困惑,元件最後的更新過程其實是: componentWillReceiveProps(static getDerivedStateFromProps)判斷state的變化 ---> shouldComponentUpdate判斷是否進行更新 render階段會根據diff演算法來生成需要更新的虛擬dom結構 ---> 更新虛擬dom ---> 虛擬dom更新完畢立刻呼叫componentDidUpdate ---> 最後完成渲染。
因為官方給出的定義是,componentDidUpdate是在元件dom更新結束之後立即呼叫,那麼這個更新結束我理解的就是dom已經更新完畢渲染好了,但是我在componentDidUpdate裡面呼叫了alert,發現其實進入該生命週期之後,其實dom還未發生變化,但是頁面上的dom未發生變化,而componentDidUpdate獲取dom的時候值確實正確的,可能這裡是虛擬dom和真實dom不同步的關係吧,總之就是,在componentDidUpdate裡面可以獲取dom節點的操作,獲取的值也是更新完畢的,下面的例子也是這樣的。
getSnapshotBeforeUpdate ---- 針對對dom的一些操作
觸發時間: update發生的時候,在render之後,在元件dom渲染之前。
返回一個值,作為componentDidUpdate的第三個引數。 配合componentDidUpdate, 可以覆蓋componentWillUpdate的所有用法。
componentWillUpdate存在的問題
- 與componentWillReceiveProps類似,同樣在一層更新過程中可能會被呼叫多次,這樣就會造成裡面的回撥函式可能會執行多次,浪費效能。
- 在React17引入async render之後,render階段和commit階段可能並不是同步連貫的,因此,componentDidUpdate和componentWillUpdate獲取到的Dom可能是不同的,這樣就會導致讀取到的dom元素的狀態是不安全的。
getSnapshotBeforeUpdate配合componentDidUpdate來保證狀態的一致
getSnapshotBeforeUpdate的發生時間在render之後,元件dom渲染之前,這樣可以保證此時讀取的dom和componentDidUpdate的dom是一致的。
getSnapshotBeforeUpdate不是靜態方法,裡面可以讀取this.props和this.state等資訊,並且呼叫之後應該返回一個值作為componentDidUpdate的第三個引數
static getDerivedStateFromProps(nextProps, prevState) {
if (nextProps.disabled !== prevState.disabled) {
return {
disabled: nextProps.disabled
};
}
return null;
}
getSnapshotBeforeUpdate(prevProps, prevState) {
return this.props.disabled;
}
componentDidUpdate(prevProps, prevState, snapshot) {
if (!snapshot) {
// 如果snapshot是false,獲取焦點
this.domRef.focus();
}
}
render() {
return (
<div>
<input ref={(ref) => this.domRef = ref} disabled={this.state.disabled} />
</div>
);
}
複製程式碼
V16.4
修復了getDerivedStateFromProps的bug,為了更好地相容即將到來的非同步渲染
這個點我還沒太弄明白,因為我準備寫的時候就已經是16.4了,也不太知道這個bug會導致什麼影響,不過看了一些文章,大概意思是下面這樣: 參考文章:React16.4 新特性
React這次更新修復了getDerivedStateFromProps這個生命週期的觸發節點, 在之前, 它觸發的方式和舊生命週期getDerivedStateFromProps類似, 都是在被父元件re-render的時候才會觸發,並且本元件的setState的呼叫也不會觸發
這種方式在之前同步渲染的時候是沒有問題的, 但是為了支援新的還未啟用的fiber非同步渲染機制, 現在, getDerivedStateFromProps在元件每一次render的時候都會觸發,也就是說無論是來自父元件的re-render, 還是元件自身的setState, 都會觸發getDerivedStateFromProps這個生命週期。
要理解為什麼react修復了這個生命週期的觸發方式, 我們首先得了解react的非同步渲染機制
react非同步渲染
要理解react非同步渲染的機制, 我們首先要說一說react之前是如何進行渲染。
在react16之前, 元件的渲染都是同步進行的, 也就是說從constructor開始到componentDidUpdate結束, react的執行都是沒有中斷的, 生命週期開始之後就會執行到其結束為止, 這樣帶來的一個缺點就是,如果元件巢狀很深, 渲染時間增長了之後, 一些重要的, 高優先順序的操作就會被阻塞, 例如使用者的輸入等, 這樣就會造成體驗上的不友好。
在之後即將到來的非同步渲染機制中, 會允許首先解決高優先順序的執行,同時會暫停當前的渲染程式,當高優先順序的程式結束之後, 再返回繼續執行當前程式, 這樣會大大的提高react的流暢度,給使用者帶來更好的體驗
而這次修復getDerivedStateFromProps, 正是為了保證與即將到來的非同步渲染模式的相容。
複製程式碼
React pointer events
pointer events是HTML5規範的WEB API,它主要目的是用來將滑鼠(Mouse)、觸控(touch)和觸控筆(pen)三種事件整合為統一的API。
如果你的應用涉及到指標的相關事件,那麼這個API還是很有用的,不過這個API的相容性不怎麼樣,基本主流瀏覽器的最新版本才支援,從React增加了這個pointer events事件來看,說明React官方還是很看重這個API的,我覺得相容性肯定滿滿的會越來越好。
因為相容性不太好,所以官方的建議是使用的時候配合第三方的polyfill來用。
React提供的pointer events
- onPointerDown
- onPointerMove
- onPointerUp
- onPointerCancel
- onGotPointerCapture
- onLostPointerCapture
- onPointerEnter
- onPointerLeave
- onPointerOver
- onPointerOut
因為平時接觸較少,所以沒怎麼用過,就用官方Demo給大家看看吧,一定要升級到14以上哦,否則沒有這些屬性,感興趣的深入研究研究,畢竟這篇文章目的就是讓自己瞭解一下新特性~
官方demo效果如下:
【坑來了】:我自信滿滿的升級到Firefox和chrome到最新版本,然後把官方demo跑了一下,但是WTF?是下面這樣的結果。。。
很明顯,這些屬性依然不能被支援,也可能是我自己的問題?不清楚了,反正就是不能用。然後呢,我就查唄,讓我查到了這個東東 ——react-pointable
,
// 首先,安裝包
yarn add react-pointable
// 然後程式碼變成下面
import Pointable from 'react-pointable';
...
<Pointable
style={circleStyle}
onPointerDown={this.onDown}
onPointerMove={this.onMove}
onPointerUp={this.onUp}
onPointerCancel={this.onUp}
onGotPointerCapture={this.onGotCapture}
onLostPointerCapture={this.onLostCapture}
/>
複製程式碼
OK,可以動了!等一下~官方Demo摁住和鬆開的時候會變顏色,這裡沒變顏色,開啟控制檯發現還是有兩個報錯: 嗯,原來是6個,現在變成了兩個,說明還是解決了一部分問題,這是為啥呢,原來官方文件說了:它支援的事件如下,但是並不支援官方Demo裡面的onGotPointerCapture和onLostPointerCapture。這個包就是一種polyfill吧,按照我的理解,它最後渲染出來的效果就是官方程式碼那個樣子。 然後看下執行效果:
原本我想提個issue來的,O(∩_∩)O哈哈~,但是發現好像有人提了,反正暫時不支援就對了。也可能是我配置的不對?因為官方demo確實可以執行,而且瀏覽器版本也都支援pointer events事件,如果有大牛給我解答還是萬分感謝的~
總結
升級react日後應該是必然的事情,所以提前瞭解一下還是有幫助的,作為使用者暫時不做深入分析,當然,我也分析不明白,單純從更新角度來寫幾個demo給大家看一下變化,應該還挺清楚的~感謝閱讀!