【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

javascript 复制代码
constructor(props)

如果不初始化 state 或不进行方法绑定,则不需要为 React 组件实现构造函数。

constructor中通常只做两件事情:

  • 通过给 this.state 赋值对象来初始化内部的state
  • 为事件绑定实例(this);

componentDidMount

javascript 复制代码
componentDidMount()

componentDidMount() 会在组件挂载后(插入 DOM 树中)立即调用。

componentDidMount中通常进行哪里操作呢?

  • 依赖于DOM的操作可以在这里进行;
  • 在此处发送网络请求就最好的地方;(官方建议)
  • 可以在此处添加一些订阅(会在componentWillUnmount取消订阅);

componentDidUpdate

javascript 复制代码
componentDidUpdate(prevProps, prevState, snapshot)

componentDidUpdate() 会在更新后会被立即调用,首次渲染不会执行此方法。

  • 当组件更新后,可以在此处对 DOM 进行操作;
  • 如果你对更新前后的 props 进行了比较,也可以选择在此处进行网络请求;(例如,当 props 未发生变化时,则不会执行网络请求)。
javascript 复制代码
componentDidUpdate(prevProps) {
  // 典型用法(不要忘记比较 props):
  if (this.props.userID !== prevProps.userID) {
    this.fetchData(this.props.userID);
  }
}

componentWillUnmount

javascript 复制代码
componentWillUnmount()

componentWillUnmount() 会在组件卸载及销毁之前直接调用。

  • 在此方法中执行必要的清理操作;
  • 例如,清除 timer,取消网络请求或清除在 componentDidMount() 中创建的订阅等;

代码验证所有的生命周期函数:

javascript 复制代码
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

javascript 复制代码
constructor(props) {
	super(props);
	// 不要在这里调用 this.setState() 
	this.state = { counter: 0};
	this.handleClick = this.handleClick.bind(this); 
}

只能在构造函数中直接为 this.state 赋值。如需在其他方法中赋值,你应使用 this.setState() 替代。

你可以用类似的方式改写代码来避免可变对象的产生。例如,我们有一个叫做 colormap 的对象。我们希望写一个方法来将 colormap.right 设置为 'blue'。我们可以这么写:

javascript 复制代码
function updateColorMap(colormap) { 
	colormap.right = 'blue';
}

为了不改变原本的对象,我们可以使用 Object.assign 方法:

javascript 复制代码
function updateColorMap(colormap) {
	return Object.assign({}, colormap, {right: 'blue'});
}

现在 updateColorMap 返回了一个新的对象,而不是修改老对象。Object.assign 是ES6的方法,需要polyfill。

不可变数据的力量

避免该问题最简单的方式是避免更改你正用于propsstate的值。例如,上面handleClick 方法可以用 concat 重写:

javascript 复制代码
handleClick() {
	this.setState(state => ({
		words: state.words.concat(['marklar']) 
	}));
}

ES6数组支持扩展运算符 ,这让代码写起来更方便了。如果你在使用Create React App,该语法已经默认支持了。

javascript 复制代码
handleClick() {
	this.setState(state => ({
		words: [...state.words, 'marklar'], 
	}));
}; 

参数一为带有形式参数的 updater 函数:(state, props) => stateChange

  • state 是对应用变化时组件状态的引用。当然,它不应直接被修改。
  • 你应该使用基于 stateprops 构建的新对象来表示变化。

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

javascript 复制代码
this.setState((state, props) => {
	return {counter: state.counter + props.step}; 
});

updater函数中接收的stateprops都保证为最新。updater的返回值会与state进行浅合并。

setState异步更新

setState的更新是异步的?

javascript 复制代码
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设计为异步,可以显著的提升性能;
    • 如果每次调用 setState都进行一次更新,那么意味着render函数会被频繁调用,界面重新渲染,这样效率是很低的;
    • 最好的办法应该是获取到多个更新,之后进行批量更新
  • 如果同步更新了state,但是还没有执行render函数,那么stateprops不能保持同步
    • stateprops不能保持一致性,会在开发中产生很多的问题;

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

  • setState接受两个参数:第二个参数是一个回调函数,这个回调函数会在更新后会执行;
  • 格式如下:setState(partialState, callback)
