React 16在2017年9月正式发布带来了很多重要的新特性和改进,比如全新的Fiber架构Error BoundaryPortalFragment支持返回字符串/数字createRefforwardRef新的Context API等等性能也有很大的提升。

React 16是React历史上一个重要的里程碑Fiber架构的引入为未来的异步渲染时间切片等特性打下了基础也让React的性能和用户体验有了很大的提升。

我最近在项目中使用React 16踩了不少坑也积累了一些实战经验今天总结一下React 16的新特性以及,使用过程中的踩坑经验和实战技巧希望能帮大家更好地使用React 16。

一、Fiber架构:React 16的核心

React 16最核心的变化就是引入了全新的Fiber架构这是React核心算法的重写对开发者来说大部分是透明的,但是理解Fiber能帮我们更好地理解React 16的行为和性能特点。

Fiber是什么

Fiber是React新的协调(reconciliation)引擎它把渲染过程拆分成一个个小的任务(Fiber)可以中断暂停恢复优先级调度等等而React 15的渲染过程是同步的一旦开始就不能中断必须执行完复杂的组件树渲染会阻塞主线程导致页面卡顿掉帧。

Fiber的引入让React能实现增量渲染把渲染工作分成多个帧来执行每一帧执行一部分把控制权还给浏览器让浏览器能响应用户交互避免页面卡顿提升用户体验。

Fiber带来的好处

  1. 更好的响应性:渲染过程可以中断不会长时间阻塞主线程用户交互能得到及时响应页面更流畅。
  2. 更好的性能:Fiber能做更好的优化,比如优先级调度复用节点等等整体性能比React 15好。
  3. 为未来特性打基础:Fiber是异步渲染时间切片Suspense等未来特性的基础有了Fiber这些特性才能实现。

踩坑经验

  1. 生命周期函数的变化:React 16因为Fiber的引入有些生命周期函数被标记为不安全(UNSAFE_)比如componentWillMountcomponentWillReceivePropscomponentWillUpdate这些生命周期在异步渲染模式下可能会被调用多次导致问题,所以不推荐使用应该用新的生命周期替代,比如getDerivedStateFromPropsgetSnapshotBeforeUpdate等等。
  2. setState的行为变化:React 16里setState在某些情况下行为有变化,比如在事件处理器外调用setState是同步的(React 15也是这样)但是React 16对setState的批量更新处理有一些变化需要注意。
  3. 不要在render里做副作用:因为Fiber的渲染过程可能会被中断重新执行,所以render函数必须是纯函数不要在render里做副作用(比如修改全局变量订阅事件等等)否则可能会导致问题这在React 15里也是推荐的,但是在React 16里更重要。

二、Error Boundary:错误边界

React 16引入了Error Boundary(错误边界)这是一个非常实用的新特性能捕获组件树中的JavaScript错误避免整个应用崩溃显示降级UI。

什么是Error Boundary

Error Boundary是一种特殊的组件能捕获它的子组件树中任何地方的JavaScript错误记录错误,并且显示降级UI而不是让整个组件树崩溃白屏。

Error Boundary通过两个生命周期来实现:

  • static getDerivedStateFromError(error):在,渲染阶段调用用来更新state显示降级UI。
  • componentDidCatch(error, info):在,提交阶段调用用来记录错误信息上报错误等等。

使用示例

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

  static getDerivedStateFromError(error) {
    return { hasError: true }
  }

  componentDidCatch(error, info) {
    console.error('Error caught by boundary:', error, info)
    // 上报错误到监控系统
    reportError(error, info)
  }

  render() {
    if (this.state.hasError) {
      return <h1>Something went wrong.</h1>
    }
    return this.props.children
  }
}

// 使用
<ErrorBoundary>
  <MyComponent />
</ErrorBoundary>

