React 16.0+ 新特性初探(How to use)

React 16.0之后的改动还是很大的,除了新增加了很多新特性之外,还明确表示未来会增加async render,增加async render之后,将会在17.0的版本完全废除当前版本的三个生命周期,对于已经习惯现在写法的小伙伴来说感觉有点方(至少我有点方),所以还是提前熟悉一下,做好升级的准备吧~

个人觉得升级是必然的事情,所以,还是提前准备一下,做好升级准备!

我技术没有大牛的水平,所以我写文章并不是为了吸引人,一方面是记录自己新学的东西,写出来觉得自己的理解也会加深;另一方面是让比我还入门的人找到个非常合适的入门文章。我喜欢配上一些Demo,这样不太明白的人才能看懂,受教人群不一样,大牛可以去看官方文档说明,小白可以看看demo感受一下新特性~ Demo地址 Demo大概长这个样子:

V16.0

16.0算是一个大版本,增加了很多新特性,解决了很多痛点问题~比如,可以render字符串和数组,比如增加了Fragment,这些在使用中都有效减少了dom节点的数量;还有可以使用portals将新节点插入在任何其他非父节点的dom节点下,对于modal,message等插件是福音;还有增加了error boundary,如果使用的好你再也不会在项目里看到满屏红色或者崩溃了,哈哈~

render多类型

16.0以后,react的render函数增加了几种类型,包括字符串和数组类型。

 render() {

     //不需要再把所有的元素绑定到一个单独的元素中了

      return [

        // 别忘记加上key值

        <li key="A"/>First itemli>,

        <li key="B"/>Second itemli>,

        <li key="C"/>Third itemli>,

      ];

    }
// 也可以用下面这种写法
 // 不需要再把所有的元素绑定到一个单独的元素中了
  render() {
    const arr = ['Adams', 'Bill', 'Charlie'];
    const Arr = () => (arr.map((item, index) => <p key={index}>{item}</p>));

    return <Arr />
  }
复制代码

从上图可以看出,解决了以往必须在外层包一个父元素div的限制,有效的减少了不必要的dom元素。

React.Fragment

解决的痛点问题与上面数组是相同的,不过个人感觉更加优雅,首先不需要加上key,其次就是增加一个不渲染的空标签看起来更加的整体,因为以前已经习惯了JSX语法需要一个父标签,这种写法更符合习惯。但是在16.0里提到了Fragment,而更详细的介绍是在16.2版本里,之所以放在这里说因为和返回数组解决的痛点是类似的~ 下面例子来自官网:

// 一个Table组件,里面嵌套了columns组件


class Table extends React.Component {
  render() {
    return (
      <table>
        <tr>
          <Columns />
        </tr>
      </table>
    );
  }
}
// columns组件


class Columns extends React.Component {
  render() {
    return (
      <div>
        <td>Hello</td>
        <td>World</td>
      </div>
    );
  }
}
复制代码

上面设计符合react,组件式划分,但是最后渲染出来却不是最佳的,因为columns的最外层嵌套了一层没用的div标签。这个问题存在于16.0之前。
有了Fragment以后,很好的解决问题:

import React, { Fragment } from 'react';



class Columns extends React.Component {
  render() {
    return (
      <Fragment>
        <td>Hello</td>
        <td>World</td>
      </Fragment>
    );
  }
}
// Fragment的语法糖

  <>
    <td>Hello</td>
    <td>World</td>
  </>
两个空标签
复制代码

这块糖有点苦,官方明明说的是语法糖,但是我试了,编译通不过,并且官方也特意说明了可能使用该语法糖会出现问题,但是给出的解决办法我都试了,还是不成功,可能配置的不对吧,有谁配置好了可以留言告诉我一下,不过无伤大雅,我倒是觉得语法糖也不一定必须使用。

Error Boundary

什么是Error Boundary?

单一组件内部错误,不应该导致整个应用报错并显示空白页,而Error Boundaries解决的就是这个问题。

在以前的React版本中,如果某一个组件内部出现异常错误,会导致整个项目崩溃直接显示空白页或者error红页,很不友好。error boundary就是解决这个问题的。

Error Boundary本质上是一个组件

按照我的个人理解,error boundary本质上就是一个组件,只不过组件内部多出现了一个生命周期,componentDidCatch,在这个生命周期里面,它会捕捉本组件下的所有子组件抛出的异常错误,包括堆栈信息。

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  componentDidCatch(error, info) {
    // Display fallback UI
    this.setState({ hasError: true });
    // You can also log the error to an error reporting service
    logErrorToMyService(error, info);
  }

  render() {
    if (this.state.hasError) {
      // You can render any custom fallback UI
      return <h1>Something went wrong.</h1>;
    }
    return this.props.children;
  }
}
// 使用起来就跟普通组件一样

