【React系列】React生命周期、setState深入理解、 shouldComponentUpdate和PureComponent性能优化、脚手架
本文来自#React系列教程:https://mp.weixin.qq.com/mp/appmsgalbum?__biz=Mzg5MDAzNzkwNA==&action=getalbum&album_id=1566025152667107329)
一. 生命周期
1.1. 认识生命周期
很多的事物都有从创建到销毁的整个过程,这个过程称之为是生命周期;
React组件也有自己的生命周期,了解组件的生命周期可以让我们在最合适的地方完成自己想要的功能;
生命周期和生命周期函数的关系:
- 生命周期是一个抽象的概念,在生命周期的整个过程,分成了很多个阶段;
- 比如装载阶段(Mount),组件第一次在DOM树中被渲染的过程;
- 比如更新过程(Update),组件状态发生变化,重新更新渲染的过程;
- 比如卸载过程(Unmount),组件从DOM树中被移除的过程;
- React内部为了告诉我们当前处于哪些阶段,会对我们组件内部实现的某些函数进行回调,这些函数就是生命周期函数:
- 比如实现
componentDidMount
函数:组件已经挂载到DOM上时,就会回调; - 比如实现
componentDidUpdate
函数:组件已经发生了更新时,就会回调; - 比如实现
componentWillUnmount
函数:组件即将被移除时,就会回调;
- 比如实现
我们可以在这些回调函数中编写自己的逻辑代码,来完成自己的需求功能;
我们谈React生命周期时,主要谈的类的生命周期,因为函数式组件是没有生命周期函数的;(后面我们可以通过hooks来模拟一些生命周期的回调)
1.2. 常用生命周期解析
我们先来学习一下最基础、最常用的生命周期函数:
上图第一个区域解析:
- 当我们挂载一个组件时,会先执行
constructor
构造方法来创建组件; - 紧接着调用
render
函数,获取要渲染的DOM结构(jsx),并且开始渲染DOM; - 当组件挂载成功(DOM渲染完成),会执行
componentDidMount
生命周期函数;
上图第二个区域解析:
- 当我们通过修改
props
,或者调用setState
修改内部状态,或者直接调用forceUpdate
时会重新调用render
函数,进行更新操作; - 当更新完成时,会回调
componentDidUpdate
生命周期函数;
上图第三个区域解析:
- 当我们的组件不再使用,会被从DOM中移除掉(卸载);
- 这个时候会回调
componentWillUnmount
生命周期函数;
1.3. 生命周期函数
constructor
constructor(props)
如果不初始化 state
或不进行方法绑定,则不需要为 React 组件实现构造函数。
constructor
中通常只做两件事情:
- 通过给
this.state
赋值对象来初始化内部的state
; - 为事件绑定实例(
this
);
componentDidMount
componentDidMount()
componentDidMount()
会在组件挂载后(插入 DOM 树中)立即调用。
componentDidMount
中通常进行哪里操作呢?
- 依赖于DOM的操作可以在这里进行;
- 在此处发送网络请求就最好的地方;(官方建议)
- 可以在此处添加一些订阅(会在
componentWillUnmount
取消订阅);
componentDidUpdate
componentDidUpdate(prevProps, prevState, snapshot)
componentDidUpdate()
会在更新后会被立即调用,首次渲染不会执行此方法。
- 当组件更新后,可以在此处对 DOM 进行操作;
- 如果你对更新前后的
props
进行了比较,也可以选择在此处进行网络请求;(例如,当props
未发生变化时,则不会执行网络请求)。
componentDidUpdate(prevProps) {// 典型用法(不要忘记比较 props):if (this.props.userID !== prevProps.userID) {this.fetchData(this.props.userID);}
}
componentWillUnmount
componentWillUnmount()
componentWillUnmount()
会在组件卸载及销毁之前直接调用。
- 在此方法中执行必要的清理操作;
- 例如,清除 timer,取消网络请求或清除在
componentDidMount()
中创建的订阅等;
代码验证所有的生命周期函数:
import React, { Component } from 'react';class HYTestCpn extends Component {render() {return <h2>HYTestCpn</h2>}componentWillUnmount() {console.log("HYTestCpn componentWillUnmount");}
}export default class App extends Component {constructor(props) {super(props);this.state = {counter: 0}console.log("调用constructor方法");}render() {console.log("调用render方法")return (<div><h2>当前计数: {this.state.counter}</h2>{this.state.counter <= 5 && <HYTestCpn/>}<button onClick={e => this.increment()}>+1</button></div>)}increment() {this.setState({counter: this.state.counter + 1})}componentDidMount() {console.log("调用componentDidMount方法");}componentDidUpdate() {console.log("调用componentDidUpdate方法");}componentWillUnmount() {console.log("调用componentWillUnmount方法");}
}
1.4. 不常用生命周期
除了上面介绍的生命周期函数之外,还有一些不常用的生命周期函数:
getDerivedStateFromProps
:state
的值在任何时候都依赖于props
时使用;该方法返回一个对象来更新state
;getSnapshotBeforeUpdate
:在React更新DOM之前回调的一个函数,可以获取DOM更新前的一些信息(比如说滚动位置);shouldComponentUpdate
:该生命周期函数很常用,但是我们等待讲性能优化时再来详细讲解;
另外,React中还提供了一些过期的生命周期函数,这些函数已经不推荐使用。
更详细的生命周期相关的内容,可以参考官网:https://zh-hans.reactjs.org/docs/react-component.html
二. setState
在 constructor()
函数中不要调用 setState()
方法。如果你的组件需要使用内部state
,请直接在构造函数中为this.state
赋值初始 state
:
constructor(props) {super(props);// 不要在这里调用 this.setState() this.state = { counter: 0};this.handleClick = this.handleClick.bind(this);
}
只能在构造函数中直接为 this.state
赋值。如需在其他方法中赋值,你应使用 this.setState()
替代。
你可以用类似的方式改写代码来避免可变对象的产生。例如,我们有一个叫做 colormap
的对象。我们希望写一个方法来将 colormap.right
设置为 'blue
'。我们可以这么写:
function updateColorMap(colormap) { colormap.right = 'blue';
}
为了不改变原本的对象,我们可以使用 Object.assign
方法:
function updateColorMap(colormap) {return Object.assign({}, colormap, {right: 'blue'});
}
现在 updateColorMap
返回了一个新的对象,而不是修改老对象。Object.assign
是ES6的方法,需要polyfill。
不可变数据的力量
避免该问题最简单的方式是避免更改你正用于props
或 state
的值。例如,上面handleClick
方法可以用 concat
重写:
handleClick() {this.setState(state => ({words: state.words.concat(['marklar']) }));
}
ES6数组支持扩展运算符,这让代码写起来更方便了。如果你在使用Create React App
,该语法已经默认支持了。
handleClick() {this.setState(state => ({words: [...state.words, 'marklar'], }));
};
参数一为带有形式参数的 updater
函数:(state, props) => stateChange
state
是对应用变化时组件状态的引用。当然,它不应直接被修改。- 你应该使用基于
state
和props
构建的新对象来表示变化。
例如,假设我们想根据 props.step
来增加 state
:
this.setState((state, props) => {return {counter: state.counter + props.step};
});
updater
函数中接收的state
和props
都保证为最新。updater
的返回值会与state
进行浅合并。
setState异步更新
setState的更新是异步的?
import React, { Component } from 'react'export default class App extends Component {constructor(props) {super(props);this.state = {message: "Hello World"}}render() {return (<div><h2>{this.state.message}</h2><button onClick={e => this.changeText()}>改变文本</button></div>)}changeText() {this.setState({message: "你好啊,李银河"})console.log(this.state.message); // Hello World}
}
最终打印结果是Hello World
;可见setState
是异步的操作,我们并不能在执行完setState
之后立马拿到最新的state
的结果。
为什么setState
设计为异步呢?
setState
设计为异步其实之前在GitHub上也有很多的讨论;- React核心成员(Redux的作者)Dan Abramov也有对应的回复,有兴趣的同学可以参考一下;
- https://github.com/facebook/react/issues/11527#issuecomment-360199710
我对其回答做一个简单的总结:
setState
设计为异步,可以显著的提升性能;- 如果每次调用
setState
都进行一次更新,那么意味着render
函数会被频繁调用,界面重新渲染,这样效率是很低的; - 最好的办法应该是获取到多个更新,之后进行批量更新;
- 如果每次调用
- 如果同步更新了
state
,但是还没有执行render
函数,那么state
和props
不能保持同步;state
和props
不能保持一致性,会在开发中产生很多的问题;
那么如何可以获取到更新后的值呢?
setState
接受两个参数:第二个参数是一个回调函数,这个回调函数会在更新后会执行;- 格式如下:
setState(partialState, callback)
changeText() {this.setState({message: "你好啊,李银河"}, () => {console.log(this.state.message); // 你好啊,李银河});
}
当然,我们也可以在生命周期函数:
componentDidUpdate(prevProps, provState, snapshot) {console.log(this.state.message);
}
setState一定是异步?
疑惑:setState
一定是异步更新的吗?
验证一:在setTimeout
中的更新:
changeText() {setTimeout(() => {this.setState({message: "你好啊,李银河"});console.log(this.state.message); // 你好啊,李银河}, 0);
}
验证二:原生DOM事件:
componentDidMount() {const btnEl = document.getElementById("btn");btnEl.addEventListener('click', () => {this.setState({message: "你好啊,李银河"});console.log(this.state.message); // 你好啊,李银河})
}
其实分成两种情况:
- 在 React 组件生命周期或 React 合成事件中,
setState
是异步; - 在
setTimeout
或者原生 DOM 事件中,setState
是同步;
React中其实是通过一个函数来确定的:enqueueSetState
部分实现(react-reconciler/ReactFiberClassComponent.js)
enqueueSetState(inst, payload, callback) {const fiber = getInstance(inst);// 会根据React上下文计算一个当前时间const currentTime = requestCurrentTimeForUpdate();const suspenseConfig = requestCurrentSuspenseConfig();// 这个函数会返回当前是同步还是异步更新(准确的说是优先级)const expirationTime = computeExpirationForFiber(currentTime,fiber,suspenseConfig,);const update = createUpdate(expirationTime, suspenseConfig);...
}
computeExpirationForFiber
函数的部分实现:
Sync
是优先级最高的,即创建就更新;
export function computeExpirationForFiber(currentTime: ExpirationTime,fiber: Fiber,suspenseConfig: null | SuspenseConfig,
): ExpirationTime {const mode = fiber.mode;if ((mode & BlockingMode) === NoMode) {return Sync;}const priorityLevel = getCurrentPriorityLevel();if ((mode & ConcurrentMode) === NoMode) {return priorityLevel === ImmediatePriority ? Sync : Batched;}
setState的合并
数据的合并
假如我们有这样的数据:
this.state = {name: "coderwhy",message: "Hello World"
}
我们需要更新message
:
changeText() {this.setState({message: "你好啊,李银河"});
}
- 我通过
setState
去修改message
,是不会对name
产生影响的;
为什么不会产生影响呢?源码中其实是有对 原对象 和 新对象 进行合并的:
- 事实上就是使用
Object.assign(target, ...sources)
来完成的;
多个setState合并
比如我们还是有一个counter
属性,记录当前的数字:
- 如果进行如下操作,那么
counter
会变成几呢?答案是1
; - 为什么呢?因为它会对多个
state
进行合并;
increment() {this.setState({counter: this.state.counter + 1});this.setState({counter: this.state.counter + 1});this.setState({counter: this.state.counter + 1});}
其实在源码的processUpdateQueue
中有一个do...while
循环,就是从队列中取出多个state
进行合并的;
如何可以做到,让counter
最终变成3
呢?
increment() {this.setState((state, props) => {return {counter: state.counter + 1}})this.setState((state, props) => {return {counter: state.counter + 1}})this.setState((state, props) => {return {counter: state.counter + 1}})
}
为什么传入一个函数就可以变出3呢?
- 原因是多个
state
进行合并时,每次遍历,都会执行一次函数:
三. setState性能优化
React更新机制
我们在前面已经学习React的渲染流程:
那么React的更新流程呢?
React在props
或state
发生改变时,会调用React的render
方法,会创建一颗不同的树。
React需要基于这两颗不同的树之间的差别来判断如何有效的更新UI:
- 如果一棵树参考另外一棵树进行完全比较更新,那么即使是最先进的算法,该算法的复杂程度为 O(n^3 ),其中 n 是树中元素的数量;
- https://grfia.dlsi.ua.es/ml/algorithms/references/editsurvey_bille.pdf;
- 如果在 React 中使用了该算法,那么展示 1000 个元素所需要执行的计算量将在十亿的量级范围;
- 这个开销太过昂贵了,React的更新性能会变得非常低效;
于是,React对这个算法进行了优化,将其优化成了O(n),如何优化的呢?
- 同层节点之间相互比较,不会垮节点比较;
- 不同类型的节点,产生不同的树结构;
- 开发中,可以通过
key
来指定哪些节点在不同的渲染下保持稳定;
Diffing算法
1. 对比不同类型的元素
当节点为不同的元素,React会拆卸原有的树,并且建立起新的树:
- 当一个元素从
<a>
变成<img>
,从<Article>
变成<Comment>
,或从<Button>
变成<div>
都会触发一个完整的重建流程; - 当卸载一棵树时,对应的DOM节点也会被销毁,组件实例将执行
componentWillUnmount()
方法; - 当建立一棵新的树时,对应的 DOM 节点会被创建以及插入到 DOM 中,组件实例将执行
componentWillMount()
方法,紧接着componentDidMount()
方法;
比如下面的代码更改:
- React 会销毁
Counter
组件并且重新装载一个新的组件,而不会对Counter
进行复用;
<div><Counter />
</div><span><Counter />
</span>
2. 对比同一类型的元素
当比对两个相同类型的 React 元素时,React 会保留 DOM 节点,仅比对及更新有改变的属性。
比如下面的代码更改:
- 通过比对这两个元素,React 知道只需要修改 DOM 元素上的
className
属性;
<div className="before" title="stuff" />
<div className="after" title="stuff" />
比如下面的代码更改:
- 当更新
style
属性时,React 仅更新有所更变的属性。 - 通过比对这两个元素,React 知道只需要修改 DOM 元素上的
color
样式,无需修改fontWeight
。
<div style={{color: 'red', fontWeight: 'bold'}} />
<div style={{color: 'green', fontWeight: 'bold'}} />
如果是同类型的组件元素:
- 组件会保持不变,React会更新该组件的
props
,并且调用componentWillReceiveProps()
和componentWillUpdate()
方法; - 下一步,调用
render()
方法,diff 算法将在之前的结果以及新的结果中进行递归;
3. 对子节点进行递归
在默认条件下,当递归 DOM 节点的子元素时,React 会同时遍历两个子元素的列表;当产生差异时,生成一个 mutation
。
我们来看一下在最后插入一条数据的情况:
<ul><li>first</li><li>second</li>
</ul><ul><li>first</li><li>second</li><li>third</li>
</ul>
- 前面两个比较是完全相同的,所以不会产生
mutation
; - 最后一个比较,产生一个
mutation
,将其插入到新的DOM树中即可;
但是如果我们是在中间插入一条数据:
<ul><li>星际穿越</li><li>盗梦空间</li>
</ul><ul><li>大话西游</li><li>星际穿越</li><li>盗梦空间</li>
</ul>
- React会对每一个子元素产生一个
mutation
,而不是保持<li>星际穿越</li>
和<li>盗梦空间</li>
的不变; - 这种低效的比较方式会带来一定的性能问题;
keys的优化
我们在前面遍历列表时,总是会提示一个警告,让我们加入一个key
属性:
我们来看一个案例:
import React, { Component } from 'react'export default class App extends Component {constructor(props) {super(props);this.state = {movies: ["星际穿越", "盗梦空间"]}}render() {return (<div><h2>电影列表</h2><ul>{this.state.movies.map((item, index) => {return <li>{item}</li>})}</ul><button onClick={e => this.insertMovie()}>插入数据</button></div>)}insertMovie() {}
}
方式一:在最后位置插入数据
- 这种情况,有无
key
意义并不大
insertMovie() {const newMovies = [...this.state.movies, "大话西游"];this.setState({movies: newMovies})
}
方式二:在前面插入数据
- 这种做法,在没有
key
的情况下,所有的li
都需要进行修改;
insertMovie() {const newMovies = ["大话西游", ...this.state.movies];this.setState({movies: newMovies})
}
当子元素(这里的li
)拥有 key
时,React 使用 key
来匹配原有树上的子元素以及最新树上的子元素:
- 在下面这种场景下,
key
为111
和222
的元素仅仅进行位移,不需要进行任何的修改; - 将
key
为333
的元素插入到最前面的位置即可;
<ul><li key="111">星际穿越</li><li key="222">盗梦空间</li>
</ul><ul><li key="333">Connecticut</li><li key="111">星际穿越</li><li key="222">盗梦空间</li>
</ul>
key
的注意事项:
key
应该是唯一的;key
不要使用随机数(随机数在下一次render
时,会重新生成一个数字);- 使用
index
作为key
,对性能是没有优化的;
SCU的优化(shouldComponentUpdate)
render函数被调用
我们使用之前的一个嵌套案例:
import React, { Component } from 'react';function Header() {console.log("Header Render 被调用");return <h2>Header</h2>
}class Main extends Component {render() {console.log("Main Render 被调用");return (<div><Banner/><ProductList/></div>)}
}function Banner() {console.log("Banner Render 被调用");return <div>Banner</div>
}function ProductList() {console.log("ProductList Render 被调用");return (<ul><li>商品1</li><li>商品2</li><li>商品3</li><li>商品4</li><li>商品5</li></ul>)
}function Footer() {console.log("Footer Render 被调用");return <h2>Footer</h2>
}export default class App extends Component {constructor(props) {super(props);this.state = {counter: 0}}render() {console.log("App Render 被调用");return (<div><h2>当前计数: {this.state.counter}</h2><button onClick={e => this.increment()}>+1</button><Header/><Main/><Footer/></div>)}increment() {this.setState({counter: this.state.counter + 1})}
}
- 在
App
中,我们增加了一个计数器的代码; - 当点击
+1
时,会重新调用App
的render
函数; - 而当
App
的render
函数被调用时,所有的子组件的render
函数都会被重新调用;
那么,我们可以思考一下,在以后的开发中,我们只要是修改了App
中的数据,所有的组件都需要重新render
,进行diff
算法,性能必然是很低的:
- 事实上,很多的组件没有必须要重新
render
; - 它们调用
render
应该有一个前提,就是依赖的数据(state、props
)发生改变时,再调用自己的render
方法;
如何来控制render
方法是否被调用呢?
- 通过
shouldComponentUpdate
方法即可;
shouldComponentUpdate
React给我们提供了一个生命周期方法 shouldComponentUpdate
(很多时候,我们简称为SCU),这个方法接受参数,并且需要有返回值:
该方法有两个参数:
- 参数一:
nextProps
修改之后,最新的props
属性 - 参数二:
nextState
修改之后,最新的state
属性
该方法返回值是一个boolean
类型
- 返回值为
true
,那么就需要调用render
方法; - 返回值为
false
,那么就不需要调用render
方法; - 默认返回的是
true
,也就是只要state
发生改变,就会调用render
方法;
shouldComponentUpdate(nextProps, nextState) {return true;
}
我们可以控制它返回的内容,来决定是否需要重新渲染。
比如我们在App
中增加一个message
属性:
export default class App extends Component {constructor(props) {super(props);this.state = {counter: 0,message: "Hello World"}}render() {console.log("App Render 被调用");return (<div><h2>当前计数: {this.state.counter}</h2><button onClick={e => this.increment()}>+1</button><button onClick={e => this.changeText()}>改变文本</button><Header/><Main/><Footer/></div>)}increment() {this.setState({counter: this.state.counter + 1})}changeText() {this.setState({message: "你好啊,李银河"})}
}
jsx
中并没有依赖这个message
,那么它的改变不应该引起重新渲染;- 但是因为
render
监听到state
的改变,就会重新render
,所以最后render
方法还是被重新调用了;
这个时候,我们可以通过实现shouldComponentUpdate
来决定要不要重新调用render
方法:
shouldComponentUpdate(nextProps, nextState) {if (nextState.counter !== this.state.counter) {return true;}return false;
}
- 这个时候,我们改变
counter
时,会重新渲染; - 如果,我们改变的是
message
,那么默认返回的是false
,那么就不会重新渲染;
但是我们的代码依然没有优化到最好,因为当counter
改变时,所有的子组件依然重新渲染了:
- 所以,事实上,我们应该实现所有的子组件的
shouldComponentUpdate
;
比如Main
组件,可以进行如下实现:
class Main extends Component {shouldComponentUpdate(nextProps, nextState) {return false;}render() {console.log("Main Render 被调用");return (<div><Banner/><ProductList/></div>)}
}
shouldComponentUpdate
默认返回一个false
;- 在特定情况下,需要更新时,我们在上面添加对应的条件即可;
PureComponent 和 memo
如果所有的类,我们都需要手动来实现 shouldComponentUpdate
,那么会给我们开发者增加非常多的工作量。
我们来设想一下shouldComponentUpdate
中的各种判断的目的是什么?
props
或者state
中的数据是否发生了改变,来决定shouldComponentUpdate
返回true
或者false
;
事实上React已经考虑到了这一点,所以React已经默认帮我们实现好了,如何实现呢?
- 将
class
继承自PureComponent
。
比如我们修改Main
组件的代码:
class Main extends PureComponent {render() {console.log("Main Render 被调用");return (<div><Banner/><ProductList/></div>)}
}
PureComponent的原理是什么呢?
- 对
props
和state
进行浅层比较;
查看PureComponent
相关的源码:
react/ReactBaseClasses.js
中:
- 在
PureComponent
的原型上增加一个isPureReactComponent
为true
的属性
React-reconcilier/ReactFiberClassComponent.js
:
这个方法中,调用 !shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState)
,这个shallowEqual
就是进行浅层比较:
那么,如果是一个函数式组件呢?
我们需要使用一个高阶组件memo
:
- 我们将之前的
Header、Banner、ProductList
都通过memo
函数进行一层包裹; Footer
没有使用memo
函数进行包裹;- 最终的效果是,当
counter
发生改变时,Header、Banner、ProductList
的函数不会重新执行,而Footer
的函数会被重新执行;
import React, { Component, PureComponent, memo } from 'react';const MemoHeader = memo(function() {console.log("Header Render 被调用");return <h2>Header</h2>
})class Main extends PureComponent {render() {console.log("Main Render 被调用");return (<div><MemoBanner/><MemoProductList/></div>)}
}const MemoBanner = memo(function() {console.log("Banner Render 被调用");return <div>Banner</div>
})const MemoProductList = memo(function() {console.log("ProductList Render 被调用");return (<ul><li>商品1</li><li>商品2</li><li>商品3</li><li>商品4</li><li>商品5</li></ul>)
})function Footer() {console.log("Footer Render 被调用");return <h2>Footer</h2>
}export default class App extends Component {constructor(props) {super(props);this.state = {counter: 0,message: "Hello World"}}render() {console.log("App Render 被调用");return (<div><h2>当前计数: {this.state.counter}</h2><button onClick={e => this.increment()}>+1</button><button onClick={e => this.changeText()}>改变文本</button><MemoHeader/><Main/><Footer/></div>)}increment() {this.setState({counter: this.state.counter + 1})}shouldComponentUpdate(nextProps, nextState) {if (nextState.counter !== this.state.counter) {return true;}return false;}changeText() {this.setState({message: "你好啊,李银河"})}
}
memo
的原理是什么呢?
react/memo.js
:
- 最终返回一个对象,这个对象中有一个
compare
函数
- 默认是进行了浅比较
不可变数据的力量
我们通过一个案例来演练我们之前说的不可变数据的重要性:
import React, { PureComponent } from 'react'export default class App extends PureComponent {constructor(props) {super(props);this.state = {friends: [{ name: "lilei", age: 20, height: 1.76 },{ name: "lucy", age: 18, height: 1.65 },{ name: "tom", age: 30, height: 1.78 }]}}render() {return (<div><h2>朋友列表</h2><ul>{this.state.friends.map((item, index) => {return (<li key={item.name}><span>{`姓名:${item.name} 年龄: ${item.age}`}</span><button onClick={e => this.incrementAge(index)}>年龄+1</button></li>)})}</ul><button onClick={e => this.insertFriend()}>添加新数据</button></div>)}insertFriend() {}incrementAge(index) {}
}
我们来思考一下inertFriend
应该如何实现?
实现方式一:
insertFriend() {this.state.friends.push({name: "why", age: 18, height: 1.88});this.setState({friends: this.state.friends})
}
- 这种方式会造成界面不会发生刷新,添加新的数据;
- 原因是继承自
PureComponent
,会进行浅层比较,浅层比较过程中两个friends
是相同的对象;
实现方式二:
insertFriend() {this.setState({friends: [...this.state.friends, {name: "why", age: 18, height: 1.88}]})
}
[...this.state.friends, {name: "why", age: 18, height: 1.88}]
会生成一个新的数组引用;- 在进行浅层比较时,两个引用的是不同的数组,所以它们是不相同的;
我们再来思考一下incrementAge
应该如何实现?
实现方式一:
incrementAge(index) {this.state.friends[index].age += 1;this.setState({friends: this.state.friends})
}
- 和上面方式一类似
实现方式二:
incrementAge(index) {const newFriends = [...this.state.friends];newFriends[index].age += 1;this.setState({friends: newFriends})
}
- 和上面方式二类似
所以,在真实开发中,我们要尽量保证state、props
中的数据不可变性,这样我们才能合理和安全的使用PureComponent
和memo
。
当然,后面项目中我会结合immutable.js
来保证数据的不可变性。
总之,更新state的原则是,不要直接修改state中的原始对象或数组,而是要通过新创建一个对象或数组的拷贝,在拷贝的对象上进行修改之后,再通过setState
设置更新给state中的原始对象。
官网对 shouldComponentUpdate 的描述
四. React的脚手架:create-react-app
create-react-app 创建项目以及 npm、yarn 包管理工具的使用 参考这里
快捷创建react应用的脚手架命令:
启动React项目
编译React项目
五. React开发依赖
认识 Babel
引入 React
相关文章:

【React系列】React生命周期、setState深入理解、 shouldComponentUpdate和PureComponent性能优化、脚手架
本文来自#React系列教程:https://mp.weixin.qq.com/mp/appmsgalbum?__bizMzg5MDAzNzkwNA&actiongetalbum&album_id1566025152667107329) 一. 生命周期 1.1. 认识生命周期 很多的事物都有从创建到销毁的整个过程,这个过程称之为是生命周期&…...

一文初步了解slam技术
本文初步介绍slam技术,主要是slam技术的概述,涉及技术原理、应用场景、分类、以及各自优缺点,和slam技术的未来展望。 🎬个人简介:一个全栈工程师的升级之路! 📋个人专栏:slam精进之…...

滑动窗口协议仿真(2024)
1.题目描述 滑动窗口协议以基于分组的数据传输协议为特征,该协议适用于在数据链路层以及传输层中对按 顺序传送分组的可靠性要求较高的环境。在长管道传输过程(特别是无线环境)中,相应的滑动窗口 协议可实现高效的重传恢复。附录 …...
uniapp上传文件时用到的api是什么?格式是什么?
在UniApp中,你可以使用uni.uploadFile()方法来上传文件。这是一个异步方法,用于将本地资源上传到服务器。 该方法的基本格式如下: uni.uploadFile({url: 上传接口地址,filePath: 要上传的文件路径,name: 后端接收的文件参数名,formData: {/…...

Java面试——框架篇
1、Spring框架中的单例bean是线程安全的吗? 所谓单例就是所有的请求都用一个对象来处理,而多例则指每个请求用一个新的对象来处理。 结论:线程不安全。 Spring框架中有一个Scope注解,默认的值就是singleton,单例的。一…...

GO语言笔记1-安装与hello world
SDK开发工具包下载 Go语言官网地址:golang.org,无法访问Golang中文社区:首页 - Go语言中文网 - Golang中文社区下载地址:Go下载 - Go语言中文网 - Golang中文社区 尽量去下载稳定版本,根据使用系统下载压缩包格式的安装…...

指针传参误区
C语言中指针作为形参传递时,func(*a, *b) 这种形式的话,是无法通过简单的 ab来修改的,在函数体内a的地址确实被修改成b的地址了,但是当函数执行结束时,a的地址会重新回到原本的地址里面…...
力扣-42.接雨水
题目: 给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。 示例 1: 输入:height [0,1,0,2,1,0,1,3,2,1,2,1] 输出:6 解释:上面是由数组[0,1,0,2…...

LeetCode-移动零(283)
题目描述: 给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。 请注意 ,必须在不复制数组的情况下原地对数组进行操作。 思路: 这里的思路跟以前做过的去重复数字的思路有点像&…...

文件系统与日志分析
一,文件系统 (一)inode 和block概述 1,文件数据包括元信息与实际数据 2,文件存储在硬盘上,硬盘最小存储单位是“扇区”,每个扇区存储512字节 3,block (块) 连续的八个扇区组成一…...

labview 与三菱FX 小型PLC通信(OPC)
NI OPC服务器与三菱FX3U PLC通讯方法 一、新建通道名称为:MIT 二、选择三菱FX系列 三、确认端口号相关的参数(COM端:7.波特率:9600,数据位:7,校验:奇校验,停止位…...
掌握Linux网络配置:价格亲民,操作简便!
前言 在Linux系统中,网络配置是实现连接、通信和安全的重要一环。无论你是初学者还是有经验的用户,掌握网络配置命令能帮助你轻松管理网络接口、设置IP地址以及查看连接状态。以下是一些关键命令和示例,让你快速掌握网络操作的精髓ÿ…...

郑州大学算法设计与分析实验2
判断题 1 #include<bits/stdc.h> using namespace std;const int N 50; int f[N], n;int main() { // freopen("1.in", "r", stdin);ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);cin >> n;f[1] 1; f[2] 1;for(int i 3; i &l…...

【CMake】1. VSCode 开发环境安装与运行
CMake 示例工程代码 https://github.com/LABELNET/cmake-simple 插件 使用 VSCode 开发C项目,安装 CMake 插件 CMakeCMake ToolsCMake Language Support (建议,语法提示) 1. 配置 CMake Language Support , Windows 配置 donet 环境 这…...
使用vue3+<script setup>+element-plus中el-table前端切片完成分页效果
<template><div><el-table :data"visibleData" :row-key"row > row.id"><el-table-column prop"name" label"姓名"></el-table-column><el-table-column prop"age" label"年龄&qu…...
vue 中 computed 和 watch 的区别
在Vue中,computed和watch都是用于监听数据的变化,并且根据变化做出相应的反应。 computed是一个计算属性,它会根据依赖的数据的变化自动计算得出一个新的值,并且具有缓存的特性。当依赖的数据发生变化时,computed属性…...

gephi——graphviz插件设置
gephi_graphviz插件设置 以下是我总结出来的一点经验 1. 安装graphviz软件,请见作者其他博客 2. 安装gephi 插件,并激活 3. 运行graphviz布局,会遇到找不到dot问题 问题描述:Graphviz process error X There was an error launc…...

wireshark抓包分析HTTP协议,HTTP协议执行流程,
「作者主页」:士别三日wyx 「作者简介」:CSDN top100、阿里云博客专家、华为云享专家、网络安全领域优质创作者 「推荐专栏」:对网络安全感兴趣的小伙伴可以关注专栏《网络安全入门到精通》 使用WireShark工具抓取「HTTP协议」的数据包&#…...

Linux第13步_安装“vim编辑器”及应用介绍
学习“磁盘重新分区”后,嵌入式Linux系统环境搭建进入安装“vim编辑器”这个环节。vim编辑器可以用来修改文件,在后期使用中,会经常用到。 1、安装“vim编辑器” 输入“sudo apt-get install vim回车”,就可以执行安装“vim编辑…...

Yapi安装配置(CentOs)
环境要求 nodejs(7.6) mongodb(2.6) git 准备工作 清除yum命令缓存 sudo yum clean all卸载低版本nodejs yum remove nodejs npm -y安装nodejs,获取资源,安装高版本nodejs curl -sL https://rpm.nodesource.com/setup_8.x | bash - #安装 s…...

网络编程(Modbus进阶)
思维导图 Modbus RTU(先学一点理论) 概念 Modbus RTU 是工业自动化领域 最广泛应用的串行通信协议,由 Modicon 公司(现施耐德电气)于 1979 年推出。它以 高效率、强健性、易实现的特点成为工业控制系统的通信标准。 包…...
生成xcframework
打包 XCFramework 的方法 XCFramework 是苹果推出的一种多平台二进制分发格式,可以包含多个架构和平台的代码。打包 XCFramework 通常用于分发库或框架。 使用 Xcode 命令行工具打包 通过 xcodebuild 命令可以打包 XCFramework。确保项目已经配置好需要支持的平台…...
进程地址空间(比特课总结)
一、进程地址空间 1. 环境变量 1 )⽤户级环境变量与系统级环境变量 全局属性:环境变量具有全局属性,会被⼦进程继承。例如当bash启动⼦进程时,环 境变量会⾃动传递给⼦进程。 本地变量限制:本地变量只在当前进程(ba…...

使用分级同态加密防御梯度泄漏
抽象 联邦学习 (FL) 支持跨分布式客户端进行协作模型训练,而无需共享原始数据,这使其成为在互联和自动驾驶汽车 (CAV) 等领域保护隐私的机器学习的一种很有前途的方法。然而,最近的研究表明&…...
CRMEB 框架中 PHP 上传扩展开发:涵盖本地上传及阿里云 OSS、腾讯云 COS、七牛云
目前已有本地上传、阿里云OSS上传、腾讯云COS上传、七牛云上传扩展 扩展入口文件 文件目录 crmeb\services\upload\Upload.php namespace crmeb\services\upload;use crmeb\basic\BaseManager; use think\facade\Config;/*** Class Upload* package crmeb\services\upload* …...
【JavaSE】绘图与事件入门学习笔记
-Java绘图坐标体系 坐标体系-介绍 坐标原点位于左上角,以像素为单位。 在Java坐标系中,第一个是x坐标,表示当前位置为水平方向,距离坐标原点x个像素;第二个是y坐标,表示当前位置为垂直方向,距离坐标原点y个像素。 坐标体系-像素 …...

Redis数据倾斜问题解决
Redis 数据倾斜问题解析与解决方案 什么是 Redis 数据倾斜 Redis 数据倾斜指的是在 Redis 集群中,部分节点存储的数据量或访问量远高于其他节点,导致这些节点负载过高,影响整体性能。 数据倾斜的主要表现 部分节点内存使用率远高于其他节…...

Unsafe Fileupload篇补充-木马的详细教程与木马分享(中国蚁剑方式)
在之前的皮卡丘靶场第九期Unsafe Fileupload篇中我们学习了木马的原理并且学了一个简单的木马文件 本期内容是为了更好的为大家解释木马(服务器方面的)的原理,连接,以及各种木马及连接工具的分享 文件木马:https://w…...

【分享】推荐一些办公小工具
1、PDF 在线转换 https://smallpdf.com/cn/pdf-tools 推荐理由:大部分的转换软件需要收费,要么功能不齐全,而开会员又用不了几次浪费钱,借用别人的又不安全。 这个网站它不需要登录或下载安装。而且提供的免费功能就能满足日常…...
QT3D学习笔记——圆台、圆锥
类名作用Qt3DWindow3D渲染窗口容器QEntity场景中的实体(对象或容器)QCamera控制观察视角QPointLight点光源QConeMesh圆锥几何网格QTransform控制实体的位置/旋转/缩放QPhongMaterialPhong光照材质(定义颜色、反光等)QFirstPersonC…...