React v16新特性在qq音乐产品上的实践

2018-09-1508:40:34WEB前端开发Comments2,411 views字数 5876阅读模式
React v16新特性在qq音乐产品上的实践

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

一、render 方法优化

React v16新特性在qq音乐产品上的实践

为了符合 React 的 component tree 和 diff 结构设计,在组件的 render() 方法中顶层必须包裹为单节点,因此实际组件设计和使用中总是需要注意嵌套后的层级变深,这是 React 的一个经常被人诟病的问题。比如以下的内容结构就必须再嵌套一个 div 使其变为单节点进行返回:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/4659.html

render() {
  return (
    <div>
      注:
      <p>产品说明一</p>
      <p>产品说明二</p>
    </div>
  );
}
复制代码

现在在更新 v16 版本后,这个问题有了新的改进,render 方法可以支持返回数组了:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/4659.html

render() {
  return [
    "注:",
    <p key="t-1">产品说明一</h2>,
    <p key="t-2">产品说明二</h2>,
  ];
}
复制代码

这样确实少了一层,但大家又继续发现代码还是不够简洁。首先 TEXT 节点需要用引号包起来,其次由于是数组,每条内容当然还需要添加逗号分隔,另外 element 上还需要手动加 key 来辅助 diff。给人感觉就是不像在写 JSX 了。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/4659.html

于是 React v16.2 趁热打铁,提供了更直接的方法,就是 Fragment:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/4659.html

render() {
  return (
    <React.Fragment>
      注:        
      <p>产品说明一</p>
      <p>产品说明二</p>
    </React.Fragment>
  );
}
复制代码

可以看到是一个正常单节点写法,直接包裹里面的内容。但是 Fragment 本身并不会产生真实的 DOM 节点,因此也不会导致层级嵌套增加。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/4659.html

另外 Fragment 还提供了新的 JSX 简写方式 <></>:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/4659.html

render() {
  return (
    <>
      注:
      <p>产品说明一</p>
      <p>产品说明二</p>
    </>
  );}
复制代码

看上去是否舒服多了。不过注意如果需要给 Fragment 添加 key prop,是不支持使用简写的(这也是 Fragment 唯一会遇到需要添加props的情况):文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/4659.html

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

二、错误边界 (Error Boundaries)

React v16新特性在qq音乐产品上的实践

错误边界是指以在组件上定义 componentDidCatch 方法的方式来创建一个有错误捕捉功能的组件,在其内嵌套的组件在生命过程中发生的错误都会被其捕捉到,而不会上升到外部导致整个页面和组件树异常 crash。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/4659.html

例如下面的例子就是通过一个 ErrorBoundary 组件对其内的内容进行保护和错误捕捉,并在发生错误时进行兜底的UI展示:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/4659.html

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 等方式。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/4659.html

三、React.createPortal()

React v16新特性在qq音乐产品上的实践

这个 API 是用来将部分内容分离式地 render 到指定的 DOM 节点上。不同于使用 ReactDom.render 新创建一个 DOM tree 的方式,对于要通过 createPortal() “分离”出去的内容,其间的数据传递,生命周期,甚至事件冒泡,依然存在于原本的抽象组件树结构当中。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/4659.html

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 
    ); 
  }
}
复制代码

例如以上代码, 通过 把里面的 React v16新特性在qq音乐产品上的实践 内容渲染到了一个独立的节点上。在实际的 DOM 结构中,img 已经脱离了 Creater 本身的 DOM 树存在于另一个独立节点。但当点击 img 时,仍然可以神奇的触发到 Creater 内的 div 上的 onclick 事件。这里实际依赖于 React 代理和重写了整套事件系统,让整个抽象组件树的逻辑得以保持同步。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/4659.html

四、Context API

React v16新特性在qq音乐产品上的实践

以前的版本中 Context API 是作为未公开的实验性功能存在的,随着越来越多的声音要求对其进行完善,在 v16.3 版本,React 团队重新设计并发布了新的官方 Context API。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/4659.html