<ErrorBoundary>
  <ChildCompA />
  <ChildCompB />
  ...
</ErrorBoundary>

复制代码

上面代码是官网给出的例子,在ErrorBoundary组件内,定义state={ hasError: false },在componentDidCatch内部捕捉到error,然后动态渲染组件,如果出现异常使用提前定义好的替换组件代替发生异常的组件,这样整个页面只有发生异常的部分被替换不影响其他内容的展示。

Portals

有些元素需要被挂载在更高层级的位置。最典型的应用场景:当父组件具有overflow: hidden或者z-index的样式设置时,组件有可能被其他元素遮挡,这个时候你就可以考虑要不要使用Portal使组件的挂载脱离父组件。

Portals 提供了一种很好的将子节点渲染到父组件以外的 DOM 节点的方式。

render() {
  // React does *not* create a new div. It renders the children into `domNode`.
  // `domNode` is any valid DOM node, regardless of its location in the DOM.
  return ReactDOM.createPortal(
    this.props.children,
    domNode,
  );
}

复制代码

一般而言,组件在装载的时候会就近装载在该组件最近的父元素下,而现在你可以使用Portal将组件渲染到任意一个已存在的dom元素下,这个dom元素并不一定必须是组件的父组件。

Portals的应用 —— Modal,message等消息提示

Portals的事件冒泡

从上图可以看出来,弹窗的父组件应该是挂载在#app这个dom下面的,通过portals,我们将modal框挂载在#portal_modal这个dom下了。虽然最后的modal组件没有挂载在整个应用所在的#app下,但是portals创建的组件里面的事件依然会冒泡给它自身的父组件,父组件可以捕获到被挂载在#portal_modal节点下面的modal的点击事件。

class PortalsComp extends Component {
  constructor(props) {
    super(props);
    this.state = { showModal: false, clickTime: 0 };
  }

  handleShow = () => {
    this.setState({ showModal: true });
  }
  
  handleHide = () => {
    this.setState({ showModal: false });
  }

  handleClick = () => {
    let { clickTime } = this.state;
    clickTime += 1;
    this.setState({ clickTime });
  }

  render() {
    const protalModal = this.state.showModal ? (
      <PortalModal>
        <ModalContent hideModal={this.handleHide} />
      </PortalModal>
    ) : null;
    return (
      <div className={s.portalContainer} onClick={this.handleClick}>
        <div>该组件被点击了: {this.state.clickTime}次</div>
        <Button onClick={this.handleShow} type='primary'>点我弹出Modal</Button>
        {protalModal}
      </div>
    );
  }
}

export default PortalsComp;
复制代码

从上图可以看出来,portals的组件虽然挂载在其他dom下,但是父组件依然可以捕获到modal的冒泡事件,打开和关闭,父组件显示点击次数为2。

V16.3

废弃的几个生命周期

  • componentWillMount
  • componentWillReceiveProps
  • componentWillUpdate

这三个生命周期之中,componentWillReceiveProps平时用的频率还是特别多的,所以对于以前的项目,可能升级会是一种麻烦事,但是说是废弃,但是其实在整个V16版本,还都是可以使用的,只不过会抛出警告,而且官方会建议使用的时候加上前缀UNSAFE_。 componentWillReceiveProps ---> UNSAFE_componentWillReceiveProps

为什么要废弃这三个生命周期

React16.0之前的生命周期设计如下图:

可以看到从开始到结束,这些生命周期的设计可以捕捉到组件的每一个state和props的改变,并没有任何逻辑上的问题,而且对于我们来说写法已经形成习惯,如果废弃肯定是费力不讨好的事情。那么为啥官方还是要皮这么一下呢?

虽然我英文不好,但是还是大致看了一下,意思呢,首先就是说这三个API经常被滥用和误用,再者就是在未来版本中,要引入async render(异步渲染),而在异步渲染的场景下,这些生命周期里面的代码会在未来的React版本里存在缺陷,因此就抛弃了。 这三个API存在的问题: React v16.3 版本新生命周期函数浅析及升级方案这里讲的很清楚。

为了弥补不足新增了两个生命周期

static getDerivedStateFromProps

触发时间:在组件构建之后(虚拟dom之后,实际dom挂载之前) ,以及每次获取新的props之后。

每次接收新的props之后都会返回一个对象作为新的state,返回null则说明不需要更新state. 配合componentDidUpdate,可以覆盖componentWillReceiveProps的所有用法。