javascript 复制代码
changeText() {
  this.setState({
    message: "你好啊,李银河"
  }, () => {
    console.log(this.state.message); // 你好啊,李银河
  });
}

当然,我们也可以在生命周期函数:

javascript 复制代码
componentDidUpdate(prevProps, provState, snapshot) {
  console.log(this.state.message);
}

setState一定是异步?

疑惑:setState一定是异步更新的吗?

验证一:在setTimeout中的更新:

javascript 复制代码
changeText() {
  setTimeout(() => {
    this.setState({
      message: "你好啊,李银河"
    });
    console.log(this.state.message); // 你好啊,李银河
  }, 0);
}

验证二:原生DOM事件:

javascript 复制代码
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)

javascript 复制代码
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是优先级最高的,即创建就更新;
javascript 复制代码
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的合并

数据的合并

假如我们有这样的数据:

javascript 复制代码
this.state = {
  name: "coderwhy",
  message: "Hello World"
}

我们需要更新message

javascript 复制代码
changeText() {
  this.setState({
    message: "你好啊,李银河"
  });
}
  • 我通过setState去修改message,是不会对name产生影响的;

为什么不会产生影响呢?源码中其实是有对 原对象新对象 进行合并的

  • 事实上就是使用 Object.assign(target, ...sources) 来完成的;

多个setState合并

比如我们还是有一个counter属性,记录当前的数字:

  • 如果进行如下操作,那么counter会变成几呢?答案是1
  • 为什么呢?因为它会对多个state进行合并;
javascript 复制代码
  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呢?

javascript 复制代码
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进行复用;
html 复制代码
<div>
  <Counter />
</div>

<span>
  <Counter />
</span>

2. 对比同一类型的元素

当比对两个相同类型的 React 元素时,React 会保留 DOM 节点,仅比对及更新有改变的属性

比如下面的代码更改:

  • 通过比对这两个元素,React 知道只需要修改 DOM 元素上的 className 属性;
html 复制代码
<div className="before" title="stuff" />
<div className="after" title="stuff" />

比如下面的代码更改:

  • 当更新 style 属性时,React 仅更新有所更变的属性。
  • 通过比对这两个元素,React 知道只需要修改 DOM 元素上的 color 样式,无需修改 fontWeight
javascript 复制代码
<div style={{color: 'red', fontWeight: 'bold'}} />
<div style={{color: 'green', fontWeight: 'bold'}} />

如果是同类型的组件元素:

  • 组件会保持不变,React会更新该组件的props,并且调用componentWillReceiveProps()componentWillUpdate() 方法;
  • 下一步,调用 render() 方法,diff 算法将在之前的结果以及新的结果中进行递归;

3. 对子节点进行递归

在默认条件下,当递归 DOM 节点的子元素时,React 会同时遍历两个子元素的列表;当产生差异时,生成一个 mutation

我们来看一下在最后插入一条数据的情况:

html 复制代码
<ul>
  <li>first</li>
  <li>second</li>
</ul>

<ul>
  <li>first</li>
  <li>second</li>
  <li>third</li>
</ul>
  • 前面两个比较是完全相同的,所以不会产生mutation
  • 最后一个比较,产生一个mutation,将其插入到新的DOM树中即可;

但是如果我们是在中间插入一条数据:

html 复制代码
<ul>
  <li>星际穿越</li>
  <li>盗梦空间</li>
</ul>

<ul>
  <li>大话西游</li>
  <li>星际穿越</li>
  <li>盗梦空间</li>
</ul>
  • React会对每一个子元素产生一个mutation,而不是保持 <li>星际穿越</li><li>盗梦空间</li>的不变;
  • 这种低效的比较方式会带来一定的性能问题;

keys的优化

我们在前面遍历列表时,总是会提示一个警告,让我们加入一个key属性:

