React new feature

224 阅读4分钟

React 新技能学习笔记

React Fragments/Protals/Strict Mode

Fragments

通常在render方法中需要返回一堆元素。需要在并列元素外包裹一个元素,导致每新增一个组件就会多一层节点,特别是很多时候这层包裹毫无意义,只是为了满足语法要求而存在,现在可以利用Fragment:

class AppWidthFragment extends React.Component {
  render() {
    return (
      <React.Fragment>
        <span>AppWidthFragment</span>
        <div className='app-item'>hello</div>
        <div className='app-item'>world</div>
      </React.Fragment>
    )
  }
}
class AppWidthoutFragment extends React.Component {
  render() {
    return (
      <div>
        <span>AppWidthoutFragment</span>
        <div className='app-item'>hello</div>
        <div className='app-item'>world</div>
      </div>
    )
  }
}

具体节点位置如下图:

其实除了Fragments,render函数也支持返回数组

class AppWidthArray extends React.Component {
  render() {
    return [
        <div className='app-item'>hello</div>
        <div className='app-item'>world</div>
    ]
  }
}

Portals

React提供了一个能改变挂载节点的API。通常的开发,组件会挂载在 其最近的父节点上。在某些特定的需求上,需要挂载在特定的节点上。

  1. 之前的做法是封装成方法,而不是在render中引入。
  2. 既想在render中引入,又能自定义挂载节点就需要Portals。

一个 portal 的典型用例是当父组件有 overflow: hidden 或 z-index 样式时,但你需要子组件能够在视觉上“跳出”其容器。例如,对话框、悬浮卡以及提示框:官方实例demo1

有趣的是Portal 中事件冒泡行为;一个从 portal 内部触发的事件会一直冒泡至包含 React 树的祖先,即便这些元素并不是 DOM 树 中的祖先。 HTML 结构:

<html>
  <body>
    <div id="app-root"></div>
    <div id="modal-root"></div>
  </body>
</html>

Parent 组件能够捕获到从兄弟节点 #modal-root 冒泡上来的事件。

const appRoot = document.getElementById('app-root');
const modalRoot = document.getElementById('modal-root');

class Modal extends React.Component {
  constructor(props) {
    super(props);
    this.el = document.createElement('div');
  }

  componentDidMount() {
    modalRoot.appendChild(this.el);
  }

  componentWillUnmount() {
    modalRoot.removeChild(this.el);
  }

  render() {
    return ReactDOM.createPortal(
      this.props.children,
      this.el
    );
  }
}

class Parent extends React.Component {
  constructor(props) {
    super(props);
    this.state = {clicks: 0};
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    this.setState(state => ({
      clicks: state.clicks + 1
    }));
  }

  render() {
    return (
      <div onClick={this.handleClick}>
        <p>Number of clicks: {this.state.clicks}</p>
        <p>
          Open up the browser DevTools
          to observe that the button
          is not a child of the div
          with the onClick handler.
        </p>
        <Modal>
          <Child />
        </Modal>
      </div>
    );
  }
}

function Child() {
  return (
    <div className="modal">
      <button>Click</button>
    </div>
  );
}

dom树上表现形式如下图:

官方实例demo2

Strict Mode

Strict Mode提供一个可以显示潜在问题的组件。

  1. 检测是否使用即将废弃的生命周期函数。
  2. 检测是否使用string ref 和findDOMNode 以及老版的context api。
  3. 检测是否多次调用不可预测的副作用。

如下,检测到使用过时字符串 ref API ,控制台输出警告

class Parent extends React.Component {
  render() {
    return (
      <React.StrictMode>
        <div className='parent'>
        <div ref='child'>i am a child node</div>
      </div>
      </React.StrictMode>
    );
  }
}

React Concurrent Mode

  • 一个还在实验阶段的特性;
  • 目的:让React应用能够更好的响应交互并且还能根据用户设备的硬件性能和网络条件进行对应的调节。