// before
componentWillReceiveProps(nextProps) {
  if (nextProps.flag !== this.props.flag) {
    this.setState({	flag: nextProps.flag }, () => {
        if (nextProps.flag) {
            this.doSmething();
          }
    });
  }
}
// 在16.3之后的版本使用,react推荐下面这种写法,否则eslint可能会提示警告
UNSAFE_componentWillReceiveProps(nextProps) {
   // your code
}

// after
static getDrivedStateFromProps(nextProps, prevState) {
  if (nextProps.flag !== prevState.flag) {
      // 更新state
      return {
          flag: nextProps.flag
      }
  }
  // 不更新state
  return null;
}
// state更新过后需要做的事放在componentDidUpdate里
componentDidUpdate(prevProps, prevState) {
    if (prevState.flag !== this.props.flag) {
        this.doSomething();
    }
}
复制代码

写法与之前相比要麻烦了一些,但是处理逻辑上应该是更清晰了。在 componentWillReceiveProps 中,一般会进行两件事,第一、判断this.props与nextProps的异同,然后更新组件state;第二、根据state的变化更新组件或者执行一些回调函数。在以前的写法里,这两件事我们都需要在 componentWillReceiveProps 中去做。而在新版本中,官方将两件事分配到了两个不同的生命周期 getDerivedStateFromProps 与 componentDidUpdate 中去做,使得组件整体的更新逻辑更为清晰,getDerivedStateFromProps里面进行state的更新,componentDidUpdate里做更新之后的各种回调。而且在 getDerivedStateFromProps 中还禁止了组件去访问 this.props(static方法,获取不到组件的this),强制让开发者去比较 nextProps 与 prevState 中的值,以确保当开发者用到 getDerivedStateFromProps 这个生命周期函数时,就是在根据当前的 props 来更新组件的 state,而不是去做其他一些让组件自身状态变得更加不可预测的事情。

还是需要适应,虽然习惯了以前的写法,但是现在这种性能要更好。而且毕竟以后会废弃。

这里解决了我的很久一个困惑,组件最后的更新过程其实是: componentWillReceiveProps(static getDerivedStateFromProps)判断state的变化 ---> shouldComponentUpdate判断是否进行更新 render阶段会根据diff算法来生成需要更新的虚拟dom结构 ---> 更新虚拟dom ---> 虚拟dom更新完毕立刻调用componentDidUpdate ---> 最后完成渲染。

因为官方给出的定义是,componentDidUpdate是在组件dom更新结束之后立即调用,那么这个更新结束我理解的就是dom已经更新完毕渲染好了,但是我在componentDidUpdate里面调用了alert,发现其实进入该生命周期之后,其实dom还未发生变化,但是页面上的dom未发生变化,而componentDidUpdate获取dom的时候值确实正确的,可能这里是虚拟dom和真实dom不同步的关系吧,总之就是,在componentDidUpdate里面可以获取dom节点的操作,获取的值也是更新完毕的,下面的例子也是这样的。

getSnapshotBeforeUpdate ---- 针对对dom的一些操作

触发时间: update发生的时候,在render之后,在组件dom渲染之前。

返回一个值,作为componentDidUpdate的第三个参数。 配合componentDidUpdate, 可以覆盖componentWillUpdate的所有用法。

componentWillUpdate存在的问题
  • 与componentWillReceiveProps类似,同样在一层更新过程中可能会被调用多次,这样就会造成里面的回调函数可能会执行多次,浪费性能。
  • 在React17引入async render之后,render阶段和commit阶段可能并不是同步连贯的,因此,componentDidUpdate和componentWillUpdate获取到的Dom可能是不同的,这样就会导致读取到的dom元素的状态是不安全的。
getSnapshotBeforeUpdate配合componentDidUpdate来保证状态的一致

getSnapshotBeforeUpdate的发生时间在render之后,组件dom渲染之前,这样可以保证此时读取的dom和componentDidUpdate的dom是一致的。

getSnapshotBeforeUpdate不是静态方法,里面可以读取this.props和this.state等信息,并且调用之后应该返回一个值作为componentDidUpdate的第三个参数
static getDerivedStateFromProps(nextProps, prevState) {
    if (nextProps.disabled !== prevState.disabled) {
      return {
        disabled: nextProps.disabled
      };
    }
    return null;
  }
  

  getSnapshotBeforeUpdate(prevProps, prevState) {
    return this.props.disabled;
  }

  componentDidUpdate(prevProps, prevState, snapshot) {
    if (!snapshot) {
      // 如果snapshot是false,获取焦点
      this.domRef.focus();
    }
  }

  render() {
    return (
      <div>
        <input ref={(ref) => this.domRef = ref} disabled={this.state.disabled} />
      </div>
    );
  }
复制代码

V16.4

修复了getDerivedStateFromProps的bug,为了更好地兼容即将到来的异步渲染

