当前位置: 首页 > news >正文

【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. 不常用生命周期

除了上面介绍的生命周期函数之外,还有一些不常用的生命周期函数:

在这里插入图片描述

  • getDerivedStateFromPropsstate 的值在任何时候都依赖于 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。

不可变数据的力量

避免该问题最简单的方式是避免更改你正用于propsstate的值。例如,上面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 是对应用变化时组件状态的引用。当然,它不应直接被修改。
  • 你应该使用基于 stateprops 构建的新对象来表示变化。

例如,假设我们想根据 props.step 来增加 state

this.setState((state, props) => {return {counter: state.counter + props.step}; 
});

updater函数中接收的stateprops都保证为最新。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函数,那么stateprops不能保持同步
    • stateprops不能保持一致性,会在开发中产生很多的问题;

那么如何可以获取到更新后的值呢?

  • 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在propsstate发生改变时,会调用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 来匹配原有树上的子元素以及最新树上的子元素:

  • 在下面这种场景下,key111222的元素仅仅进行位移,不需要进行任何的修改;
  • key333的元素插入到最前面的位置即可;
<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时,会重新调用Apprender函数;
  • 而当Apprender函数被调用时,所有的子组件的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的原理是什么呢?

  • propsstate进行浅层比较;

查看PureComponent相关的源码:

react/ReactBaseClasses.js中:

在这里插入图片描述

  • PureComponent的原型上增加一个isPureReactComponenttrue的属性

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中的数据不可变性,这样我们才能合理和安全的使用PureComponentmemo

当然,后面项目中我会结合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系列教程&#xff1a;https://mp.weixin.qq.com/mp/appmsgalbum?__bizMzg5MDAzNzkwNA&actiongetalbum&album_id1566025152667107329) 一. 生命周期 1.1. 认识生命周期 很多的事物都有从创建到销毁的整个过程&#xff0c;这个过程称之为是生命周期&…...

一文初步了解slam技术

本文初步介绍slam技术&#xff0c;主要是slam技术的概述&#xff0c;涉及技术原理、应用场景、分类、以及各自优缺点&#xff0c;和slam技术的未来展望。 &#x1f3ac;个人简介&#xff1a;一个全栈工程师的升级之路&#xff01; &#x1f4cb;个人专栏&#xff1a;slam精进之…...

滑动窗口协议仿真(2024)

1.题目描述 滑动窗口协议以基于分组的数据传输协议为特征&#xff0c;该协议适用于在数据链路层以及传输层中对按 顺序传送分组的可靠性要求较高的环境。在长管道传输过程&#xff08;特别是无线环境&#xff09;中&#xff0c;相应的滑动窗口 协议可实现高效的重传恢复。附录 …...

uniapp上传文件时用到的api是什么?格式是什么?

在UniApp中&#xff0c;你可以使用uni.uploadFile()方法来上传文件。这是一个异步方法&#xff0c;用于将本地资源上传到服务器。 该方法的基本格式如下&#xff1a; uni.uploadFile({url: 上传接口地址,filePath: 要上传的文件路径,name: 后端接收的文件参数名,formData: {/…...

Java面试——框架篇

1、Spring框架中的单例bean是线程安全的吗&#xff1f; 所谓单例就是所有的请求都用一个对象来处理&#xff0c;而多例则指每个请求用一个新的对象来处理。 结论&#xff1a;线程不安全。 Spring框架中有一个Scope注解&#xff0c;默认的值就是singleton&#xff0c;单例的。一…...

GO语言笔记1-安装与hello world

SDK开发工具包下载 Go语言官网地址&#xff1a;golang.org&#xff0c;无法访问Golang中文社区&#xff1a;首页 - Go语言中文网 - Golang中文社区下载地址&#xff1a;Go下载 - Go语言中文网 - Golang中文社区 尽量去下载稳定版本&#xff0c;根据使用系统下载压缩包格式的安装…...

指针传参误区

C语言中指针作为形参传递时&#xff0c;func&#xff08;*a, *b&#xff09; 这种形式的话&#xff0c;是无法通过简单的 ab来修改的&#xff0c;在函数体内a的地址确实被修改成b的地址了&#xff0c;但是当函数执行结束时&#xff0c;a的地址会重新回到原本的地址里面&#xf…...

力扣-42.接雨水

