React 16 新特性使用总结

2,508 阅读6分钟

从React 16.0开始,React引入了以下新的特性(组件、API、生命周期函数、hook等)。从15.X升级到16.X是一种增强式的升级,之前的用法依然有效,基本可以直接将npm包进行平滑升级。但是随着React 16.5的发布,新特性基本已经完善和固定下来,为了享受新版本带来的更高效率架构和更规范的开发流程,现在是时候使用上React ^16.0 的新特性进行功能开发和代码重构。 我直接使用了React 16.5,具体变更请查看React 更新日志,以下是几个常用的新特性,每个特性都从与之前的比较和实例来说明:

一、生命周期函数

生命周期函数的改动有两个方面

  • 新增: static getDerivedStateFromProps(nextProps, prevState)
    getSnapshotBeforeUpdate(prevProps, prevState)
    componentDidCatch(error, info)
  • 不安全: UNSAFE_componentWillMount(nextProps, nextState)
    UNSAFE_componentWillReceiveProps(nextProps)
    UNSAFE_componentWillUpdate(nextProps, nextState)

在16.X版本中依然可以使用不安全的周期函数(UNSAFE_前缀可加可不加),用法也和之前的一样。但是如果使用了新增的周期函数就不能同时使用不安全的周期函数,否则会报错并且不安全的周期函数不会被执行。
使用新增的周期函数的执行顺序还是按照阶段来说明:

  1. 装载阶段
    constructor -> getDerivedStateFromProps -> render -> componentDidMount
  2. 更新阶段
    getDerivedStateFromProps -> shouldComponentUpdate -> render -> getSnapshotBeforeUpdate -> componentDidUpdate
  3. 卸载阶段
    componenWillUnMount
  • 新增的生命周期函数
  1. getDerivedStateFromProps周期函数的作用是根据props初始化或更新state,相当于绑定了props。有两个参数分别是nextProps(本次渲染的props)和prevState(当前组件的state),通过对比nextProps和prevState对应字段的值是否相同从而获知外界传入的props是否改变,然后返回更改后的state。

注意:关于更新阶段在16.4后的getDerivedStateFromProps作用于所有更新方式(自身state变化,传入props变化,自身forceupdate, 父组件引发的re-render) 这意味着我们的组件里state接收了从外部传递的props初始化后就不能通过this.setState方法修改自己,例如下面例子会导致state不能内部更新与 15.x对比:

static getDerivedStateFromProps(nextProps, preState) {
    return {
      info: nextProps.info, //state.info绑定了props.info
      text: preState.text // state.text由组件自己管理
    }
  }
  handleChange = () => {
    this.setState({
      info: 'change', //组件内setState无法更新state.info
      text: 'new text' // 可以正常更新
    })
  }
React 15.X 
class Parent extends Component{ //父组件
    render(){
        return (
        <Child age={18} />
        )
    }
}
class Child extends Components{ //子组件
    constructor(props){
        super(props);
        this.state = {
            age: this.props.age //装载阶段初始化state
        }
    }
    componentWillReceiveProps(nextProps){
        if(nextProps.age !== this.state.age){
            this.setState({
                age: nextProps.age
            }) // props.age改变时修改state.age
        }
    }
    handleChange = () => {
      this.setState({
        age: 20 // 可以正常更新
      })
    }
    render(){...}
} 

刚开始使用getDerivedStateFromProps可能会觉得反而比之前的使用场景少了,但其实我们将props映射到state的场景是很少的,多数情况下界面可以直接使用props的值,只有当需要对props进行计算或处理再使用的情况下才需要放进state来使用,而这时的state也无需通过组件内部去更改。

  1. getSnapshotBeforeUpdate作用是替代componentWillUpdate,有两个参数分别是prevProps和prevState。它放在了render函数后、componentDidUpdate之前执行,此时的prevProps和prevState已经是更新后的props和state,但dom结构还没渲染,因此可以获取到dom节点的信息返回,componentDidUpdate中可以拿到它的返回值,然后获取最新dom节点信息与之进行对比,从而实现某些功能。
列表项数目改变时scroll到最后一条
 getSnapshotBeforeUpdate(prevProps, prevState) {
    if (prevProps.list.length !== this.props.list.length) {
      const list = this.listRef.current //dom渲染前的列表元素ref
      return list.scrollHeight - list.scrollTop
    }
    return null
  }
​
  componentDidUpdate(prevProps, prevState, snapshot) {
    // snapshot 是getSnapshotBeforeUpdate返回的偏移量
    if (snapshot !== null) {
      const list = this.listRef.current //dom渲染后的列表元素ref
      list.scrollTop = list.scrollHeight - snapshot // 滚动到之前的位置
    }
  }
  1. componentDidCatch React组件发生错误时的处理逻辑,调用第三方的错误监听服务。用法是在上级组件中定义componentDidCatch,他有error和info 两个参数,可以在函数中处理错误.