这个点我还没太弄明白,因为我准备写的时候就已经是16.4了,也不太知道这个bug会导致什么影响,不过看了一些文章,大概意思是下面这样: 参考文章:React16.4 新特性

React这次更新修复了getDerivedStateFromProps这个生命周期的触发节点, 在之前, 它触发的方式和旧生命周期getDerivedStateFromProps类似, 都是在被父组件re-render的时候才会触发,并且本组件的setState的调用也不会触发
这种方式在之前同步渲染的时候是没有问题的, 但是为了支持新的还未启用的fiber异步渲染机制, 现在, getDerivedStateFromProps在组件每一次render的时候都会触发,也就是说无论是来自父组件的re-render, 还是组件自身的setState, 都会触发getDerivedStateFromProps这个生命周期。
要理解为什么react修复了这个生命周期的触发方式, 我们首先得了解react的异步渲染机制

react异步渲染

要理解react异步渲染的机制, 我们首先要说一说react之前是如何进行渲染。
在react16之前, 组件的渲染都是同步进行的, 也就是说从constructor开始到componentDidUpdate结束, react的运行都是没有中断的, 生命周期开始之后就会运行到其结束为止, 这样带来的一个缺点就是,如果组件嵌套很深, 渲染时间增长了之后, 一些重要的, 高优先级的操作就会被阻塞, 例如用户的输入等, 这样就会造成体验上的不友好。

在之后即将到来的异步渲染机制中, 会允许首先解决高优先级的运行,同时会暂停当前的渲染进程,当高优先级的进程结束之后, 再返回继续运行当前进程, 这样会大大的提高react的流畅度,给用户带来更好的体验

而这次修复getDerivedStateFromProps, 正是为了保证与即将到来的异步渲染模式的兼容。
复制代码

React pointer events

pointer events是HTML5规范的WEB API,它主要目的是用来将鼠标(Mouse)、触摸(touch)和触控笔(pen)三种事件整合为统一的API。

如果你的应用涉及到指针的相关事件,那么这个API还是很有用的,不过这个API的兼容性不怎么样,基本主流浏览器的最新版本才支持,从React增加了这个pointer events事件来看,说明React官方还是很看重这个API的,我觉得兼容性肯定满满的会越来越好。

因为兼容性不太好,所以官方的建议是使用的时候配合第三方的polyfill来用。

React提供的pointer events

  • onPointerDown
  • onPointerMove
  • onPointerUp
  • onPointerCancel
  • onGotPointerCapture
  • onLostPointerCapture
  • onPointerEnter
  • onPointerLeave
  • onPointerOver
  • onPointerOut

因为平时接触较少,所以没怎么用过,就用官方Demo给大家看看吧,一定要升级到14以上哦,否则没有这些属性,感兴趣的深入研究研究,毕竟这篇文章目的就是让自己了解一下新特性~

官方demo效果如下:

【坑来了】:我自信满满的升级到Firefox和chrome到最新版本,然后把官方demo跑了一下,但是WTF?是下面这样的结果。。。

很明显,这些属性依然不能被支持,也可能是我自己的问题?不清楚了,反正就是不能用。然后呢,我就查呗,让我查到了这个东东 —— react-pointable,

// 首先,安装包
yarn add react-pointable
// 然后代码变成下面
import Pointable from 'react-pointable';
...
<Pointable
  style={circleStyle}
  onPointerDown={this.onDown}
  onPointerMove={this.onMove}
  onPointerUp={this.onUp}
  onPointerCancel={this.onUp}
  onGotPointerCapture={this.onGotCapture}
  onLostPointerCapture={this.onLostCapture}
/>
复制代码

这个包就是一种polyfill吧,按照我的理解,它最后渲染出来的效果就是官方代码那个样子。 然后看下运行效果:

OK,可以动了!等一下~官方Demo摁住和松开的时候会变颜色,这里没变颜色,打开控制台发现还是有两个报错:

嗯,原来是6个,现在变成了两个,说明还是解决了一部分问题,这是为啥呢,原来官方文档说了:它支持的事件如下,但是并不支持官方Demo里面的onGotPointerCapture和onLostPointerCapture。

原本我想提个issue来的,O(∩_∩)O哈哈~,但是发现好像有人提了,反正暂时不支持就对了。也可能是我配置的不对?因为官方demo确实可以运行,而且浏览器版本也都支持pointer events事件,如果有大牛给我解答还是万分感谢的~

总结

升级react日后应该是必然的事情,所以提前了解一下还是有帮助的,作为使用者暂时不做深入分析,当然,我也分析不明白,单纯从更新角度来写几个demo给大家看一下变化,应该还挺清楚的~感谢阅读!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值