题目&#xff1a; 给定 n 个非负整数表示每个宽度为 1 的柱子的高度图&#xff0c;计算按此排列的柱子&#xff0c;下雨之后能接多少雨水。 示例 1&#xff1a; 输入&#xff1a;height [0,1,0,2,1,0,1,3,2,1,2,1] 输出&#xff1a;6 解释&#xff1a;上面是由数组[0,1,0,2…...

LeetCode-移动零(283)

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

文件系统与日志分析

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

labview 与三菱FX 小型PLC通信(OPC)

NI OPC服务器与三菱FX3U PLC通讯方法 一、新建通道名称为&#xff1a;MIT 二、选择三菱FX系列 三、确认端口号相关的参数&#xff08;COM端&#xff1a;7.波特率&#xff1a;9600&#xff0c;数据位&#xff1a;7&#xff0c;校验&#xff1a;奇校验&#xff0c;停止位&#xf…...

掌握Linux网络配置:价格亲民,操作简便!

前言 在Linux系统中&#xff0c;网络配置是实现连接、通信和安全的重要一环。无论你是初学者还是有经验的用户&#xff0c;掌握网络配置命令能帮助你轻松管理网络接口、设置IP地址以及查看连接状态。以下是一些关键命令和示例&#xff0c;让你快速掌握网络操作的精髓&#xff…...

郑州大学算法设计与分析实验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项目&#xff0c;安装 CMake 插件 CMakeCMake ToolsCMake Language Support &#xff08;建议&#xff0c;语法提示) 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中&#xff0c;computed和watch都是用于监听数据的变化&#xff0c;并且根据变化做出相应的反应。 computed是一个计算属性&#xff0c;它会根据依赖的数据的变化自动计算得出一个新的值&#xff0c;并且具有缓存的特性。当依赖的数据发生变化时&#xff0c;computed属性…...

gephi——graphviz插件设置

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

wireshark抓包分析HTTP协议,HTTP协议执行流程,

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

Linux第13步_安装“vim编辑器”及应用介绍

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

Yapi安装配置(CentOs)

环境要求 nodejs&#xff08;7.6) mongodb&#xff08;2.6&#xff09; 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…...

HCIA-Datacom题库(自己整理分类的)_08_FTP协议【8道题】

一、单选 1.在使用FTP协议升级路由器软件时&#xff0c;传输模式应该选用___ 二进制模式 字节模式 文字模式 流字节模式 解析&#xff1a;二进制模式&#xff1a;在数据连接中传输&#xff0c;不对数据进行任何处理&#xff0c;不需要转换或格式化就可以传输字符。 2.以…...

【开源GPT项目 - 在问】让知识无界,智能触手可及

Chatanywhere: chatAnywhere 在问 | 让知识无界&#xff0c;智能触手可及 项目简介 这是一个免费的在线聊天工具&#xff0c;旨在让用户更方便地享受科技带来的便利。用户可以使用我们的工具来获取答案、寻求建议、进行翻译和计算等等。这是由一位个人开发者创建的&#xff…...

【2023 CCF 大数据与计算智能大赛】基于TPU平台实现超分辨率重建模型部署 基于Real-ESRGAN的TPU超分模型部署

2023 CCF 大数据与计算智能大赛 《基于TPU平台实现超分辨率重建模型部署》 洋洋很棒 李鹏飞 算法工程师 中国-烟台 2155477673qq.com 团队简介 本人从事工业、互联网场景传统图像算法及深度学习算法开发、部署工作。其中端侧算法开发及部署工作5年时间。 摘要 本文是…...

Vue中的组件通信方式及应用场景

在Vue中&#xff0c;组件通信有以下几种方式&#xff1a; Props / $emit&#xff1a;父组件通过给子组件传递props属性&#xff0c;子组件通过$emit事件将数据传递给父组件。适用于父组件向子组件传递数据。 自定义事件&#xff1a;父组件通过$on监听子组件触发的事件&#xf…...

RA8900CE汽车用c总线接口实时时钟模块

汽车用c总线接口实时时钟模块内置调频32.768 kHz晶体单元和DTCXO&#xff0c;高稳定性和电源切换。 接口类型我 2C-Bus接口(400kHz)界面电压范围2.5V ~ 5.5V温度补偿电压范围2.0V至5.5V计时电压范围1.6V ~ 5.5V可选时钟输出(32.768 kHz, 1024 Hz, 1 Hz)各种功能齐全的日历、报…...

屏幕截图--Snagit

