React v16 小记

Apr 23 2018

总体变化

1. 核心算法重新实现

采用了全新的 Fiber 架构,以支持一系列新特性和对性能的提高。
老架构 Stack Reconciler demo 新架构 Fiber Reconciler demo 的对比。

2. 体积更加小巧

核心文件 react + react-dom 大小相比之前的版本减少了 32%。
(使用 Rollup 作为构建工具;去除了 DOM 属性的白名单列表)
体积变化

3. 更新开源协议

BSD license 改为更宽松的 MIT license,响应社区的呼声。

4. 重写服务器端渲染

支持流( Stream )传输,可以向客户端更快地发送字节,SSR 性能提升了3倍左右。
性能提升

5. 一系列新特性

新特性介绍

Fragments and Strings as new render return types (支持 render 返回 JSX 数组和字符串)

以往 render 返回必须要有根元素,而现在我们可以直接输出数组或者字符串(数字等同字符串)。

代码示例:

Returning arrays

const Li = () => (<li >二娃</li>)

const Members = () => [
  // 别忘了加上 key 属性
  <li key='1'>大娃</li>,
  <Li key='2' />
]

class App extends Component {
  render () {
    // 不需要加上一层多余的根元素了
    return [
      <div key='1'>前端组</div>,
      <ul key='2'>
         <Members />
        <li>三娃</li>
        <li>四娃</li>
      </ul>
    ]
  }
}

Returning string

// 直接返回字符串的组件
const Comment = ({ text }) => text.replace('[微笑]', ':)')

class App extends Component {
  render () {
    return (
      <div>
        <Comment text='任天堂就是世界的主宰,[微笑]' />
      </div>
    )
  }
}
总之,React v16 的 render 函数执行时,可以有以下返回:
  • React elements:正常渲染JSX,原生DOM组件或者自定义高阶组件。
  • Booleans:如果为布尔值则返回 null,例如:return flag && <div />
  • String and numbers:将会被当作文本节点加入DOM中。
  • Arrays:多个 elements 以数组形式返回,如上例代码。
  • Portals:这是一个 React v16 新加入的特性,会在下文详细介绍。
  • null:什么都不渲染。

Error handling using Error boundaries(船新的异常处理机制 组件层面的 try-catch)

React v16 之前的行为

组件内的异常有可能会影响到 React 的内部状态,进而导致下一轮渲染时出现错误。
在之前的版本,React 更多的是交托给开发者自行解决,而没有提供较好的组件层面处理这些异常的方式。

错误边界的介绍

为了解决这个问题,React v16 引入了“错误边界(Error boundaries)”这一新概念,使用了更有弹性的错误处理策略。错误边界作为特殊的 React 组件,会捕获渲染期间,生命周期方法中以及在其整个树的构造函数中的异常。利用它我们便可以上报这些错误,展示备用 UI 而不是让组件树崩溃。

注意:错误边界无法捕获其自身的错误,若错误边界尝试渲染错误信息失败,该错误会向上传递。React v16 默认对于未被错误边界捕获的异常,整个组件树都会被卸载。

代码示例:
class ErrorBoundary extends Component {
  constructor (props) {
    super(props)
    this.state = { hasError: false }
  }

  componentDidCatch (err, info) {
    console.log(err, info)
    this.setState({ hasError: true })
    // 可以在此处上报错误
    // reportError(err, info)
  }

  render () {
    if (this.state.hasError) {
      // 显示异常状态的界面
      // const a = null
      // console.log(a.b)
      return <div>小姐姐丑拒了你!</div>
    }

    return this.props.children
  }
}

// user 为 null 时,该组件会出错
const Invitation = ({ user }) => (<div>{user.name}: 小姐姐,有空一起去看电影吗?</div>)

class App extends Component {
  constructor (props) {
    super(props)
    this.state = {
      user: { name: 'abc' }
    }
  }

  onClick () {
    this.setState({ user: null })
  }

  render () {
    return (
      <div>
        {/* 用正常组件的方式去使用 */}
        <ErrorBoundary>
          <Invitation user={this.state.user} />
        </ErrorBoundary>
        <button onClick={this.onClick.bind(this)}>丑拒</button>
      </div>
    )
  }
}

React v16 之前有类似但不完善的实现 unstable_handleError

class ErrorBoundary extends React.Component {
      ...
      unstable_handleError() {
        reportError()
        this.setState({ error: true })
      }

      render() {
        if (this.state.error){
          return <SafeComponent />
        } else {
          return <ComponentWithErrors />
        }
      }
}

Support for Defining Custom DOM Attributes(支持自定义DOM属性)

// Your code:
<div
  mycustomattribute='something'
  自定义属性='abc'
  class='someclass'
  tabindex='1'
  mycustomattributeobj={{}}
  mycustomattributefn={() => {}}
  onclick='alert(1)'
/>

// React 15 output:
<div
>
  Hello
</div>

// React 16 output:
<div
  mycustomattribute='something'
  自定义属性='abc'
  class='someclass'
  tabindex='1'
  mycustomattributeobj='[object Object]'
>
  Hello
</div>

