歡迎大家前往騰訊雲+社群,獲取更多騰訊海量技術實踐乾貨哦~
![img](https://i.iter01.com/images/dc6066717b6d69b637679024ce070ef2e5177417edc57422cf5fcfb887cf2578.jpg)
自從去年9月份 React 團隊釋出了 v16.0 版本開始,到18年3月剛釋出的 v16.3 版本,React 陸續推出了多項重磅新特性,並改進了原有功能中反饋呼聲很高的一些問題,例如 render 方法內單節點層級巢狀問題,提供生命週期錯誤捕捉,元件指定 render 到任意 DOM 節點 (Portal) 等能力,以及最新的 Context API 和 Ref API。我們在對以上新特性經過一段時間的使用過後,通過本文進行一些細節分享和總結。
一、render 方法優化
![img](https://i.iter01.com/images/b48db9edb40582edf0e4d7f008d4f78f69660f1846a68474d9a99742372b3f27.jpg)
為了符合 React 的 component tree 和 diff 結構設計,在元件的 render() 方法中頂層必須包裹為單節點,因此實際元件設計和使用中總是需要注意巢狀後的層級變深,這是 React 的一個經常被人詬病的問題。比如以下的內容結構就必須再巢狀一個 div 使其變為單節點進行返回:
render() {
return (
<div>
注:
<p>產品說明一</p>
<p>產品說明二</p>
</div>
);
}
複製程式碼
現在在更新 v16 版本後,這個問題有了新的改進,render 方法可以支援返回陣列了:
render() {
return [
"注:",
<p key="t-1">產品說明一</h2>,
<p key="t-2">產品說明二</h2>,
];
}
複製程式碼
這樣確實少了一層,但大家又繼續發現程式碼還是不夠簡潔。首先 TEXT 節點需要用引號包起來,其次由於是陣列,每條內容當然還需要新增逗號分隔,另外 element 上還需要手動加 key 來輔助 diff。給人感覺就是不像在寫 JSX 了。
於是 React v16.2 趁熱打鐵,提供了更直接的方法,就是 Fragment:
render() {
return (
<React.Fragment>
注:
<p>產品說明一</p>
<p>產品說明二</p>
</React.Fragment>
);
}
複製程式碼
可以看到是一個正常單節點寫法,直接包裹裡面的內容。但是 Fragment 本身並不會產生真實的 DOM 節點,因此也不會導致層級巢狀增加。
另外 Fragment 還提供了新的 JSX 簡寫方式 <></>:
render() {
return (
<>
注:
<p>產品說明一</p>
<p>產品說明二</p>
</>
);}
複製程式碼
看上去是否舒服多了。不過注意如果需要給 Fragment 新增 key prop,是不支援使用簡寫的(這也是 Fragment 唯一會遇到需要新增props的情況):
<dl>
{props.items.map(item => (
// 要傳key用不了 <></>
<Fragment key={item.id}>
<dt>{item.term}</dt>
<dd>{item.description}</dd>
</Fragment>
))}
</dl>
複製程式碼
二、錯誤邊界 (Error Boundaries)
![img](https://i.iter01.com/images/6e37db9f0444b1447b2cba41aea697e5b5a0a97cbc3c3daac27588d7fb0117f9.jpg)
錯誤邊界是指以在元件上定義 componentDidCatch 方法的方式來建立一個有錯誤捕捉功能的元件,在其內巢狀的元件在生命過程中發生的錯誤都會被其捕捉到,而不會上升到外部導致整個頁面和元件樹異常 crash。
例如下面的例子就是通過一個 ErrorBoundary 元件對其內的內容進行保護和錯誤捕捉,並在發生錯誤時進行兜底的UI展示:
class ErrorBoundary extends Component {
constructor(props) {
super(props);
this.state = { error: null };
}
componentDidCatch(error,
{componentStack}
) {
this.setState({
error,
componentStack,
});
}
render() {
if (this.state.error) {
return (
<>
<h1>報錯了.</h1>
<ErrorPanel {...this.state} />
</>
);
}
return this.props.children;
}
}
export default function App(){
return (
<ErrorBoundary>
<Content />
</ErrorBoundary>
);
}
複製程式碼
需要注意的是錯誤邊界只能捕捉生命週期中的錯誤 (willMount / render 等方法內)。無法捕捉非同步的、事件回撥中的錯誤,要捕捉和覆蓋所有場景依然需要配合 window.onerror、Promise.catch、 try/catch 等方式。
三、React.createPortal()
![img](https://i.iter01.com/images/e0b02a68a0b852ad68cb4937c3a49775ad92b7d772b50557de55030d1d8e6088.jpg)
這個 API 是用來將部分內容分離式地 render 到指定的 DOM 節點上。不同於使用 ReactDom.render 新建立一個 DOM tree 的方式,對於要通過 createPortal() “分離”出去的內容,其間的資料傳遞,生命週期,甚至事件冒泡,依然存在於原本的抽象元件樹結構當中。
class Creater extends Component {
render(){
return (
<div onClick={() =>
alert("clicked!")
}>
<Portal>
<img src={myImg} />
</Portal>
</div>
);
}
}
class Portal extends Component {
render(){
const node = getDOMNode();
return createPortal(
this.props.children,
node
);
}
}
複製程式碼
例如以上程式碼, 通過 把裡面的 內容渲染到了一個獨立的節點上。在實際的 DOM 結構中,img 已經脫離了 Creater 本身的 DOM 樹存在於另一個獨立節點。但當點選 img 時,仍然可以神奇的觸發到 Creater 內的 div 上的 onclick 事件。這裡實際依賴於 React 代理和重寫了整套事件系統,讓整個抽象元件樹的邏輯得以保持同步。
四、Context API
![img](https://i.iter01.com/images/a7fd998681554cfe4629dbb11150b30824092cfd9852631aa7d28aed33a88afc.jpg)
以前的版本中 Context API 是作為未公開的實驗性功能存在的,隨著越來越多的聲音要求對其進行完善,在 v16.3 版本,React 團隊重新設計併發布了新的官方 Context API。
使用 Context API 可以更方便的在元件中傳遞和共享某些 "全域性" 資料,這是為了解決以往元件間共享公共資料需要通過多餘的 props 進行層層傳遞的問題 (props drilling)。比如以下程式碼:
const HeadTitle = (props) => {
return (
<Text>
{props.lang.title}
</Text>;
);
};
// 中間元件
const Head = (props) => {
return (
<div>
<HeadTitle lang={props.lang} />
</div>
);
};
class App extends React.Component {
render() {
return (
<Head lang={this.props.lang} />;
);
}
}
export default App = connect((state) => {
return {
lang:state.lang
}
})(App);
複製程式碼
我們為了使用一個語言包,把語言配置儲存到一個 store 裡,通過 Redux connect 到頂層元件,然而僅僅是最底端的子元件才需要用到。我們也不可能為每個元件都單獨加上 connect,這會造成資料驅動更新的重複和不可維護。因此中間元件需要一層層不斷傳遞下去,就是所謂的 props drilling。
對於這種全域性、不常修改的資料共享,就比較適合用 Context API 來實現:
首先第一步,類似 store,我們可以先建立一個 Context,並加入預設值:
const LangContext = React.createContext({
title:"預設標題"
});
複製程式碼
然後在頂層通過 Provider 向元件樹提供 Context 的訪問。這裡可以通過傳入 value 修改 Context 中的資料,當value變化的時候,涉及的 Consumer 內整個內容將重新 render:
class App extends React.Component {
render() {
return (
<LangContext.Provider
value={this.state.lang}
>
<Head />
</LangContext.Provider>
);
}
}
複製程式碼
在需要使用資料的地方,直接用 Context.Consumer 包裹,裡面可以傳入一個 render 函式,執行時從中取得 Context 的資料。
const HeadTitle = (props) => {
return (
<LangContext.Consumer>
{lang =>
<Text>{lang.title}</Text>
}
</LangContext.Consumer>
);
};
複製程式碼
之後的中間元件也不再需要層層傳遞了,少了很多 props,減少了中間漏傳導致出錯,程式碼也更加清爽:
// 中間元件
const Head = () => {
return (
<div>
<HeadTitle />
</div>
);
};
複製程式碼
那麼看了上面的例子,我們是否可以直接使用 Context API 來代替掉所有的資料傳遞,包括去掉 redux 這些資料同步 library 了?其實並不合適。前面也有提到,Context API 應該用於需要全域性共享資料的場景,並且資料最好是不用頻繁更改的。因為作為上層存在的 Context,在資料變化時,容易導致所有涉及的 Consumer 重新 render。
比如下面這個例子:
render() {
return (
<Provider value={{
title:"my title"
}} >
<Content />
</Provider>
);
}
複製程式碼
實際每次 render 的時候,這裡的 value 都是傳入一個新的物件。這將很容易導致所有的 Consumer 都重新執行 render 影響效能。
因此不建議濫用 Context,對於某些非全域性的業務資料,也不建議作為全域性 Context 放到頂層中共享,以免導致過多的 Context 巢狀和頻繁重新渲染。
五、Ref API
除了 Context API 外,v16.3 還推出了兩個新的 Ref API,用來在元件中更方便的管理和使用 ref。
在此之前先看一下我們之前使用 ref 的兩種方法。
// string命名獲取
componentDidMount(){
console.log(this.refs.input);
}
render() {
return (
<input
ref="input"
/>
);
}
複製程式碼
// callback 獲取
render() {
return (
<input
ref={el => {this.input = el;}}
/>
);
}
複製程式碼
前一種 string 的方式比較侷限,不方便於多元件間的傳遞或動態獲取。後一種 callback 方法是之前比較推薦的方法。但是寫起來略顯麻煩,而且 update 過程中有發生清除可能會有多次呼叫 (callback 收到 null)。
為了提升易用性,新版本推出了 CreateRef API 來建立一個 ref object, 傳遞到 component 的 ref 上之後可以直接獲得引用:
constructor(props) {
super(props);
this.input = React.createRef();
}
componentDidMount() {
console.log(this.input);
}
render() {
return <input ref={this.input} />;
}
複製程式碼
另外還提供了 ForwardRef API 來輔助簡化巢狀元件、component 至 element 間的 ref 傳遞,避免出現 this.ref.ref.ref 的問題。
例如我們有一個包裝過的 Button 元件,想獲取裡面真正的 button DOM element,本來需要這樣做:
class MyButton extends Component {
constructor(props){
super(props);
this.buttonRef = React.createRef();
}
render(){
return (
<button ref={this.buttonRef}>
{props.children}
</button>
);
}
}
class App extends Component {
constructor(props){
super(props);
this.myRef = React.createRef();
}
componentDidComponent{
// 通過ref一層層訪問
console.log(this.myRef.buttonRef);
}
render(){
return (
<MyButton ref={this.myRef}>
Press here
</MyButton>
);
}
}
複製程式碼
這種場景使用 forwardRef API 的方式做一個“穿透”,就能簡便許多:
import { createRef, forwardRef } from "react";
const MyButton = forwardRef((props, ref) => (
<button ref={ref}>
{props.children}
</button>
));
class App extends Component {
constructor(props){
super(props);
this.realButton = createRef();
}
componentDidComponent{
//直接拿到 inner element ref
console.log(this.realButton);
}
render(){
return (
<MyButton ref={this.realButton}>
Press here
</MyButton>
);
}
}
複製程式碼
總結
以上就是 React v16 釋出以來幾個比較重要和有用的新特性,優化的同時也帶來了開發體驗的提升。另外 v16 對比之前版本還有不錯的包大小降低,也是非常具有優勢的:
![img](https://i.iter01.com/images/bac4b497b41e8c9d13fa2f881314d79c1046c6c9ecc961c7c117b5c8ac424ee8.jpg)
除此之外,想要了解更多的一些變更比如生命週期的更新 (getDerivedStateFromProps, getSnapshotBeforeUpdate) 和 SSR 的優化 (hydrate),以及即將推出的 React Fiber (async render) 動向,可以點選檢視原文了解更多的官方資訊。
這麼多激動人心的特性,如果你還在用 v15 甚至舊版,就趕快升級體驗吧!
問答
相關閱讀
Android Native 開發之 NewString 與 NewStringUtf 解析
此文已由作者授權騰訊雲+社群釋出,原文連結:https://cloud.tencent.com/developer/article/1137778?fromSource=waitui
歡迎大家前往騰訊雲+社群或關注雲加社群微信公眾號(QcloudCommunity),第一時間獲取更多海量技術實踐乾貨哦~