zixinfeng

2018-06-04 14:02

React生命周期简单分析

本文作者:IMWeb zixinfeng 原文出处:IMWeb社区 未经同意,禁止转载

关于React16.3以及旧版生命周期的探讨

1.React16.3的发布带来了一些新的特性, 除了新的ContextAPI之外, 还对生命周期做了部分修改, 为了支持未来的异步渲染特性, 一下生命周期将被废弃

  • componentWillMount 请使用 componentDidMount代替
  • componentWillUpdate 请使用 componentDidUpdate代替
  • componentWillReceiveProps 请使用新增的 static getDerivedStateFromProps代替

2.废弃警告会在React16.4开启, 废弃的函数预计在React 17.0移除

旧版生命周期

1.App.jsx

state = {
  name: 'fff',
  age: 18
}
handleNameClick() {
  this.setState({name: 'zzz'});
}
render() {
  return (
    <div>
      <Button onClick={this.handleNameClick.bind(this)}>点击修改Name属性</Button>

      <Children name={this.state.name} age={this.state.age}/>
    </div>
  )
}

2.Children.jsx

render() {
  return (
    <div}>
      Name: {this.props.name}
      Age: {this.props.age}
    </div>
  )
}

3.项目启动的时候生命周期

  • App constructor(props) - Parent
  • APP componentsWillMount - Parent
  • App render() - Parent
  • Child constructor(props)
  • Child componentsWillMount()
  • Child render()
  • Child componentDidMount()
  • APP componentDidMount()

4.点击父元素App中的按钮, 修改state中的name属性值

  • APP shouldComponentUpdate(nextProps, nextState)
  • APP componentWillUpdate(nextProps, nextState)
  • App render()
  • Child componentWillReceiveProps(nextProps)
  • Child shouldComponentUpdate(nextProps, nextState)
  • Child componentWillUpdate()
  • Child render()
  • Child componentDidUpdate(prevProps, prevState)
  • APP componentDidUpdate(prevProps, prevState)

5.在父元素中shouldComponentUpdate中添加对age变化判断

1.App.jsx

shouldComponentUpdate(nextProps,nextState) {
      //如果更改之后的age和之前一样 返回false, 表示不重新渲染
      if (nextState.age === this.state.age) {
        return false;
      }
      return true;
}
onAgeChange() {
      this.setState({age: 18});
}
render() {
      return (
        <div>
          <Button onClick={this.handleNameClick.bind(this)}>点击修改Name属性</Button>
          <Children name={this.state.name} age={this.state.age} onAgeChange={this.onAgeChange}/>
        </div>
      )
}

2.Children.jsx

    render() {
      return (
        <div>
          Name: {this.props.name}
          Age: {this.props.age}
          <button onClick={this.props.onAgeChange}>点击修改Age (not change age): {age}</button>
        </div>
      )
    }

2.1 在Children组件中, 点击按钮, 调用父元素中的的onAgeChange函数, 但是在父元素中这里我们setState的修改后的age和修改之前prevState中age状态值是一样的,age都是18, 所以App的shouldComponentUpdate中返回false, 表示我们是不需要重新渲染的, 因为state中age并没有改变; 在上述情况下调用的生命周期如下

  • APP shouldComponentUpdate(nextProps, nextState)
  • 生命周期中只会调用App的shouldComponent, 其余的生命周期全部不会调用, 包括子元素生命周期
    2.2 合理利用shouldComponent我们可以减少不必要的渲染

ComponentWillMount

1.服务器端和客户端都只调用一次,在初始化渲染执行之前立刻调用. 如果在这个方法内调用 setState,render() 将会感知到更新后的 state,将会执行仅一次,尽管 state 改变了. 在服务端渲染时 componentDidMount 是不会被调用的,只会调用componentWillMount.

2.在componentWillMount中, 我们一般会用来异步获取数据, 但是在新版生命周期中, 官方不推荐我们这么做, 也不建议我们在constructor中, 有以下两点原因

  • 会阻碍组件的实例化,阻碍组件的渲染
  • 如果用setState,在componentWillMount里面触发setState不会重新渲染

