QQ音樂:React v16 新特性實踐

歡迎你們前往騰訊雲+社區,獲取更多騰訊海量技術實踐乾貨哦~node

本文由 QQ音樂技術團隊發表於 雲+社區專欄

img

自從去年9月份 React 團隊發佈了 v16.0 版本開始,到18年3月剛發佈的 v16.3 版本,React 陸續推出了多項重磅新特性,並改進了原有功能中反饋呼聲很高的一些問題,例如 render 方法內單節點層級嵌套問題,提供生命週期錯誤捕捉,組件指定 render 到任意 DOM 節點 (Portal) 等能力,以及最新的 Context API 和 Ref API。咱們在對以上新特性通過一段時間的使用事後,經過本文進行一些細節分享和總結。react

1、render 方法優化

img

爲了符合 React 的 component tree 和 diff 結構設計,在組件的 render() 方法中頂層必須包裹爲單節點,所以實際組件設計和使用中老是須要注意嵌套後的層級變深,這是 React 的一個常常被人詬病的問題。好比如下的內容結構就必須再嵌套一個 div 使其變爲單節點進行返回:redux

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 簡寫方式 <></>:async

render() {
  return (
    <>
      注:
      <p>產品說明一</p>
      <p>產品說明二</p>
    </>
  );}

看上去是否舒服多了。不過注意若是須要給 Fragment 添加 key prop,是不支持使用簡寫的(這也是 Fragment 惟一會遇到須要添加props的狀況):ide

<dl>
  {props.items.map(item => (
    // 要傳key用不了 <></>
    <Fragment key={item.id}>
      <dt>{item.term}</dt>
      <dd>{item.description}</dd>
    </Fragment>
  ))}
</dl>

2、錯誤邊界 (Error Boundaries)

img

錯誤邊界是指以在組件上定義 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 等方式。

3、React.createPortal()

img

這個 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 
    ); 
  }
}

例如以上代碼,<Creater> 經過 <Portal> 把裏面的 <img > 內容渲染到了一個獨立的節點上。在實際的 DOM 結構中,img 已經脫離了 Creater 自己的 DOM 樹存在於另外一個獨立節點。但當點擊 img 時,仍然能夠神奇的觸發到 Creater 內的 div 上的 onclick 事件。這裏實際依賴於 React 代理和重寫了整套事件系統,讓整個抽象組件樹的邏輯得以保持同步。

4、Context API

img

之前的版本中 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 嵌套和頻繁從新渲染。

5、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

除此以外,想要了解更多的一些變動好比生命週期的更新 (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/dev...

歡迎你們前往騰訊雲+社區或關注雲加社區微信公衆號(QcloudCommunity),第一時間獲取更多海量技術實踐乾貨哦~

相關文章
相關標籤/搜索