歡迎大家前往騰訊雲+社區,獲取更多騰訊海量技術實踐干貨哦~
自從去年9月份 React 團隊發布了 v16.0 版本開始,到18年3月剛發布的 v16.3 版本,React 陸續推出了多項重磅新特性,並改進了原有功能中反饋呼聲很高的一些問題,例如 render 方法內單節點層級嵌套問題,提供生命周期錯誤捕捉,組件指定 render 到任意 DOM 節點 (Portal) 等能力,以及最新的 Context API 和 Ref API。我們在對以上新特性經過一段時間的使用過后,通過本文進行一些細節分享和總結。
一、render 方法優化
為了符合 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)
錯誤邊界是指以在組件上定義 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()
這個 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
以前的版本中 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 對比之前版本還有不錯的包大小降低,也是非常具有優勢的:
除此之外,想要了解更多的一些變更比如生命周期的更新 (getDerivedStateFromProps, getSnapshotBeforeUpdate) 和 SSR 的優化 (hydrate),以及即將推出的 React Fiber (async render) 動向,可以點擊查看原文了解更多的官方信息。
這么多激動人心的特性,如果你還在用 v15 甚至舊版,就趕快升級體驗吧!
問答 如何從jQuery轉到React.js? 相關閱讀 React Native在全民K歌APP中的使用分享 Android Native 開發之 NewString 與 NewStringUtf 解析 React-Native 分包實踐
**此文已由作者授權騰訊雲+社區發布,原文鏈接:https://cloud.tencent.com/developer/article/1137778?fromSource=waitui **
歡迎大家前往騰訊雲+社區或關注雲加社區微信公眾號(QcloudCommunity),第一時間獲取更多海量技術實踐干貨哦~