3.官方推荐我们使用componentDidMount, 选择在componentDidMount有几个原因:

  • componentDidMount指的是第一次插入dom完毕,无论在同步和异步模式下都仅会触发一次
  • 在目前16.3之前的react版本中 ,react是同步渲染的, 在componentWillMount中接口调用,有可能不会触发界面渲染,而在componentDidMount中渲染一定会触发界面渲染,具体可以看这个issue
  • 在16.3之后react开始异步渲染,在异步渲染模式下,使用componentWillMount会被多次调用,并且存在内存泄漏等问题
  • 关于在componentWillMount比componentDidMount请求早,具体应该是componentWillMount会立即执行,执行完之后会立即进行render
  • 在componentDidMount 被调用后,componentWillUnmount 一定会随后被调用到, 所以我们在componentDidMount里面注册的事件订阅都可以在这里取消掉, 而componentWillMount被调用并不能保证componentWillUnmount一定随后被调用

4.componentDidMount

  • 这个方法在组件被mount后被立即调用. 如果需要从远端加载数据的话, 推荐在这个方法中初始化
  • 由于这个方法发生初始化挂载render方法之后, 因此在这个方法中调用setState()会导致一次额外的渲染, 只不过这次渲染会发生在浏览器更新屏幕之前. 因此即使渲染了两次, 用户也不会看到中间状态, 即不会有那种状态突然跳一下的情况发生. 只不过, 虽然在用户视觉体验上可能没有影响, 但是这种操作可能会导致性能方面的问题, 因此还需慎用.
    // App.js
    constructor(props) {
      super(props)
      this.state({
        name: 'ccc'
      })
    }
    componentDidMount() {
      this.setState({name: 'fff'})
    }
    render() {
      return (
        <Child name={this.state.name} />
      )
    }

    // Child.js
    render() {
      <div>
        {this.props.name}
      </div>
    }

5.上面代码生命周期调用过程

  • App constructor
  • APP componentsWillMount
  • App render
  • Child constructor
  • Child componentsWillMount
  • Child render
  • Child componentDidMount
  • APP componentDidMount (这里更新了state, 后续都是update的操作)
  • APP shouldComponentUpdate
  • APP componentWillUpdate
  • App render
  • Child componentWillReceiveProps
  • Child shouldComponentUpdate
  • Child render
  • Child componentDidUpdate
  • APP componentDidUpdate - Parent

componentWillUpdate(nextProps,nextState)

1.在接收到新的 props 或者 state 之前立刻调用. 在初始化渲染的时候该方法不会被调用, 在render方法之前. 使用该方法做一些更新之前的准备工作, 例如读取当前某个 DOM 元素的状态并在componentDidUpdate中进行处理. 该生命周期有可能在一次更新中被调用多次, 也就是说写在这里的回调函数也有可能会被调用多次, 这显然是不可取的. 所以将原先写在 componentWillUpdate 中的回调迁移至 componentDidUpdate 就可以解决这个问题 2.同时注意:你不能在componentWillUpdate方法中使用 this.setState(). 如果需要更新 state 来响应某个prop的改变, 请使用getDerivedStateFromProps

3.关于在组件更新之前读取DOM元素的状态, React 提供了一个新的生命周期函数getSnapshotBeforeUpdate(prevProps, prevState), 具体我们可以看官方提供的例子

class ScrollingList extends React.Component {
  listRef = null;

  getSnapshotBeforeUpdate(prevProps, prevState) {
    // Are we adding new items to the list?
    // Capture the scroll position so we can adjust scroll later.
    if (prevProps.list.length < this.props.list.length) {
      return (
        this.listRef.scrollHeight - this.listRef.scrollTop
      );
    }
    return null;
  }

  componentDidUpdate(prevProps, prevState, snapshot) {
    // If we have a snapshot value, we've just added new items.
    // Adjust scroll so these new items don't push the old ones out of view.
    // (snapshot here is the value returned from getSnapshotBeforeUpdate)
    if (snapshot !== null) {
      this.listRef.scrollTop =
        this.listRef.scrollHeight - snapshot;
    }
  }

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

  setListRef = ref => {
    this.listRef = ref;
  };
}

与componentWillUpdate 不同, getSnapshotBeforeUpdate 会在最终的 render 之前被调用, 也就是说在 getSnapshotBeforeUpdate 中读取到的 DOM 元素状态是可以保证与 componentDidUpdate 中一致的. 虽然 getSnapshotBeforeUpdate 不是一个静态方法, 但我们也应该尽量使用它去返回一个值. 这个值会随后被传入到 componentDidUpdate 中, 然后我们就可以在 componentDidUpdate 中去更新组件的状态, 而不是在 getSnapshotBeforeUpdate 中直接更新组件状态.

