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