在Concurrent模式下,渲染更新是可以被中断的。特别是当渲染的过程特别消耗的时候,中断渲染来响应用户行为,让整个应用的体验得到提升,也就是说ConcurrentMode可以降低当前组件更新的优先级,而flushSync模拟在更新过程中插入优先级更高任务的操作。

Concurrent Mode API

详细可挪步react官网文档

开启Concurrent模式
1. createRoot
ReactDOM.createRoot(rootNode).render(<App />);
// 使用上述代码替换 ReactDOM.render(<App />, rootNode) , 用 Concurrent 模式。
2. createBlockingRoot
ReactDOM.createBlockingRoot(rootNode).render(<App />);
// 使用上述代码替换 ReactDOM.render(<App />, rootNode) , 用 Blocking 模式。
//  Blocking仅提供了 concurrent 模式的小部分功能,它更接 近于 React 今天的工作方式
Concurrent模式初探
import React, { ConcurrentMode } from 'react'
import { flushSync } from 'react-dom'
import './style.css'

class Parent extends React.Component {
  state = {
    async: true,
    num: 1,
    length: 2000,
  }

  componentDidMount() {
    this.interval = setInterval(() => {
      this.updateNum()
    }, 200)
  }

  updateNum() {
    const newNum = this.state.num === 5 ? 0 : this.state.num + 1
  
    this.state.async 
    ? this.setState({ num: newNum }) 
    : flushSync(() => this.setState({ num: newNum }))
  }

  render() {
    const children = []
    const { length, num, async } = this.state
    for (let i = 0; i < length; i++) {
      children.push(
        <div className="item" key={i}>
          {num}
        </div>,
      )
    }

    return (
      <div className="main">
        async:{' '}
        <input
          type="checkbox"
          checked={async}
          onChange={() => flushSync(() => this.setState({ async: !async }))}
        />
        <div className="wrapper">{children}</div>
      </div>
    )
  }
}

export default () => (
  <ConcurrentMode>
    <Parent />
  </ConcurrentMode>
)
/* less */
@keyframes slide {
  0% {
    margin-left: 0;
    /* transform: translateX(0); */
  }

  50% {
    margin-left: 200px;
    /* transform: translateX(200px); */
  }

  100% {
    margin-left: 0;
    /* transform: translateX(0); */
  }
}
.wrapper {
  width: 400px;
  animation-duration: 3s;
  animation-name: slide;
  animation-iteration-count: infinite;
  display: flex;
  flex-wrap: wrap;
  background: red;
}
.item {
  width: 20px;
  height: 20px;
  line-height: 20px;
  text-align: center;
  border: 1px solid #aaa;
}

再这demo中ConcurrentMode的作用是降低当前组件的优先级,而flushSync模拟在更新过程中插入优先级更高任务的操作。运行结果如果下: 这里我们设置num更新200ms一次,但代码运行起来时我们发现,实际3秒左右才更新,可见当我们的组件被ConcurrentMode包裹以后会被标记为低优先级任务,Parent里面的this.setState触发视图更新,但由于低于我们的动画优先级,所以等动画结束的时候才能更新当前dom,而当我们提升优先级(flushSync 为最高优先级)之后,当前组件渲染情况就变成了我们正常使用的场景。

*要还原这个demo注意两点:

  1. react和react-dom下载的版本为"16.7.0-alpha.2",因为目前在稳定版本中还不可用
  2. 浏览器开启slowdown CPU模拟cpu较慢的情况
Suspense

这是我最期待的新特性,在开发中常有一个页面切换到另一个页面的场景,新页面可能需要一些时间才能加载完成,让用户停留在空白页面或者空转加载状态符,十分影响用户体验感。useTransition结合Suspense应该可以更高效的解决这个问题。 推荐阅读useTransition

  1. Suspense
  2. SuspenseList
  3. useTransition
  4. useDeferredValue

。。。待更新完善

欢迎指正