4.针对项目修改方案

  • 将现有的 componentWillUpdate 中的回调函数迁移至 componentDidUpdate.
  • 如果触发某些回调函数时需要用到 DOM 元素的状态,则将对比或计算的过程迁移至 getSnapshotBeforeUpdate,然后在 componentDidUpdate 中统一触发回调或更新状态.

componentWillReceiveProps(nextProps)

1.在旧版的 React 中,如果组件自身的某个 state 跟其 props 密切相关的话,一直都没有一种很优雅的处理方式去更新 state,而是需要在 componentWillReceiveProps 中判断前后两个 props 是否相同,如果不同再将新的 props 更新到相应的 state 上去. 这样做一来会破坏 state 数据的单一数据源,导致组件状态变得不可预测,也会增加组件的重绘次数.

// App.jsx
// 在其中添加上static getDerivedStateFromProps()
static getDerivedStateFromProps(extProps, prevState) {

} 

// ChildComponent.jsx
constructor(props) {
  super(props);
  this.state = {
      age: props.age
  }
}
// Before
componentWillReceiveProps(nextProps) {  
  if (nextProps.age !== this.props.age) {
    this.setState({ 
        age: nextProps.age, 
    }); 
  } 
}

// After
static getDerivedStateFromProps(extProps, prevState) {
  if (nextProps.age !== prevState.age) {
      return {age}
  } 
}

2.旧版本的组件实力化的时候的生命周期

  • App constructor
  • APP componentsWillMount
  • App render
  • Child constructor
  • Child componentsWillMount
  • Child render
  • Child componentDidMount
  • APP componentDidMount - Parent

3.分别给App.jsx和Child.jsx中添加上static getDerivedStateFromProps方法后

  • App constructor
  • APP getDerivedStateFromProps
  • App render
  • Child constructor
  • getDerivedStateFromProps
  • Child render
  • Child componentDidMount
  • APP componentDidMount

2.简单分析static getDerivedStateFromProps
2.1 这个方法将会在组件实例化和接收到新的 props 的时候被调用. 而componentWillReceiveProps只会在接收到新的props的时候才会调用
2.1.1 当组件实例化的时候,这个方法替代了componentWillMount(),而当接收到新的 props 时,该方法替代了 componentWillReceiveProps() 和 componentWillUpdate().

2.1.2 需要注意,这个方法是个 static 的方法,因此使用 this 在这个方法中并不指代本实例组件,如果打印出来会发现这个this在这个方法中是null. 而且这个方法会返回值. 当需要更新状态时,需要返回一个 object ,如果不需要任何更新,则返回null即可.

2.1.3 如果由于父组件的原因导致该组件重新渲染,这个方法也会被调用, 如果只想要处理更新的话,最好加上判断条件if (nextProp !== prevProp). 另外,虽然this.setState()也会导致组件重新渲染,但并不会导致这个方法的重新调用.

  1. 针对componentWillReceiveProps的改造
  2. 将现有 componentWillReceiveProps 中的代码根据更新 state 或回调,分别在 getDerivedStateFromProps 及 componentDidUpdate 中进行相应的重写即可,注意新老生命周期函数中 prevProps,this.props,nextProps,prevState,this.state 的不同.
  • 将componentWillReceiveProps中放在实例变量上的属性放在state中, 比如
    state = {
      name: 0
    }
    // old
    componentWillReceiveProps(nextProps) {
      if (nextProps.name !== this.props.name) {
        if (this.state.loginStatus === null) {
          fakeServerRequest(nextProps.name).then(result => {
            // 实例属性 没有挂载在state中
            this.loginStatus = result;
          });
        }
        this.setState({
          name: nextProps.name
        })
      }
    }

    // new
    state = {
      loginStatus: null,
      name: 0
    }
    static getDerivedStateFromProps(nextProps, prevState) {
      if (nextProps.name !== prevState.name) {
         return {
          name: nextProps.name
          loginStatus
        }
      }
      return null;
    }

    // 将以前放在componenWillReceiveProps中的异步网络请求放在DidUpdate中
    componentDidUpdate(nextProps) {
      const { name } = nextProps;
      if (this.state.loginStatus === null) {
        fakeServerRequest(name).then(result => {
          this.setState({ loginStatus: result });
        });
      }
    }
  • 如果在项目中使用static getDerivedStateFromProps, 代表的就是使用react16.3新的生命周期, 如果不小心同时将componentWillReceiveProps也添加上了, 那么控制台就会给出错误警告