踩坑经验

  1. Error Boundary只能捕获子组件的错误:Error Boundary只能捕获它的子组件树中的错误不能捕获它自己的错误也不能捕获事件处理器中的错误异步代码的错误(setTimeoutPromise等等)服务端渲染的错误这些错误需要用其他方式处理,比如try/catchwindow.onerrorunhandledrejection等等。
  2. 不要过度使用Error Boundary:Error Boundary不是越多越好过度使用会让错误被隐藏不利于开发调试一般在应用的顶层放一个Error Boundary防止整个应用崩溃,然后在一些重要的组件(比如第三方组件可能出错的地方)放Error Boundary就够了。
  3. 开发环境和生产环境的区别:在开发环境React会把错误栈显示在页面上方便调试Error Boundary的降级UI可能不会显示在,生产环境才会正常显示降级UI所以测试Error Boundary要在生产构建下测试。
  4. getDerivedStateFromError是静态方法:getDerivedStateFromError是静态方法不能访问this只能通过返回值来更新state不要在里面做副作用副作用应该在componentDidCatch里做。

三、Portal:传送门

React 16引入了Portal(传送门)能把组件渲染到DOM树的其他位置而不是当前组件的父容器里这对于弹窗对话框提示框等需要跳出父容器overflow限制的组件非常有用。

什么是Portal

Portal让你能把组件的DOM渲染到指定的DOM节点里而不是当前组件的父容器里,但是组件的其他行为(事件冒泡Context生命周期等等)和正常组件一样就像组件还在原来的位置一样只是DOM被传送到了别的地方。

使用示例

import ReactDOM from 'react-dom'

function Modal({ children, onClose }) {
  return ReactDOM.createPortal(
    <div className="modal-overlay" onClick={onClose}>
      <div className="modal-content" onClick={e => e.stopPropagation()}>
        {children}
        <button onClick={onClose}>Close</button>
      </div>
    </div>,
    document.body // 渲染到body下
  )
}

踩坑经验

  1. 事件冒泡的行为:Portal的DOM虽然渲染到了别的地方,但是事件冒泡还是按照React组件树来冒泡而不是按照DOM树来冒泡这一点很重要也容易让人困惑,比如Portal渲染到document.body但是点击事件还是会冒泡到Portal的父组件而不是body的父元素这是React的设计能让事件处理更符合组件的逻辑,但是要注意这个行为。
  2. 服务端渲染:Portal在服务端渲染(SSR)的时候,需要注意,因为服务端没有document不能直接用document.body需要判断环境,或者用其他方式处理,否则会报错。
  3. CSS和样式:Portal的DOM渲染到了别的地方,所以父容器的CSS样式(比如overflow: hiddenz-indexposition等等)不会影响Portal的组件这正是Portal的用处,但是也要注意Portal的组件的样式需要自己处理,比如z-index定位等等确保显示正常。
  4. 卸载时清理DOM:如果Portal渲染到自己创建的DOM节点(而不是已有,的document.body)需要在组件卸载时清理创建的DOM节点避免内存泄漏。

四、Fragment:碎片

React 16引入了Fragment(碎片)能让组件返回多个元素而不需要包裹一个额外的DOM元素这能减少不必要的DOM层级让DOM结构更简洁。

什么是Fragment

在React 15里组件的render必须返回一个单一的元素,如果你想返回多个元素必须用一个div或者其他元素包裹起来这会增加不必要的DOM层级有时候还会影响样式(比如flex布局table布局等等)。

React 16支持返回数组和Fragment能返回多个元素而不需要包裹额外的DOM。

使用示例

// 方式1:数组(需要key)
render() {
  return [
    <li key="a">First item</li>,
    <li key="b">Second item</li>,
    <li key="c">Third item</li>
  ]
}

// 方式2:Fragment
import { Fragment } from 'react'
render() {
  return (
    <Fragment>
      <li>First item</li>
      <li>Second item</li>
      <li>Third item</li>
    </Fragment>
  )
}

// 方式3:短语法(React 16.2+支持)
render() {
  return (
    <>
      <li>First item</li>
      <li>Second item</li>
      <li>Third item</li>
    </>
  )
}