旧版的 React 会维护一个 DOM 属性的白名单,如果传递的属性不在当中就会被忽略并抛出 warning。React v16 移除了这个白名单,不再忽略未被识别的 HTML 和 SVG 属性,而是将它们的值转换为 String 并传递给 DOM。

注意:React 官方明确表示,数据不应该存放在标签里,而应该用 state 或者 store 去管理,非要自定义属性的话,最好加上 data-* 前缀。

Portals(传送门–以实现一个 Modal 为例)

方案一 最简单的实现,利用 state 控制子元素弹窗的展示

方案一

class App extends Component {
  constructor (props) {
    super(props)
    this.state = { visible: false }
  }

  showModal = () => {
    this.setState({ visible: true })
  }

  closeModal = () => {
    this.setState({ visible: false })
  }

  render () {
    return (
      <div className='app'>
        <div className='hello-top'>
          <h1>Hello top</h1>
          <button onClick={this.showModal}>Show Modal</button>
        </div>
        <div className='hello-bottom'>
          <h1>Hello bottom</h1>
          {this.state.visible && <Modal
            onClose={this.closeModal}
          >
            <h1> Modal title </h1>
            <p> Modal content</p>
          </Modal>}
        </div>
      </div>
    )
  }
}

class Modal extends Component {
  render () {
    return (
      <div className='modal'>
        <span className='modal-close' onClick={this.props.onClose}>
          &times;
        </span>
        {this.props.children}
      </div>
    )
  }

此方案可能受到 z-index 影响,有层叠问题无法完全突出显示,因此需要调整 Modal 在 DOM 中的位置。

方案二 Modal 外移,进行组件间通信

方案二

对 Modal 组件出现的位置进行移动,放在 DOM 结构最外部,使其不受调用位置的层级影响。但这就需要 Modal 组件和触发其出现的深层次组件进行通信,React 组件间通信无外乎 props 或 context,或通过 Redux、Mobx 等工具,这样的做法太过复杂,不利于维护。

方案三 React v16 前常用的方法,借助生命周期动态创建 Modal

方案三

class Modal extends Component {
  constructor (props) {
    super(props)
    this.modalTarget = document.createElement('div')
    document.body.appendChild(this.modalTarget)
  }

  componentDidMount () {
    this.renderModal()
  }

  componentDidUpdate () {
    this.renderModal()
  }

  componentWillUnmount () {
    // 清理DOM防止内存泄露
    ReactDOM.unmountComponentAtNode(this.modalTarget)
    document.body.removeChild(this.modalTarget)
  }

  renderModal () {
    // 第一种
    ReactDOM.render(
      (
        <div className='modal'>
          <span className='modal-close' onClick={this.props.onClose}>
            &times;
          </span>
          {this.props.children}
        </div>
      ),
      this.modalTarget
    )
    // 第二种 现在这个API已经被废除了
    // React.unstable_renderSubtreeIntoContainer(
    //   this, // 这个参数就是用来指定新的 React组件树根节点的父组件,连接Context
    //   (
    //     <div className='modal'>
    //       <span className='modal-close' onClick={this.props.onClose}>
    //         &times;
    //       </span>
    //       {this.props.children}
    //     </div>
    //   ),
    //   this.modalTarget
    // )
  }

  render () {
    return null
  }
}

方案四 React v16 的 Portal API

在 React v16 中,使用 Portal 创建 Modal 简单多了,不需要牵扯到 componentDidMount、componentDidUpdate,也不用手动清理DOM,直接在 render 函数里使用,便能使这个 render 的结果挂载在其他 DOM 节点上。

class Modal extends Component {
  constructor (props) {
    super(props)
    this.modalTarget = document.createElement('div')
    document.body.appendChild(this.modalTarget)
  }

  componentWillUnmount () {
    document.body.removeChild(this.modalTarget)
  }

  render () {
    // React 并不会创建一个新的 div 节点,他只是将它渲染到 modalTarget 这个DOM节点上
    return ReactDOM.createPortal(
      <div className='modal'>
        <span className='modal-close' onClick={this.props.onClose}>
          &times;
        </span>
        {this.props.children}
      </div>,
      this.modalTarget
    )
  }
}

Portals 组件与正常的 React Elements 并没有什么差异。在事件冒泡传递中,Portal 内的事件依然会向上传递到外层,所以无论逻辑上还是写法上 Portals 都是与当前组件是“一体”的,这是其他三种方案都不具有的。

环境依赖

总体来说新的特性能够解决一些日常开发的痛点,开发体验也越来越好了,不过 React v16 因为依赖ES6的一些集合数据结构例如 MapSet 所以,兼容性要求比较高。

如果需要兼容一些老的浏览器(<IE11)需要上 polyfill 比如 core.js 或 babel-polyfill。

import 'core-js/es6/map'
import 'core-js/es6/set'

import React from 'react'
import ReactDOM from 'react-dom'

ReactDOM.render(
  <h1>Hello, world!</h1>,
  document.getElementById('root')
)

另外 React v16 还依赖了 requestAnimationFrame 方法,同样如果浏览器不支持的话我们需要提供一个 shim,例如:

global.requestAnimationFrame = function(callback) {
    setTimeout(callback)
}

参考资料