正式版的context API

1 context这个特性已经存在很久了,但因为一些原因一直是处于试验性质的API. React 16.3带来了正式版的context API

2.使用context

import React, { createContext } from 'react';

const ctx = createContext({
  msg: 'hello world!',
});
const { Provider, Consumer } = ctx;

2.1 Provider组件用于将context数据传给该组件树下的所有组件 value属性是context的内容
2.2 要使用context的数据,我们需要使用Consumer组件

  // 数据提供者
  class App extends React.Component {
    render() {
      return (
        <div>
          <Provider value={{ msg: 'hello react!' }}>
            <ChildComponent1 />
            <ChildComponent2 />
          </Provider>
          <ChildComponent3 />
        </div>
      );
    }
  }
  // 数据消费者
  // 函数式
  const ChildComponent1 = () => (
    <Consumer>
      {context => <p>{context.msg}</p>}
    </Consumer>
  );
  // 类
  class ChildComponent2 extends React.Component {
    render() {
      return (
        <Consumer>
          {context => <p>{context.msg}</p>}
        </Consumer>
      );
    }
  }
  // 类
  class ChildComponent3 extends React.Component {
    render() {
      return (
        <Consumer>
          {context => <p>{context.msg}</p>}
        </Consumer>
      );
    }
  }
  /*
  Consumer下不能写其它的东西,比如<Consumer>Message:{context => <p>{context.msg}</p>}</Consumer>  只能是一个函数 返回需要渲染的组件
  */

3.既然context的内容是写在Provider的value中,如果没有将Consumer作为Provider的子组件, 如上面的ChildComponent3,那么Consumer将使用创建context时的参数作为context

4.说一说新版 Context API 的几个特点:
4.1. Provider 和 Consumer 必须来自同一次 React.createContext 调用。也就是说 NameContext.Provider 和 AgeContext.Consumer 是无法搭配使用的。
4.2. React.createContext 方法接收一个默认值作为参数。当 Consumer 外层没有对应的 Provider 时就会使用该默认值。
4.3. Provider 组件的 value prop 值发生变更时,其内部组件树中对应的 Consumer 组件会接收到新值并重新执行 children 函数。此过程不受 shouldComponentUpdete 方法的影响。

      // ParentComponent.jsx
      state = {
        msg: 'first'
      }
      changeState() {
        this.setState({
          msg: this.state.msg + '%-_-%'
        })
      }
      render() {
        return (
          <div>
            <button onClick={this.changeState.bind(this)}></button>
            <ParentContext.Provider value={this.state.msg}>
              <Children />
            </ParentContext.Provider>
          </div>
        )
      }

      // ChildrenComponent.jsx
      render() {
        <ParentContext.Consumer>
          <(msg)=> <p>{msg}</p>
        </ParentContext.Consumer>
      }

4.4.Provider 组件利用 Object.is 检测 value prop 的值是否有更新。注意 Object.is 和 === 的行为不完全相同。

4.5.Consumer 组件接收一个函数作为 children prop 并利用该函数的返回值生成组件树的模式被称为 Render Props 模式。

小结

  1. 从整体的角度再来看一下 React 这次生命周期函数调整前后的异同, 以上的这些生命周期函数的改动, 一直要到 React 17.0 中才会实装, 这给广大的 React 开发者们预留了充足的时间去适应这次改动。
  2. 相信在 React 正式开启异步渲染模式之后, 许多常用组件的性能将很有可能迎来一次整体的提升。进一步来说, 配合异步渲染, 许多现在的复杂组件都可以被处理得更加优雅, 在代码层面得到更精细粒度上的控制, 并最终为用户带来更加直观的使用体验。
  3. 旧版生命周期
  4. 新版生命周期
2条评论

    您需要 注册 一个IMWeb账号或者 才能进行评论。