踩坑经验

  1. 数组需要key:如果用数组的方式返回多个元素每个元素都需要key否则React会警告用Fragment的方式不需要key(除非是在循环里动态生成的)。
  2. 短语法的支持:短语法(<>...</>)是React 16.2才支持的,如果用的是React 16.0或者16.1不支持短语法需要用<Fragment>另外一些旧的工具(BabelESLint等等)可能不支持短语法需要升级工具,或者用Fragment。
  3. Fragment不支持key和ref:Fragment本身不支持key和ref(短语法更不支持)如果需要key(比如在循环里)需要用<Fragment key={...}>而不是短语法ref不能加在Fragment上,因为Fragment不产生DOM节点。
  4. 不要滥用Fragment:Fragment虽然能减少DOM层级,但是也不要滥用,如果包裹一个div不影响样式和布局用div也没问题Fragment主要用在不能有额外DOM的场景,比如table里的tr/tdflex布局的直接子元素等等。

五、支持返回字符串和数字

React 16支持组件的render直接返回字符串和数字不需要包裹在DOM元素里这是一个小,但是实用的特性。

使用示例

// 返回字符串
render() {
  return 'Hello World'
}

// 返回数字
render() {
  return 123
}

// 返回数组包含字符串和数字
render() {
  return ['Hello', 123, 'World']
}

踩坑经验

  1. 返回0的问题:如果render返回0React会渲染0而不是什么都不渲染这和React 15一致,但是有时候会让人困惑,比如{count && <Component />}如果count是0会渲染0而不是什么都不渲染需要注意用{count > 0 && <Component />}来避免。
  2. 返回false/null/undefined:返回falsenullundefinedReact什么都不渲染这和React 15一致。
  3. 返回字符串的样式:返回字符串的话,没有DOM元素不能直接加样式和事件需要包裹在DOM元素里才能加样式和事件,所以,如果需要样式和事件还是需要用DOM元素。

六、createRef和forwardRef

React 16.3引入了createRef和forwardRef新的refAPI替代了旧的字符串ref和,回调ref让ref的使用更简洁和清晰。

createRef

createRef能创建一个ref对象通过ref.current访问引用的DOM节点,或者组件实例。

class MyComponent extends React.Component {
  constructor(props) {
    super(props)
    this.inputRef = React.createRef()
  }

  focusInput = () => {
    this.inputRef.current.focus()
  }

  render() {
    return (
      <div>
        <input ref={this.inputRef} />
        <button onClick={this.focusInput}>Focus</button>
      </div>
    )
  }
}

forwardRef

forwardRef能让函数组件接收ref并转发给内部的DOM元素,或者类组件这对于高阶组件(HOC)和函数组件暴露DOMref非常有用。

const FancyButton = React.forwardRef((props, ref) => (
  <button ref={ref} className="fancy-button">
    {props.children}
  </button>
))

// 使用
const ref = React.createRef()
<FancyButton ref={ref}>Click me</FancyButton>
// ref.current 指向 button DOM元素

踩坑经验

  1. 函数组件不能直接用ref:函数组件没有实例,所以不能直接给函数组件加ref会报错需要用forwardRef来转发ref给内部的DOM元素,或者用useImperativeHandle(Hooks的APIReact 16.8+)来暴露方法。
  2. ref.current可能为null:在组件挂载之前ref.current是null访问ref.current的属性,或者方法会报错需要判断ref.current是否存在,或者在componentDidMount之后,再访问。
  3. 不要过度使用ref:ref能直接访问DOM和组件实例,但是不要过度使用大部分情况用React的声明式方式(stateprops)就能解决,只有在确实需要直接操作DOM(比如focus测量DOM尺寸等等)才用ref。
  4. 旧的字符串ref已废弃:React 16里字符串ref(ref="myRef")已经被标记为废弃不推荐使用应该用createRef或者回调ref替代。

七、新的Context API

React 16.3引入了新的Context API替代了旧的Context API更简洁更易用也更高效是React官方推荐的跨组件层级数据传递方案。

新的Context API

新的Context API用React.createContext创建Context用Provider提供数据用Consumer消费数据,或者用contextType(类组件)或者useContext(HooksReact 16.8+)来消费数据。

// 创建Context
const ThemeContext = React.createContext('light') // 默认值

// Provider提供数据
class App extends React.Component {
  render() {
    return (
      <ThemeContext.Provider value="dark">
        <Toolbar />
      </ThemeContext.Provider>
    )
  }
}

// Consumer消费数据
function Toolbar() {
  return (
    <ThemeContext.Consumer>
      {theme => <div>Theme: {theme}</div>}
    </ThemeContext.Consumer>
  )
}

