我们都知道 setState() 使用来更新数据的,但你知道吗?setState() 方法更新数据的操作,是异步更新操作。 也就是调用setState()之后状态值并不会立即改变,而是等同步操作执行结束后才会进行改变。所以在连续使用该方法修改数据时,后面的setState()不能依赖于前面的setState(),因为后面的setState()在被调用时,前面的setState()还并未将状态修改,所以后面的setState()获取的值,还是状态的初始值。
// 初始状态值
state = {
count: 1
}
// 修改状态的方法
handleClick = () => {
// 此处,第一次更新state
// 注意:异步更新数据的!!!
this.setState({
count: this.state.count + 1
})
console.log('count:', this.state.count) // 1
// 第二次更新state
this.setState({
// 此时第一次setState() 还没有将数据修改完成 所以获取的还是初始值
count: this.state.count + 1 // 1 + 1
})
console.log('count:', this.state.count) // 1
}
// 最终页面中 展示 count 值 还是2 因为两次 setState() 的修改结果都是 2
render() {
console.log('render')
return (
计数器:{this.state.count}
)
}
*注意:而且在React方法中多次调用 setState()方法,只会触发一次页面的重新渲染。
通过上面我们可以知道,如果想要在方法中调用两次setState()方法来修改同一个状态是无法实现我们想要的效果,所以React为我们提供一个新语法来解决上面的问题:setState((state,props) => { 修改操作 })
,该语法的参数是一个箭头函数,箭头函数的参数获取的都是页面中最新的 state 和 props,内部则是对状态进行操作。也就是说,在连续使用该语法修改数据时,后面的setState()可以依赖于前面的setState()。虽然获取的是最新的状态,但该语法更新状态 state 的操作也是异步操作:
// 初始状态值
state = {
count: 1
}
// 更新状态的方法
handleClick = () => {
// 推荐语法
// 注意:这种语法也是异步更新state的!
this.setState((state, props) => {
return {
count: state.count + 1 // 1 + 1
}
})
console.log('count:', this.state.count) // 1
this.setState((state, props) => {
console.log('第二次调用:', state)
return {
count: state.count + 1 // 2 + 1
}
})
console.log('count:', this.state.count) // 1
}
// 最终页面中 展示 count 值 是 3
render() {
return (
计数器:{this.state.count}
)
}
该语法还可以设置第二个参数,也是一个函数,表示修改状态操作的回调函数,在页面完成重新渲染后,才会立即执行的某个操作:
state = {
count: 1
}
handleClick = () => {
this.setState(
// 第一个参数 修改状态操作
(state, props) => {
return {
count: state.count + 1
}
},
// 第二个参数 回调函数
// 状态更新后并且重新渲染后,立即执行:
() => {
console.log('状态更新完成:', this.state.count) // 2
console.log(document.getElementById('title').innerText)
document.title = '更新后的count为:' + this.state.count
}
)
console.log(this.state.count) // 1
}
JSX 是React提供的对 createElement() 方法的简化语法(语法糖),在代码编译时,JSX语法会被 @babel/preset-react 插件编译为原本的 createElement() 方法,然后再被解析为 React 元素,最后被渲染到页面中:
setState() 方法在修改状态之后,还会去重新渲染组件的内容。但这个重新渲染的范围是有规定的:组件在进行重新渲染时,也会去重新渲染它所有的子组件,其父组件以及兄弟组件则不会被影响。
当Parent2被触发重新渲染时:
减轻 state 是指:在 state 中只存储跟组件渲染相关的数据,例如:数据变量、列表数据、loading等。其他数据则不要放在state中,根据场景存储到不同位置,比如定时器这类可能在多个方法中被用到的数据,应该放在 this 中,方便调用:
class Hello extends Component {
componentDidMount() {
this.timerId = setInterval(() => {},1000)
}
componentWillUnmount() {
clearInterval(this.timerId)
}
render() {...}
}
从上面我们可以得知,React 的组件更新机制是:当父组件更新时,其所有子组件无论是否有变化,都会跟着更新。这样就出现了不必要的渲染,浪费性能。我们想要实现只有发生变化的组件才进行重新渲染的效果,可以借助于更新阶段的生命周期钩子函数:shouldComponentUpdate(nextProps,nextState)
,nextProps 和 nextState 表示最新的props和最新的state。通过nextProps或nextState判断是否数据发生变化,并以该钩子函数的返回值是true/false,来决定当前组件是否重新渲染。只有当返回值为 true 时,当前组件才会重新渲染。
class App extends React.Component {
state = {
number: 0
}
shouldComponentUpdate(nextProps, nextState) {
// 比较最新的state状态和原来state状态 是否发生变化 决定是否重新渲染
return nextState.number !== this.state.number
// 或者比较最新的props和原来props 是否发生变化 决定是否重新渲染
return nextProps.number !== this.props.number
}
render() {...}
}
之前我们创建的类组件都是继承的 React.Component
,而纯组件是继承的 React.PureComponent
。纯组件与普通组件的区别在于:纯组件内部自动实现了对比前后两次 props 和 state 的数据,并根据是否有变化,来决定 是否重新渲染组件:
// 纯组件 继承 React.PureComponent
class App extends React.PureComponent {
render() {...}
}
但要注意的是:纯组件内部的自动对比 props 和 state 是浅层对比,对简单数据类型(值类型)来说没啥不同,但对复杂数据类型(引用类型)来说,浅层对比仅仅是比较引用的地址空间是否相同。如果想要修改 state或props 中复杂数据类型的状态,我们不能直接修改原数据,因为地址空间未变,无法被检测到,也就不会重新渲染。正确的做法是:我们应该去创建一个新数据,引发重新渲染:
// 生成随机数
class App extends React.PureComponent {
state = {
obj: {
number: 0
}
}
handleClick = () => {
// 正确做法:创建新对象,引用地址与旧对象不同
// 利用...扩展运算符
const newObj = { ...this.state.obj, number: Math.floor(Math.random() * 3) }
this.setState(() => {
return {
// 将新对象赋值给旧对象 引发重新渲染
obj: newObj
}
})
// 错误演示:直接修改原始对象中属性的值
/* const newObj = this.state.obj
newObj.number = Math.floor(Math.random() * 3)
this.setState(() => {
return {
obj: newObj
}
}) */
}
render() {
console.log('父组件重新render')
return (
随机数:{this.state.obj.number}
)
}
}
如果是数组类型的数据,除了创建新数组之外,还可以通过 concat()/slice() 等返回一个新数组的方法来修改数组数据。但不要用 push()/unshift() 等直接修改当前数组的方法,因为引用地址未改变,无法被检测到变化,也就无法触发重新渲染。
React更新视图的思想是:当组件的状态发生变化时,就去更新重新渲染视图,但并不是全部更新,而是依靠虚拟DOM配合Diff算法来实现部分更新,只重新渲染变化的部分视图。
虚拟DOM是什么?虚拟DOM是组件在创建并初次渲染(组件的render()调用)的时候,根据 初始 state 和 JSX结构 所构建的一个JS对象。然后根据这个虚拟DOM生成真正的DOM树,渲染到页面中。当组件的数据发生变化后(setState()),框架会根据最新的数据,创建一个新的虚拟DOM对象,并在组件的render()调用的时候将新的虚拟DOM与原来的虚拟DOM使用Diff算法进行对比,找出不同的DOM节点。最后根据最新的数据去重新渲染这部分发生变化的视图。
(认知尚浅,后续更新…)