重识React v16

核心算法(React Fiber)的一次重新实现,对使用者升级是无感知的。

生命周期的变更

废弃3个生命周期(componentWillMount、componentWillUpdate、componentWillReceiveProps),取而代之的是新增的2个周期:

  • static getDerivedStateFromProps
  • getSnapshotBeforeUpdate

getDerivedStateFromProps

替代 componentWillReceiveProps 的方案,让我们快速看看新旧间的写法差异。

  • componentWillReceiveProps
import React from 'react'
class Button extends React.Component {
  state = {color: this.props.color}
  componentWillReceiveProps(nextProps, nextContext) {
    if(nextProps.color !== this.state.color) this.setState({value: nextProps.color}) 
  }
}
  • static getDerivedStateFromProps
import React from 'react'
class Button extends React.Component {
  state = {color: this.props.color}
  static getDerivedStateFromProps(nextProps, prevState) {
    if(nextProps.color !== prevState.color) {
      return {color: nextProps.color}
    }
    return null
  }
}

二者之间的差异:

  • getDerivedStateFromProps 是静态方法,无法直接访问访问 this.statethis.props
  • getDerivedStateFromProps 接受的第二个参数改为 prevState 而非 nextContext
  • getDerivedStateFromProps 总是需要返回值,返回 null 表示不更新,而 componentWillReceiveProps 需要调用setState去更新视图数据

试一试

getSnapshotBeforeUpdate

替代componentWillUpdate 的方案,此函数会在 render 后和提交给 DOM 前调用,接收两个参数 prevProps, prevState

import React from 'react'
// list` 条目增加,渲染的 `li` 也增加。但是滚动条位置不变
class ScrollingList extends React.Component {
  listRef = React.createRef();

  getSnapshotBeforeUpdate(prevProps, prevState) {
    // 是否有新增的 list,存在就返回 list 的高度,以便我们稍后在 componentDidUpdate 调整滚动
    if (prevProps.list.length < this.props.list.length) {
      return this.listRef.current.scrollHeight;
    }
    return null;
  }

     // snapshot 接受 getSnapshotBeforeUpdate 的返回值
  componentDidUpdate(prevProps, prevState, snapshot) {
    // scrollTop 增加新的 scrollHeight 和原来 scrollHeight 的差值,以保持滚动条位置不变
    if (snapshot !== null) {
      this.listRef.current.scrollTop +=
       this.listRef.current.scrollHeight - snapshot;
    }
  }

  render() {
    return <div ref={this.listRef}>{/* ...contents... */}</div>
  }
}

试一试

React.Fragment

React 中一个常见模式是为一个组件返回多个元素。Fragment 可以让你聚合一个子元素列表,并且不在DOM中增加额外节点。

import React from 'react'
function App() {
  return (<React.Fragment>test</React.Fragment>) // "test"
}

语法糖写法:

import React from 'react'
function App() {
  return (<>test</>) // "test"
}

试一试

React.StrictMode

开发环境下使用不推荐写法和即将废弃的 API(该版本废弃了三个生命周期钩子)将抛出警告。与 Fragment 相同,并且不在DOM中增加额外节点。

import React from 'react'
function App() {
  return (<React.StrictMode>test</React.StrictMode>) // "test"
}

试一试

React.createPortal

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

import React from 'react'
import { render, createPortal } from 'react-dom'

function Toast () {
  return createPortal(
    <div style={{ width: '200px', height: '200px', background: 'red' }} />,
    document.body)
}

class App extends React.Component {
  render () {
    return <div style={{width: '100px', height: '100px', overflow: 'hidden'}}>
      <Toast />
    </div>
  }
}

render(
  <App />,
  document.getElementById('app')
)

第一个参数(child)任何可渲染的 React 子元素。第二个参数(container)则是一个 DOM 元素。试一试

forceUpdate

调用forceUpdate()将会导致组件的 render()方法被调用,并忽略shouldComponentUpdate()。

试一试

React的refs

2个核心API:

  • React.createRef
  • React.forwardRef

React.createRef,新的获取ref的方式,弥补字符串和回调函数形式的不足。

import React from 'react'

class Button extends React.Component {
  showButton() {
    alert(1)
  }
  render() {
    const { children, ...resetProps } = this.props
    return <button {...resetProps}>{children}</button>
  }
}

class App extends React.Component {
  refButton = React.createRef()
  render() {
    return (
      <div>

        <Button // 字符串模式
          ref="refButton1"
          onClick={() => this.refs.refButton1.showButton()}
        >
          字符串模式
        </Button>

        <Button // 函数模式
          ref={e => (this.refButton2 = e)}
          onClick={() => this.refButton2.showButton()}
        >
          函数模式
        </Button>

        <Button // React.createRef 模式
          ref={this.refButton}
          onClick={() => this.refButton.current.showButton()}
        >
          React.createRef 模式
        </Button>
      </div>
    )
  }
}

试一试。关于它们间的存在的问题,可以参考文章<浅谈 React Refs>

React.forwardRef主要场景是在高阶函数的运用上,将ref向被包装组件递传。

import React from 'react'

class Button extends React.Component {
  render() {
    return <button>{this.props.children}</button>
  }
}

function WrappedComponent (Component) {
  // TODO
  return React.forwardRef((props, ref) => <Component {...props} ref={ref} />)
}

const NewButton = WrappedComponent(Button)
class App extends React.Component {
  refButton = React.createRef()

  componentDidMount () {
    console.log(this.refButton.current) // Button实例
  }

  render () {
    return <NewButton ref={this.refButton}>test</NewButton>
  }
}

参考文章: forwarding-refs试一试

Context

Context 通过组件树提供了一个传递数据的方法,从而避免了在每一个层级手动的传递 props 属性。
三个核心API:

  • React.createContext
  • Provider
  • Consumer
import React from 'react'
const AppContext = React.createContext(initData)
// 提供者
class App extends React.Component {
  static contextType = AppContext
  render() {
    return <AppContext.Provider value={this.context}>
        <Button>test</Button>
    </AppContext.Provider>
  }
}

// 消费者
function Button() {
  return (<AppContext.Consumer>
      {context => <button {...context} />}
  </AppContext.Consumer>)
}

待续…

由于篇幅问题,将把 React Hooks 放在下篇文章介绍。