// 类组件用contextType
class ThemedButton extends React.Component {
  static contextType = ThemeContext
  render() {
    return <button theme={this.context}>Button</button>
  }
}

踩坑经验

  1. Provider的value引用变化导致重渲染:Provider的value如果是对象,或者数组每次渲染都创建新的引用会导致所有Consumer都重新渲染,即使数据没有变化性能差,所以value应该存在state里,或者用useMemo(Hooks)缓存避免不必要的重渲染。
  2. Context的默认值:createContext的参数是默认值,只有当组件没有匹配的Provider时才用默认值不是Provider的value为undefined时用默认值这一点容易搞混要注意。
  3. 不要用Context传递频繁变化的数据:Context适合传递不频繁变化的全局数据,比如主题用户信息语言等等,如果数据变化频繁(比如表单输入实时数据)用Context会导致大量重渲染性能差应该用ReduxMobX或者组件局部state来管理。
  4. 多个Context嵌套:如果有多个Context会嵌套很多层Provider和Consumer代码比较乱可以考虑合并相关的Context或者用状态管理库(Redux等等)来管理复杂的全局状态。
  5. 旧的Context API已废弃:React 16.3之前,的旧Context API(contextTypeschildContextTypes)已经被标记为废弃不推荐使用应该用新的Context API替代。

八、setState返回null取消更新

React 16支持setState的回调返回null来取消状态更新避免不必要的重渲染这是一个性能优化的小特性。

使用示例

handleChange = (newValue) => {
  this.setState(prevState => {
    if (prevState.value === newValue) {
      return null // 取消更新不重渲染
    }
    return { value: newValue }
  })
}

踩坑经验

  1. 只在函数式setState里有效:返回null取消更新只在函数式setState(setState(prevState => ...))里有效,如果是对象式setState(setState({...}))不能返回null来取消。
  2. 不要过度使用:这个特性主要用在需要判断是否需要更新的场景一般情况React自己会做优化(比如PureComponentshouldComponentUpdate)不需要手动返回null来取消不要过度使用让代码变复杂。

九、性能优化建议

React 16虽然性能比React 15好,但是还是需要做一些性能优化才能达到最佳体验。

性能优化建议

  1. 用PureComponent和React.memo:类组件用PureComponent函数组件用React.memo(React 16.6+)做浅比较避免不必要的重渲染。
  2. 合理使用key:列表渲染用稳定的key不要用index作为key避免不必要的DOM操作和组件状态错乱。
  3. 代码分割:用React.lazy和Suspense(React 16.6+)或者,loadable-components做代码分割按需加载减少首屏加载体积。
  4. 避免在render里创建新对象和函数:render里不要创建新的对象数组函数(比如内联箭头函数内联对象)否则每次渲染都创建新的引用导致子组件浅比较失败重渲染应该把函数和对象提到外面,或者用useCallbackuseMemo(Hooks)缓存。
  5. 虚拟列表:长列表用虚拟滚动(react-windowreact-virtualized)只渲染可视区域的项减少DOM节点提升性能。
  6. 合理使用Context:Context不要传递频繁变化的数据避免大量重渲染。
  7. 生产构建:生产环境用生产构建(NODE_ENV=production)开启代码压缩移除开发环境的警告和检查提升性能。

十、写在最后

以上就是我对React 16新特性的总结以及使用过程中的踩坑经验和实战技巧包括Fiber架构Error BoundaryPortalFragment字符串/数字返回createRef/forwardRef新的Context APIsetState返回null等等新特性以及性能优化建议。

React 16是一个伟大的版本Fiber架构的引入为React的未来打下了坚实的基础也带来了很多实用的新特性提升了开发体验和,用户体验现在React 16已经非常成熟稳定推荐大家升级到React 16享受新特性带来的便利。

希望我的总结和踩坑经验能帮大家更好地使用React 16少走弯路写出更高效更优雅的React代码。

最后用一句话结束这篇文章:"React 16不是终点而是新的起点Fiber的引入让React的未来充满可能让我们一起期待React的更多精彩。"

愿大家都能用好React 16写出优秀的前端应用。