React 基于 props 更新 state
单向数据流
所有的 prop 都使得其父子 prop 之间形成了一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外改变父级组件的状态,从而导致你的应用的数据流向难以理解。
额外的,每次父级组件发生更新时,子组件中所有的 prop 都将会刷新为最新的值。这意味着你不应该在一个子组件内部改变 prop。
Props 的只读性
决不能修改自身的 props。所有组件都必须保护它们的 props 不被更改。
State 与 props 类似,但是 state 是私有的,并且完全受控于当前组件。state 允许组件随用户操作、网络响应或者其他变化而动态更改输出内容。
让组件在 props 变化时更新 state
在 vue 中我们会这样做,定义一个本地的 data 属性并将这个 prop 用作其初始值,然后通过 watch 监控 prop 变化,然后重复赋值给本地的 data 属性。
props: ['initialCounter'], data () { return { counter: this.initialCounter } }, watch: { initialCounter (newCounter) { if (newCounter !== this.counter) { this.counter = newCounter } } }
在 react 中,从 16.3 版本开始,当 props 变化时,建议使用新的 static getDerivedStateFromProps
生命周期更新 state。创建组件以及每次组件由于 props 或 state 的改变而重新渲染时都会调用该生命周期:
class ExampleComponent extends React.Component { // 在构造函数中初始化 state, // 或者使用属性初始化器。 state = { counter: this.props.initialCounter, }; static getDerivedStateFromProps(props, state) { if (props.initialCounter !== state.counter) { return { counter: props.initialCounter }; } // 返回 null 表示无需更新 state。 return null; } handleClick = () => { // 点击之后无法修改 counter 的 bug this.setState({counter: this.state.counter + 1}) } render() { return ( <div onClick={this.handleClick}>{this.state.counter}</div> ); } }
getDerivedStateFromProps
会在调用 render 方法之前调用,并且在初始挂载及后续更新时都会被调用。它应返回一个对象来更新 state,如果返回 null 则不更新任何内容。
请注意,不管原因是什么,都会在每次渲染前触发此方法。这与 componentWillReceiveProps
形成对比,后者仅在父组件重新渲染时触发,而不是在内部调用 setState 时。
基于 props 更新 state,旧的 componentWillReceiveProps
和新的 getDerivedStateFromProps
方法都会给组件增加明显的复杂性。这通常会导致 bug。
最常见的误解就是 getDerivedStateFromProps
和 componentWillReceiveProps
只会在 props “改变”时才会调用。实际上只要父级重新渲染时,这两个生命周期函数就会重新调用,不管 props 有没有“变化”。所以,在这两个方法内直接复制 props 到 state 是不安全的。这样做会导致 state 后没有正确渲染。
希望以上能解释清楚为什么直接复制 prop 到 state 是一个非常糟糕的想法。在寻找解决方案之前,让我们看看一个相关的问题:假如我们只使用 props 中的 counter 属性更新组件呢?
虽然这个设计就有问题,但是这样的错误很常见,(我就犯过这样的错误)。任何数据,都要保证只有一个数据来源,而且避免直接复制它。
完全可控的组件
阻止上述问题发生的一个方法是,从组件里删除 state。然后传入在父组件中定义的处理函数进行修改。
class ExampleComponent extends React.Component { render() { return ( <div onClick={this.props.handleClick}> {this.props.initialCounter} </div> ); } }
虽然 vue 中没有这个问题,但是建议大家不要在组件里面修改 props,任何数据,都要保证只有一个数据来源,而且避免直接复制它。都必须保护它们的 props 不被更改。
总结
派生状态会导致代码冗余,并使组件难以维护。 确保你已熟悉这些简单的替代方案:
1. 如果你需要执行副作用(例如,数据提取或动画)以响应 props 中的更改,请改用 componentDidUpdate
。
class ExampleComponent extends React.Component { state = { counter: this.props.initialCounter } handleClick = () => { this.setState({ counter: this.state.counter + 1 }) } componentDidUpdate(prevProps) { if (this.props.initialCounter !== prevProps.initialCounter) { this.setState({ counter: this.props.initialCounter }) } } render() { return ( <div onClick={this.handleClick}> {this.state.counter} </div> ); } }
你也可以在 componentDidUpdate()
中直接调用 setState(),但请注意它必须被包裹在一个条件语句里,正如上述的例子那样进行处理,否则会导致死循环。它还会导致额外的重新渲染,虽然用户不可见,但会影响组件性能。不要将 props “镜像”给 state,请考虑直接使用 props。
3. 如果只想在 prop 更改时重新计算某些数据,请使用 memoization 帮助函数代替。
仅在输入变化时,重新计算 render 需要使用的值————这个技术叫做 memoization 。(也就是Vue中的计算属性类似)
import memoize from "memoize-one"; class Example extends Component { // state 只需要保存当前的 filter 值: state = { filterText: "" }; // 在 list 或者 filter 变化时,重新运行 filter: filter = memoize( (list, filterText) => list.filter(item => item.text.includes(filterText)) ); handleChange = event => { this.setState({ filterText: event.target.value }); }; render() { // 计算最新的过滤后的 list。 // 如果和上次 render 参数一样,`memoize-one` 会重复使用上一次的值。 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> ); } }
在使用 memoization 时,请记住这些约束:
1、大部分情况下, 每个组件内部都要引入 memoized 方法,已免实例之间相互影响。
2、一般情况下,我们会限制 memoization 帮助函数的缓存空间,以免内存泄漏。(上面的例子中,使用 memoize-one 只缓存最后一次的参数和结果)。
4. 如果你想在 prop 更改时“重置”某些 state,请考虑使组件完全受控或使用 key 使组件完全不受控 代替。
1、完全可控的组件
class ExampleComponent extends React.Component { render() { return ( <div onClick={this.props.handleClick}> {this.props.initialCounter} </div> ); } }
2、有 key 的非可控组件
另外一个选择是让组件自己存储临时的 state。在这种情况下,组件仍然可以从 prop 接收“初始值”,但是更改之后的值就和 prop 没关系了:
class ExampleComponent extends React.Component { state = { counter: this.props.initialCounter } handleClick = () => { this.setState({ counter: this.state.counter + 1 }) } render() { return ( <div onClick={this.handleClick}> {this.state.counter} </div> ); } } // <ExampleComponent initialCounter={this.state.counter} key={this.state.key} />
我们可以使用 key 这个特殊的 React 属性。当 key 变化时, React 会创建一个新的而不是更新一个既有的组件。
class ExampleComponent extends React.Component { state = { counter: this.props.initialCounter } handleClick = () => { this.setState({ counter: this.state.counter + 1 }) } render() { return ( <div onClick={this.handleClick}> {this.state.counter} </div> ); } } class App extends React.Component { state = { counter: 0, key: 0 } handleClick = () => { this.setState({ counter: this.state.counter + 1, key: this.state.key + 1, }) } render() { return ( <div> <button onClick={this.handleClick} >点击</button> <ExampleComponent initialCounter={this.state.counter} key={this.state.key} /> </div> ); } }
上面是react的写法,再介绍一下vue的最佳写法使用语法糖v-model。
<template> <div class="hello"> <h1 @click="handleClick">{{ value }}</h1> </div> </template> <script> export default { name: 'HelloWorld', props: ['value'], data() { return { counter: this.value }; }, methods: { handleClick () { this.$emit('input', this.value + 1) } } } </script> // <HelloWorld v-model="initialCounter"/>
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论