文章目录
前言
这是一个很重要的知识点,如果不加以注意,很有可能会出现bug,别问我为什么知道哈哈。
个人觉得这里是react设计的不太好的地方,不过应该也是有原因的,我们只需要记住setState时尽量不要修改原数据就好,下面来看看原因。
为什么setState时尽量不要修改原数据
首先,什么是修改原数据,如下:
let data = this.state.data
this.setState({data: data.push(1)}) // 这样修改了data原数据
this.setState({data: data.contact([1])}) // 这样就没有修改原数据,返回的是一个新的变量
两者使用的都能正常修改到数据,并且视图能正常更新,原因如下。
- 只要是
setState
了(无论视图中是否使用到),父组件一定会重新执行render
,然后底层经历diff算法后,重新渲染需要更新的部分视图。 - 因为父组件走了render,所以子组件也更新了,走
componentDidUpdate
钩子,并且执行render(即使是毫无变化的子组件)。
小问题:不知道为什么我尝试的时候发现,在子组件中,render先执行,然后才执行componentDidUpdate。
既然两种方法都能正常更新视图,那为什么网上都说setState时尽量不要修改原数据?
分析
其实原因就是当使用了shouldComponentUpdate
钩子函数时就会出现视图更新的问题了。
shouldComponentUpdate
react提供了shouldComponentUpdate这个钩子让我们去控制无论怎么setState,都执行render的行为。
shouldComponentUpdate这个函数我们不写底层是默认返回true的。和上面说的一样,无论如何,只要父组件传入的数据有变动,哪怕是重新赋值,子组件都会走更新的生命钩子并且执行render。
shouldComponentUpdate(nextProps, nextState) {
return true
}
我们可以在这个函数中利用两个入参nextProps, nextState加上判断,满足什么条件返回true就执行render,满足什么条件返回false就不执行render。
例如,父组件通过新旧的state对比去看要不要执行render,子组件可以通过新旧的props对比去看要不要执行render。
所以,当一个子组件很复杂时,无意义的重新执行render再进行diff算法,开销太大,我们一般会在子组件中重新自定义这个函数:
shouldComponentUpdate(nextProps, nextState) {
if (this.props.data === nextProps.data) { return false } // 如果是重新赋值的方式就不更新视图
return true
}
问题来了,这和setState时尽量不要修改原数据有什么关系呢?
与setState的关系
我看网上说的大概意思是:
当我们在父组件setState选择了修改原数据方式,在父组件shouldComponentUpdate中,做了对比某个state新旧值,会发现新旧都是一样的,这时候自己写的代码就会返回false,不更新父子组件。
我们来写个例子看看,首先父组件:
export default class App extends Component {
state = {
a: [1],
};
shouldComponentUpdate(nextp, nexts) {
console.log("shouldComponentUpdate", nexts, this.state);
return true;
}
changeState = () => {
let { a } = this.state;
// this.setState({ a: a.concat([2]) });
this.setState({ a: a.push(2) });
};
render() {
let { a } = this.state;
console.log('父组件走render');
return (
<div onClick={this.changeState}>
{a}
<Son a={a}></Son>
</div>
);
}
}
然后子组件:
class Son extends Component {
state = {};
shouldComponentUpdate(nextp, nexts) {
console.log("子组件shouldComponentUpdate", nextp);
return true;
}
render() {
console.log('子组件走render');
return <div>我是子组件</div>;
}
}
当我们使用不修改原数据的方式时,nexts传入的就是正常最新的修改值{a:[1,2]}
,
当我们使用修改原数据的方式时,视图同样会更新,且子组件同样会走shouldComponentUpdate和render。但是!shouldComponentUpdate的nexts传入的为{a:2}
,a直接变成2了。这就很难受了,如果我们在shouldComponentUpdate中写了判断就容易出错。
那为什么使用原数据修改的方式a会变成2呢?我发现:
let a = [1]; console.log(a.push(1)) // 控制台上打出来的就是2,其实返回的是数组的长度!
所以使用修改原数据的方式并不是这样子写的,“正确”写法应该是:
a.push(2)
this.setState({ a }); // 不能直接在这里push
这时候shouldComponentUpdate的nexts和this.state就会一样是最新的[1,2]了,那么我们在里面写判断的话,nexts.a和this.state.a就会是一样的,返回false,组件都不更新!
shouldComponentUpdate(nextp, nexts) {
console.log("shouldComponentUpdate", nexts, this.state);
if (nexts.a !== this.state.a) {
return true
}
return false;
}
并且你就算不写判断,都返回true,父组件虽然会走render,但它的视图不会更新!!
实践出真知。这下都明白了。
虽然不用shouldComponentUpdate,修改了原数据的写法也无伤大雅,但是还是要严格要求自己,因为可变和不可变的写法积累也是一个重要基础。
不要直接修改state
超级重要!
如果想用state里的引用类型变量去做一些改变原值的处理,那么一定要进行深拷贝!
不好的例子:
let { userList } = this.state
// 类似改变原值的操作
userList[0].name = 'a'
// ...
PureComponent纯组件
如果每次都要在每个组件手动shouldComponentUpdate去优化更新机制太过麻烦,所以react提供了PureComponent:
import React, { PureComponent } from 'react' // 就是把PureComponent代替掉Component
export default class App extends PureComponent {
...
}
原理就是react在底层机制已经通过shouldComponentUpdate去帮我们处理好(比较新旧state或props数据, 如果有变化才返回true, 如果没有返回false),子孙组件自动看需不需要更新。
注意点
第一:父组件更新state的时候,需要传入的是一个新的变量,因为react会对传入的变量与原来的state里的数据做比较,如果是同一个引用堆内存的地址,就不会更新所有组件(说白了也是不要修改原数据的方式去更新state):
state = { a: 1 }
...
fn = () => {
state = this.state
state.a = 2
this.setState(state) // 不会更新render
}
fn = () => {
state = this.state
state.a = 2
this.setState({...state}) // 才会更新render
}
第二:这个组件在底层做比较的时候,只会比较一维,多维的数据还是会更新的。
总结
我认为PureComponent组件只适合写小UI组件,且必须满足以下条件:
- UI纯粹、数据简单、场景固定
- 不再包含子组件
如果一上来就考虑做成PureComponent,日后拓展必然会换掉。
题外话
在函数式组件的写法中,如果还是采用修改原数据的方式去写,会偶然遇到视图不更新的问题。具体原因我还没去研究,所以还是那句话最好严格要求自己,使用一维拷贝或者深拷贝的方式去修改state。
peace~