我们来看一个案例:

javascript 复制代码
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意义并不大
javascript 复制代码
insertMovie() {
  const newMovies = [...this.state.movies, "大话西游"];
  this.setState({
    movies: newMovies
  })
}

方式二:在前面插入数据

  • 这种做法,在没有key的情况下,所有的li都需要进行修改;
javascript 复制代码
insertMovie() {
  const newMovies = ["大话西游", ...this.state.movies];
  this.setState({
    movies: newMovies
  })
}

当子元素(这里的li)拥有 key 时,React 使用 key 来匹配原有树上的子元素以及最新树上的子元素:

  • 在下面这种场景下,key111222的元素仅仅进行位移,不需要进行任何的修改;
  • key333的元素插入到最前面的位置即可;
html 复制代码
<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函数被调用

我们使用之前的一个嵌套案例:

javascript 复制代码
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方法;
javascript 复制代码
shouldComponentUpdate(nextProps, nextState) {
  return true;
}

我们可以控制它返回的内容,来决定是否需要重新渲染。

比如我们在App中增加一个message属性:

javascript 复制代码
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方法:

javascript 复制代码
shouldComponentUpdate(nextProps, nextState) {
  if (nextState.counter !== this.state.counter) {
    return true;
  }

  return false;
}
  • 这个时候,我们改变counter时,会重新渲染;
  • 如果,我们改变的是message,那么默认返回的是false,那么就不会重新渲染;

但是我们的代码依然没有优化到最好,因为当counter改变时,所有的子组件依然重新渲染了:

  • 所以,事实上,我们应该实现所有的子组件的shouldComponentUpdate

比如Main组件,可以进行如下实现:

javascript 复制代码
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组件的代码:

javascript 复制代码
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的函数会被重新执行;
javascript 复制代码
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函数
  • 默认是进行了浅比较

不可变数据的力量

我们通过一个案例来演练我们之前说的不可变数据的重要性:

javascript 复制代码
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应该如何实现?

实现方式一:

javascript 复制代码
insertFriend() {
  this.state.friends.push({name: "why", age: 18, height: 1.88});
  this.setState({
    friends: this.state.friends
  })
}
  • 这种方式会造成界面不会发生刷新,添加新的数据;
  • 原因是继承自PureComponent,会进行浅层比较,浅层比较过程中两个friends是相同的对象;

实现方式二:

javascript 复制代码
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应该如何实现?

实现方式一:

javascript 复制代码
incrementAge(index) {
  this.state.friends[index].age += 1;
  this.setState({
    friends: this.state.friends
  })
}
  • 和上面方式一类似

实现方式二:

javascript 复制代码
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

相关推荐
五点六六六3 小时前
前端常见的性能指标采集
前端·性能优化·架构
软件测试-阿涛4 小时前
【性能测试】Jmeter+Grafana+InfluxDB+Prometheus Windows安装部署教程
测试工具·jmeter·性能优化·压力测试·grafana·prometheus
海底火旺5 小时前
单页应用路由:从 Hash 到懒加载
前端·react.js·性能优化
鼠鼠我捏,要死了捏7 小时前
深入解析MongoDB分片原理与运维实践指南
mongodb·性能优化·sharding
拾光拾趣录8 小时前
内存泄漏的“隐形杀手”
前端·性能优化
友莘居士10 小时前
Dify中的Agent和发现和调用mcp工具两个节点调用的异同
agent·react·dify·functioncalling·mcp
鼠鼠我捏,要死了捏1 天前
基于Redisson实现高并发分布式锁性能优化实践指南
性能优化·分布式锁·redisson
笑衬人心。1 天前
后端项目中大量 SQL 执行的性能优化
sql·spring·性能优化
贵州晓智信息科技1 天前
Unity 性能优化全攻略
unity·性能优化·游戏引擎
UWA1 天前
UWA DAY 2025 游戏开发者大会|全议程
游戏·unity·性能优化·游戏开发·uwa·unreal engine