Snagit是一款优秀的屏幕、文本和视频捕获、编辑与转换软件。它不仅可以捕获静止的图像&#xff0c;还能获得动态的图像和声音。软件界面干净清爽&#xff0c;功能板块一目了然&#xff0c;为用户提供专业的屏幕录制方案。可以根据自己的需求调整录制视频的分辨率、帧数、输出格…...

PHP运行环境之宝塔Web站点部署

目录 Web站点部署流程(部署聚合支付后台) 项目文件 将上传的文件解压文件至根目录 修改站点关键信息 设置伪静态,选择:thinkphp,并保存 设置PHP,选择:70+版本即可。 安装网站 1设置运行目录...

使用高版本JDK编译低版本代码

背景 SonarQube运行于Java17&#xff0c;使用Sonar的Maven插件编译时&#xff0c;如果编译使用的JDK版本低于SonarQube使用的Java17&#xff0c;则会提示Java文件不匹配问题。 Error during SonarScanner execution java.lang.UnsupportedClassVersionError: org/sonar/batch/…...

Zuul相关问题及到案(2024)

1、什么是Zuul&#xff1f;它在微服务架构中有什么作用&#xff1f; Zuul是Netflix开源的一种提供API网关服务的应用程序&#xff0c;它在微服务架构中扮演着流量的前门角色。主要功能包括以下几点&#xff1a; 路由转发&#xff1a;Zuul网关将外部请求转发到具体的微服务实例…...

【CSS】讲一讲BFC、IFC、GFC、FFC

1. 前言 FC&#xff08;Formatting Contexts&#xff09;&#xff0c;是CSS2.1的一个概念&#xff0c;是页面中的一块渲染区域&#xff0c;具有一套渲染规则&#xff0c;决定FC中子元素如何定位&#xff0c;以及和其他元素的关系和相互作用。在说FC之前说一下文档流。 1.1. 普…...

济南黄河路桥建设集团官方网站/优秀企业网站欣赏

算法分类 首先&#xff0c;机器学习大的算法分为监督学习用途的算法和无监督学习用途的算法两大类。而监督学习的算法有k-近邻算法、朴素贝叶斯算法、支持向量机算法、决策树算法、线性回归算法、局部加权线性回归算法、Ridge回归算法、Lasso最小回归系数估计算法。而无监督学…...

湖南省内出行最新政策/湖南 seo

问题情境描述&#xff1a;在Spring的配置文件applicationContext.xml中配置扫描 XXXmapper.xml映射文件,配置扫描的路径&#xff0c;现在下面只扫描一个路径com/javafeng/mapping下的xml文件现在又有一个xml文件在另外一个com/inspur/NSRZCXXGR/mapping/*.xml路径下需要扫描。改…...

网站提交百度了经常修改网站/谷歌排名查询

Python程序包括格式框架、注释、变量、表达式、分支语句、循环语句、函数等语法元素。程序设计的6个步骤是利用计算机解决问题的方法步骤&#xff0c;程序设计语言则是解决问题的实现载体。1.程序的格式框架Python语言采用严格的“缩进”来表明程序的格式框架。缩进指每一行代码…...

网站后台使用/西安seo王尘宇

具体配置&#xff1a; 前题需要关闭iptables和SELinux 1、打开ssh配置文件&#xff0c;启用以下选项 vim /etc/ssh/sshd_config RSAAuthentication yes #允许RSA认证 PubkeyAuthentication yes #允许公钥认证 AuthorizedKeysFile .ssh/authorized_keys #公钥存放地址 …...

宠物主题网站模板/开发一个网站需要哪些技术

树与二叉树 1节1. 树的逻辑结构2. 树的物理结构3. 二叉树的逻辑结构4. 二叉树的物理结构4.1 顺序存储结构4.2 链式存储5. 二叉树的遍历5.1 先序遍历5.2 先序遍历代码实现5.3 中序遍历5.4 中序遍历代码实现5.5 后序遍历5.6 后序遍历代码6. 非递归的二叉树遍历6.0 辅助栈的实现6.…...

大莲网站建设公司/seo优化与sem推广有什么关系

文章目录expose()指定路径段基于请求方法路由expose() pecan.expose可以标记controller方法&#xff0c;使得该方法可以通过http访问。 pecan.expose定义如下&#xff1a; pecan.expose(templateNone, genericFalse, routeNone, **kw)template&#xff1a;模板的路径。还可以…...