使用 Context API 可以更方便的在组件中传递和共享某些 "全局" 数据,这是为了解决以往组件间共享公共数据需要通过多余的 props 进行层层传递的问题 (props drilling)。比如以下代码:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/4659.html

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。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/4659.html

对于这种全局、不常修改的数据共享,就比较适合用 Context API 来实现:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/4659.html

首先第一步,类似 store,我们可以先创建一个 Context,并加入默认值:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/4659.html

const LangContext = React.createContext({
  title:"默认标题"
});
复制代码

然后在顶层通过 Provider 向组件树提供 Context 的访问。这里可以通过传入 value 修改 Context 中的数据,当value变化的时候,涉及的 Consumer 内整个内容将重新 render:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/4659.html

class App extends React.Component {
  render() {
    return (
      <LangContext.Provider
        value={this.state.lang}
      >
        <Head />
      </LangContext.Provider>
    );
  }
}
复制代码

在需要使用数据的地方,直接用 Context.Consumer 包裹,里面可以传入一个 render 函数,执行时从中取得 Context 的数据。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/4659.html

const HeadTitle = (props) => {
  return (
    <LangContext.Consumer>
      {lang => 
        <Text>{lang.title}</Text>
      }
    </LangContext.Consumer>
  );
};
复制代码

之后的中间组件也不再需要层层传递了,少了很多 props,减少了中间漏传导致出错,代码也更加清爽:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/4659.html

// 中间组件
const Head = () => {
  return (
    <div>
      <HeadTitle />
    </div>
  );
};
复制代码

那么看了上面的例子,我们是否可以直接使用 Context API 来代替掉所有的数据传递,包括去掉 redux 这些数据同步 library 了?其实并不合适。前面也有提到,Context API 应该用于需要全局共享数据的场景,并且数据最好是不用频繁更改的。因为作为上层存在的 Context,在数据变化时,容易导致所有涉及的 Consumer 重新 render。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/4659.html

比如下面这个例子:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/4659.html

render() {
  return (
    <Provider value={{
      title:"my title"
    }} >
      <Content />
    </Provider>
  );
}
复制代码

实际每次 render 的时候,这里的 value 都是传入一个新的对象。这将很容易导致所有的 Consumer 都重新执行 render 影响性能。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/4659.html

因此不建议滥用 Context,对于某些非全局的业务数据,也不建议作为全局 Context 放到顶层中共享,以免导致过多的 Context 嵌套和频繁重新渲染。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/4659.html

五、Ref API

除了 Context API 外,v16.3 还推出了两个新的 Ref API,用来在组件中更方便的管理和使用 ref。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/4659.html

在此之前先看一下我们之前使用 ref 的两种方法。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/4659.html

// 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)。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/4659.html

为了提升易用性,新版本推出了 CreateRef API 来创建一个 ref object, 传递到 component 的 ref 上之后可以直接获得引用:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/4659.html

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 的问题。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/4659.html

例如我们有一个包装过的 Button 组件,想获取里面真正的 button DOM element,本来需要这样做:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/4659.html

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 的方式做一个“穿透”,就能简便许多:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/4659.html

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 对比之前版本还有不错的包大小降低,也是非常具有优势的:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/4659.html

React v16新特性在qq音乐产品上的实践

除此之外,想要了解更多的一些变更比如生命周期的更新 (getDerivedStateFromProps, getSnapshotBeforeUpdate) 和 SSR 的优化 (hydrate),以及即将推出的 React Fiber (async render) 动向,可以点击查看原文了解更多的官方信息。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/4659.html

作者:腾讯云加社区
链接:https://juejin.im/post/5b2236016fb9a00e9c47cb6b
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/4659.html

文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/4659.html
  • 本站内容整理自互联网,仅提供信息存储空间服务,以方便学习之用。如对文章、图片、字体等版权有疑问,请在下方留言,管理员看到后,将第一时间进行处理。
  • 转载请务必保留本文链接:https://www.cainiaoxueyuan.com/gcs/4659.html

Comment

匿名网友 填写信息

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定