QQ音乐:React v16 新特性实践

7,279 阅读9分钟

欢迎大家前往腾讯云+社区,获取更多腾讯海量技术实践干货哦~

本文由QQ音乐技术团队发表于云+社区专栏

img

自从去年9月份 React 团队发布了 v16.0 版本开始,到18年3月刚发布的 v16.3 版本,React 陆续推出了多项重磅新特性,并改进了原有功能中反馈呼声很高的一些问题,例如 render 方法内单节点层级嵌套问题,提供生命周期错误捕捉,组件指定 render 到任意 DOM 节点 (Portal) 等能力,以及最新的 Context API 和 Ref API。我们在对以上新特性经过一段时间的使用过后,通过本文进行一些细节分享和总结。

一、render 方法优化

img

为了符合 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

错误边界是指以在组件上定义 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

这个 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

以前的版本中 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

除此之外,想要了解更多的一些变更比如生命周期的更新 (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),第一时间获取更多海量技术实践干货哦~