class PotentialError extends React.Component {   //定义错误处理组件
  constructor(props) {     
    super(props);     
    this.state = { error: false };
  }
  componentDidCatch(error, info) {     
    this.setState({ error, info });
  }
  render() {
    if (this.state.error) {
      return <h1>Error: {this.state.error.toString()}</h1>;
    }
    return this.props.children;   
  } 
}
<PotentialError><App /></PotentialError> // 包裹应用

二、Context

使用Context 主要是解决props向多层嵌套的子组件传递的问题,原理是定义了一个全局对象。新的Context的使用方法跟旧的有很大差别

// React 15.x需要自己开发定义提供 Context的父组件,然后用这个父组件包裹嵌套多层的子组件。
class Provider extends Component {
    getChildContext(){
        return {
            age: 18
        }
    }
    render() {
        return this.props.children;
    }
}
Provider.childContextTypes = {
    age: PropTypes.number
};
// 子组件
class Child extends Component {
    constructor(props,context) { 
        super(props,context); //super 带上context 参数
    }
    render(){
        const {age} = this.context; //使用this.context访问
        ...
    }
}
Child.contextTypes = {
    age: PropTypes.number
};
​
// React 16.5 使用 React.createContext()
const MyContext = React.createContext();
// 使用 MyContext.Provider 包裹子组件
<MyContext.Provider value={{age: 18}}>
    <Child />
</MyContext.Provider>
//子组件使用 MyContext.Consumer获得 Context
class Child extends Component {
    render(){
       return (
       <MyContext.Consumer>
            {(context) => {
                const {age} = context;
                return ...
            }}
       </MyContext.Consumer>
       )
    }
}

三、Portal 和 Refs

Portal 是React 16 的全新特性,可以将组件渲染到非根节点其他 DOM 节点上,常用的场景是实现弹框组件的改造

用法是使用ReactDOM.createPortal(component, domNode) component是要渲染的组件,domNode是挂载的dom节点。

React 16 使用React.createRef 创建Ref对象,当组件通过ref属性绑定这个对象后就能通过this.myRef.current来获取dom节点或react组件实例,也可配合React.forwardRef将子组件的ref暴露给父组件,以下是使用的区别:

// React 15.X
class Parent extends Component {
    getChildRef= (ref) => {
        this.myRef = ref;
    }
    render() {
        <Child childRef={this.getChildRef} />
    }
}
class Child extends Component {
  constructor(props) {
    super(props)
  }
​
  render() {
    return <div ref={this.props.childRef} />
  }
}
​
// React 16.5
class Parent extends Component {
  constructor(props) {
    super(props)
    this.myRef = React.createRef()
  }
  render() {
      return <Child ref={this.myRef} />
  }
}
​
const Child = React.forwardRef((props, ref) =>  <div ref={ref} />);

四、React Hooks

react hook 是 react 16.8后新增的功能,它能够让函数组件管理自己的状态和模拟出生命周期,并提供几个组件优化的方法。

在之前函数组件没有生命周期和状态,只能由父组件更新或外界传入props更新触发重新渲染,由于完全依赖props因此作用局限于实现功能单一的无状态的展示组件。有了hook之后完全可以替代传统class组件的写法,提高代码可读性和质量。不足就是之前已经养成了面向对象的开发思想需要改变,但这是不强制的,react没有废除class组件的计划。

  • useState:
const [num, setNum] = useState(0);
setNum(1);

初始值只绑定一次,运行setXX方法后会重新执行、渲染。

  • useEffect:

useEffect可执行副作用操作,模拟了生命周期

useEffect(() => {
}) // 加载和每次更新完都执行,相当于componentDidUpdate, componentDidMount

useEffect(() => {
},[]) // 加载阶段执行 ,相当于componentDidMount

useEffect(() => {
},[datas]) // 加载执行一次, 更新时当datas改变才执行

useEffect(() => {
  return () => {
  }
},[]) -> 卸载时执行return的函数
  • useRef:

获取组件实例或dom元素

function Home() {
  const inputEl = useRef(null);  
  inputEl.current.focus();
  
  return (
    <>
      <input ref={inputEl} type="text" />
    </>
  );
}

useMemo:

缓存计算结果或对象

const data = useMemo(()=>{        
  return {name}    
},[name]) // 当name改变时data才改变

useCallback:

缓存函数,避免每次更新都重新定义函数

const onChange = useCallback((e)=>{        
  setText(e.target.value())   
},[]) // 有依赖时,将依赖项填入数组中

以上就是React 16 新特性的简单介绍,实际上前端开发中经常遇到新技术出现或者技术升级的情况,我们应该紧跟技术进步的步伐,从文档入手,再结合开源项目和最佳实践进行练手,相信很快就能掌握它们。