一.对componentWillReceiveProps的误解
componentWillReceiveProps通常被认为是propsWillChange,我们确实也通过它来判断props change。但实际上,componentWillReceiveProps在每次rerender时都会调用,无论props变了没:

class A extends React.Component {  render() {    return <div>Hello {this.props.name}</div>;  }  componentWillReceiveProps(nextProps) {    console.log('Running A.componentWillReceiveProps()');  }}class B extends React.Component {  constructor() {    super();    this.state = { counter: 0 };  }  render() {    return <A name="World" />  }  componentDidMount() {    setInterval(() => {      this.setState({        counter: this.state.counter + 1      });    }, 1000)  }}ReactDOM.render(<B/>, document.getElementById('container'));

上例中,父组件B的state change引发子组件A的render及componentWillReceiveProps被调用了,但A并没有发生props change

没错,只要接到了新的props,componentWillReceiveProps就会被调用,即便新props与旧的完全一样:

UNSAFE_componentWillReceiveProps() is invoked before a mounted component receives new props.Note that if a parent component causes your component to re-render, this method will be called even if props have not changed.

相关实现如下:

updateComponent: function () {  var willReceive = false;  var nextContext;  if (this._context !== nextUnmaskedContext) {    nextContext = this._processContext(nextUnmaskedContext);    willReceive = true;  }  // Not a simple state update but a props update  if (prevParentElement !== nextParentElement) {    willReceive = true;  }  if (willReceive && inst.componentWillReceiveProps) {    inst.componentWillReceiveProps(nextProps, nextContext);  }}

(摘自典藏版ReactDOM v15.6.1)

也就是说,componentWillReceiveProps的调用时机是:

引发当前组件更新 && (context发生变化 || 父组件render结果发生变化,即当前组件需要rerender)
注意,这里并没有对props做diff:

React doesn’t make an attempt to diff props for user-defined components so it doesn’t know whether you’ve changed them.

因为props值没什么约束,难以diff:

Oftentimes a prop is a complex object or function that’s hard or impossible to diff, so we call it always (and rerender always) when a parent component rerenders.

唯一能保证的是props change一定会触发componentWillReceiveProps,但反之不然:

The only guarantee is that it will be called if props change.

P.S.更多相关讨论见Documentation for componentWillReceiveProps() is confusing

二.如何理解getDerivedStateFromProps
getDerivedStateFromProps是用来替代componentWillReceiveProps的,应对state需要关联props变化的场景:

getDerivedStateFromProps exists for only one purpose. It enables a component to update its internal state as the result of changes in props.

即允许props变化引发state变化(称之为derived state,即派生state),虽然多数时候并不需要把props值往state里塞,但在一些场景下是不可避免的,比如:

记录当前滚动方向(recording the current scroll direction based on a changing offset prop)

取props发请求(loading external data specified by a source prop)

这些场景的特点是与props变化有关,需要取新旧props进行比较/计算,

与componentWillReceiveProps类似,getDerivedStateFromProps也不只是在props change时才触发,具体而言,其触发时机为:

With React 16.4.0 the expected behavior is for getDerivedStateFromProps to fire in all cases before shouldComponentUpdate.

更新流程中,在shouldComponentUpdate之前调用。也就是说,只要走进更新流程(无论更新原因是props change还是state change),就会触发getDerivedStateFromProps

就具体实现而言,与计算nextContext(nextContext = this._processContext(nextUnmaskedContext))类似,在确定是否需要更新(shouldComponentUpdate)之前,要先计算nextState:

export function applyDerivedStateFromProps(  workInProgress: Fiber,  ctor: any,  getDerivedStateFromProps: (props: any, state: any) => any,  nextProps: any,) {  const prevState = workInProgress.memoizedState;  const partialState = getDerivedStateFromProps(nextProps, prevState);  // Merge the partial state and the previous state.  const memoizedState =    partialState === null || partialState === undefined      ? prevState      : Object.assign({}, prevState, partialState);  workInProgress.memoizedState = memoizedState;  // Once the update queue is empty, persist the derived state onto the  // base state.  const updateQueue = workInProgress.updateQueue;  if (updateQueue !== null && workInProgress.expirationTime === NoWork) {    updateQueue.baseState = memoizedState;  }}

(摘自react/packages/react-reconciler/src/ReactFiberClassComponent.js)

getDerivedStateFromProps成了计算nextState的必要环节:

getDerivedStateFromProps is invoked right before calling the render method, both on the initial mount and on subsequent updates.function mountIndeterminateComponent(  current,  workInProgress,  Component,  renderExpirationTime,) {  workInProgress.tag = ClassComponent;  workInProgress.memoizedState =    value.state !== null && value.state !== undefined ? value.state : null;  const getDerivedStateFromProps = Component.getDerivedStateFromProps;  if (typeof getDerivedStateFromProps === 'function') {    applyDerivedStateFromProps(      workInProgress,      Component,      getDerivedStateFromProps,      props,    );  }  adoptClassInstance(workInProgress, value);  mountClassInstance(workInProgress, Component, props, renderExpirationTime);  // 调用render,第一阶段结束  return finishClassComponent(    current,    workInProgress,    Component,    true,    hasContext,    renderExpirationTime,  );}

(摘自react/packages/react-reconciler/src/ReactFiberBeginWork.js)

所以在首次渲染时也会调用,这是与componentWillReceiveProps相比最大的区别

三.派生state实践原则
实现派生state有两种方式:

getDerivedStateFromProps:从props派生出部分state,其返回值会被merge到当前state

componentWillReceiveProps:在该生命周期函数里setState

实际应用中,在两种常见场景中容易出问题(被称为anti-pattern,即反模式):

props变化时无条件更新state

更新state中缓存的props

在componentWillReceiveProps时无条件更新state,会导致通过setState()手动更新的state被覆盖掉,从而出现非预期的状态丢失:

When the source prop changes, the loading state should always be overridden. Conversely, the state is overridden only when the prop changes and is otherwise managed by the component.

例如(仅以componentWillReceiveProps为例,getDerivedStateFromProps同理):

class EmailInput extends Component {  state = { email: this.props.email };  render() {    return <input onChange={this.handleChange} value={this.state.email} />;  }  handleChange = event => {    this.setState({ email: event.target.value });  };  componentWillReceiveProps(nextProps) {    // This will erase any local state updates!    // Do not do this.    this.setState({ email: nextProps.email });  }}

上例中,用户在input控件中输入一串字符(相当于手动更新state),如果此时父组件更新引发该组件rerender了,用户输入的内容就被nextProps.email覆盖掉了(见在线Demo),出现状态丢失

针对这个问题,我们一般会这样解决:

class EmailInput extends Component {  state = {    email: this.props.email  };  componentWillReceiveProps(nextProps) {    // Any time props.email changes, update state.    if (nextProps.email !== this.props.email) {      this.setState({        email: nextProps.email      });    }  }}

精确限定props change到email,不再无条件重置state。似乎完美了,真的吗?

其实还存在一个尴尬的问题,有些时候需要从外部重置state(比如重置密码输入),而限定state重置条件之后,来自父组件的props.email更新不再无条件传递到input控件。所以,之前可以利用引发EmailInput组件rerender把输入内容重置为props.email,现在就不灵了

那么,需要想办法从外部把输入内容重置回props.email,有很多种方式:

EmailInput提供resetValue()方法,外部通过ref调用

外部改变EmailInput的key,强制重新创建一个EmailInput,从而达到重置回初始状态的目的

嫌key杀伤力太大(删除重建,以及组件初始化成本),或者不方便(key已经有别的作用了)的话,添个props.myKey结合componentWillReceiveProps实现局部状态重置

其中,第一种方法只适用于class形式的组件,后两种则没有这个限制,可根据具体场景灵活选择。第三种方法略绕,具体操作见Alternative 1: Reset uncontrolled component with an ID prop

类似的场景之所以容易出问题,根源在于:

when a derived state value is also updated by setState calls, there isn’t a single source of truth for the data.

一边通过props计算state,一边手动setState更新,此时该state有两个来源,违背了组件数据的单一源原则

解决这个问题的关键是保证单一数据源,杜绝不必要的拷贝:

For any piece of data, you need to pick a single component that owns it as the source of truth, and avoid duplicating it in other components.

所以有两种方案(砍掉一个数据源即可):

完全去掉state,这样就不存在state与props的冲突了

忽略props change,仅保留第一次传入的props作为默认值

两种方式都保证了单一数据源(前者是props,后者是state),这样的组件也可以称之为完全受控组件与完全不受控组件

四.“受控”与“不受控”
组件分为受控组件与不受控组件,同样,数据也可以这样理解

受控组件与不受控组件
针对表单输入控件(<input>、<textarea>、<select>等)提出的概念,语义上的区别在于受控组件的表单数据由React组件来处理(受React组件控制),而不受控组件的表单数据交由DOM机制来处理(不受React组件控制)

受控组件维护一份自己的状态,并根据用户输入更新这份状态:

An input form element whose value is controlled by React is called a controlled component. When a user enters data into a controlled component a change event handler is triggered and your code decides whether the input is valid (by re-rendering with the updated value). If you do not re-render then the form element will remain unchanged.

用户与受控组件交互时,用户输入反馈到UI与否,取决于change事件对应的处理函数(是否需要改变内部状态,通过rerender反馈到UI),用户输入受React组件控制,例如:

class NameForm extends React.Component {  constructor(props) {    super(props);    this.state = {value: ''};    this.handleChange = this.handleChange.bind(this);  }  handleChange(event) {    // 在这里决定是否把输入反馈到UI    this.setState({value: event.target.value});  }  render() {    return (      <input type="text" value={this.state.value} onChange={this.handleChange} />    );  }}

不受控组件不维护这样的状态,用户输入不受React组件控制:

An uncontrolled component works like form elements do outside of React. When a user inputs data into a form field (an input box, dropdown, etc) the updated information is reflected without React needing to do anything. However, this also means that you can’t force the field to have a certain value.

用户与不受控组件的交互不受React组件控制,输入会立即反馈到UI。例如:

class NameForm extends React.Component {  constructor(props) {    super(props);    this.handleSubmit = this.handleSubmit.bind(this);    this.input = React.createRef();  }  handleSubmit(event) {    // input的输入直接反馈到UI,仅在需要时从DOM读取    alert('A name was submitted: ' + this.input.current.value);    event.preventDefault();  }  render() {    return (      <form onSubmit={this.handleSubmit}>        <label>          Name:          <input type="text" ref={this.input} />        </label>        <input type="submit" value="Submit" />      </form>    );  }}

从数据角度看受控与不受控
不受控组件把DOM当作数据源:

An uncontrolled component keeps the source of truth in the DOM.

而受控组件把自身维护的state当作数据源:

Since the value attribute is set on our form element, the displayed value will always be this.state.value, making the React state the source of truth.

让程序行为可预测的关键在于减少变因,即保证唯一数据源。那么就有数据源唯一的组件,称之为完全受控组件与完全不受控组件

对应到之前派生state的场景,就有了这两种解决方案:

// 完全不受控组件,不再维护输入statefunction EmailInput(props) {  return <input onChange={props.onChange} value={props.email} />;}// 完全受控组件,只维护自己的state,不接受来自props的更新class EmailInput extends Component {  state = { email: this.props.defaultEmail };  handleChange = event => {    this.setState({ email: event.target.value });  };  render() {    return <input onChange={this.handleChange} value={this.state.email} />;  }}

所以,在需要复制props到state的场景,要么考虑把props收进来完全作为自己的state,不再受外界影响(使数据受控):

Instead of trying to “mirror” a prop value in state, make the component controlled

要么把自己的state丢掉,完全放弃对数据的控制:

Remove state from our component entirely.

五.缓存计算结果
另一些时候,拷贝props到state是为了缓存计算结果,避免重复计算

例如,常见的列表项按输入关键词筛选的场景:

class Example extends Component {  state = {    filterText: "",  };  static getDerivedStateFromProps(props, state) {    if (      props.list !== state.prevPropsList ||      state.prevFilterText !== state.filterText    ) {      return {        prevPropsList: props.list,        prevFilterText: state.filterText,        // 缓存props结算结果到state        filteredList: props.list.filter(item => item.text.includes(state.filterText))      };    }    return null;  }  handleChange = event => {    this.setState({ filterText: event.target.value });  };  render() {    return (      <Fragment>        <input onChange={this.handleChange} value={this.state.filterText} />        <ul>{this.state.filteredList.map(item => <li key={item.id}>{item.text}</li>)}</ul>      </Fragment>    );  }}

能用,但过于复杂了。通过getDerivedStateFromProps创造了另一个变因(state.filteredList),这样props change和state change都可能影响筛选结果,容易出问题

事实上,想要避免重复计算的话,并不用缓存一份结果到state,比如:

class Example extends PureComponent {  state = {    filterText: ""  };  handleChange = event => {    this.setState({ filterText: event.target.value });  };  render() {    const filteredList = this.props.list.filter(      item => item.text.includes(this.state.filterText)    )    return (      <Fragment>        <input onChange={this.handleChange} value={this.state.filterText} />        <ul>{filteredList.map(item => <li key={item.id}>{item.text}</li>)}</ul>      </Fragment>    );  }}

利用PureComponent的render()只在props change或state change时才会再次调用的特性,直接在render()里放心做计算

看起来很完美,但实际场景的state和props一般不会这么单一,如果另一个计算无关的props或state更新了也会引发rerender,产生重复计算

所以干脆抛开“不可靠”的PureComponent,这样解决:

import memoize from "memoize-one";class Example extends Component {  state = { filterText: "" };  filter = memoize(    (list, filterText) => list.filter(item => item.text.includes(filterText))  );  handleChange = event => {    this.setState({ filterText: event.target.value });  };  render() {    const filteredList = this.filter(this.props.list, this.state.filterText);    return (      <Fragment>        <input onChange={this.handleChange} value={this.state.filterText} />        <ul>{filteredList.map(item => <li key={item.id}>{item.text}</li>)}</ul>      </Fragment>    );  }}

不把计算结果放到state里,也不避免rerender,而是缓存到外部,既干净又可靠

参考资料
You Probably Don’t Need Derived State

React.Component

When “getDerivedStateFromProps” is invoked on Update phase?

Controlled vs. Uncontrolled Components

更多相关文章

  1. SpringMVC源码分析:一个request请求的完整流程和各组件介绍
  2. 几款代码高亮组件的体验,说不定你以后会用到
  3. Springboot整合mybatis多数据源(注解完整版)
  4. JavaScript测试教程–part 4:模拟 API 调用和模拟 React 组件交互
  5. JavaScript测试教程-part 2:引入 Enzyme 并测试 React 组件[每日
  6. Spring Boot 项目中的三种多数据源方案,一个比一个强!
  7. 认识DHTML中的“行为”组件
  8. 利用js、jQuery和css实现环形进度条组件封装
  9. 扩展htmlhelper.DropDownListFor 支持list数据源和option增加属

随机推荐

  1. 如何使用Amazon S3创建一次性下载链接?
  2. 什么是从我的PHP网站过滤无效的utf8?
  3. php会话不在chrome和firefox中工作但在IE
  4. 快速配置IIS 6.0 php fastcgi +Zend Opti
  5. Ionic-3如何动态改变离子主题
  6. Apache引起的wampserver安装好第二次使用
  7. PHP面向对象笔记 —— 113 封装概念
  8. 如果改变输入值,jQuery提交表单
  9. Kohana框架3.3 cookie设置两次
  10. angular.min.js:107 Error: [ng:areq] ht