文章目录

  • 为什么要是用 setState
  • setState 异步更新
    • 为什么要异步更新
    • 如何获取更新后的值
    • 同步 or 异步
  • setState的合并
    • 数据的合并
    • 多个setState合并
  • setState性能优化
    • react 更新机制
    • diff算法的作用
      • 传统 diff算法
      • react diff算法
        • 对比不同类型的元素
        • 对比同一类型的元素
        • 对子节点进行递归
      • keys的优化
        • 在最后位置插入数据
        • 在前面插入数据

为什么要是用 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: "文本已经被改变"})}
}

当我们调用setState时,会重新执行render函数,根据最新的State来创建ReactElement对象;然后再根据最新的ReactElement对象,对DOM进行修改

另外,setState方法是从Component中继承过来的

setState 异步更新

changeText() {this.setState({message: "文本已经被改变"})console.log(this.state.message); // Hello World
}

上面代码中再使用 setState 方法后,再打印 message,发现 message 没有被改变,由此可以看到 setState 是异步操作,在执行完setState之后不能够立刻拿到最新的state的结果

为什么要异步更新

传送门

  • 设计成异步,可以显著提升性能,如果每次调用 setState都进行一次更新,那么意味着render函数会被频繁调用,界面重新渲染,这样效率是很低的,所以最好的办法是获取到多个更新,然后进行批量的更新
  • 如果同步更新了state,但是还没有执行render函数,那么state和props不能保持同步,这样做会引起很多问题

如何获取更新后的值

1、setState接受两个参数:第二个参数是一个回调函数,这个回调函数会在更新后会执行

changeText() {this.setState({message: "文本已经被改变"}, () => {console.log(this.state.message); // 文本已经被改变});
}

2、通过生命周期函数 componentDidUpdate

componentDidUpdate(prevProps, provState, snapshot) {console.log(this.state.message);
}

同步 or 异步

setState 方法到底是同步还是异步分两种情况

  • 在组件生命周期或React合成事件中,setState是异步

  • 在setTimeout或者原生dom事件中,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); // 文本已经被改变})
}

setState的合并

数据的合并

this.state = {name: '小冯',age: 19
}// 通过setState去修改age,是不会对name产生影响的
this.setState({age: 18
})

多个setState合并

this.state = {counter: 0
}increment() {this.setState({counter: this.state.counter + 1});this.setState({counter: this.state.counter + 1});this.setState({counter: this.state.counter + 1});}

setState 方法执行了3次,但是 counter 的值还是为1,这就是多个state进行合并

如果想要变为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}})
}

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来指定哪些节点在不同的渲染下保持稳定;

diff算法的作用

计算出虚拟 dom中真正变化的部分,并只针对该部分进行原生DOM操作,而非重新渲染整个页面

传统 diff算法

通过循环递归节点进行依次比较,复杂度为O(n^3) ,n为是树中节点(元素)的数量

如果在 React 中使用了该算法,那么展示 1000 个元素就需要进行上亿次比较,太浪费性能

传送门

react diff算法

  • 同层节点之间相互比较,不会跨节点比较;
  • 不同类型的节点,产生不同的树结构;
  • 开发中,可以通过key来指定哪些节点在不同的渲染下保持稳定;
对比不同类型的元素

当节点为不同的元素,React会拆卸原有的树,并且建立起新的树:

  • 当一个元素从 <a> 变成 <img>,从 <Article> 变成 <Comment>,或从 <Button> 变成<div> 都会触发一个完整的重建流程;
  • 当卸载一棵树时,对应的DOM节点也会被销毁,组件实例将执行componentWillUnmount() 方法;
  • 当建立一棵新的树时,对应的 DOM 节点会被创建以及插入到 DOM 中,组件实例将执行 componentWillMount() 方法,紧接着 componentDidMount() 方法;

比如下面的代码更改:

  • React 会销毁 Counter 组件并且重新装载一个新的组件,而不会对Counter进行复用;
<div><Counter />
</div><span><Counter />
</span>
对比同一类型的元素

当比对两个相同类型的 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 算法将在之前的结果以及新的结果中进行递归;
对子节点进行递归

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

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

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

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

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

推荐阅读1

推荐阅读2

keys的优化

在遍历数据进行渲染时,需要加 key 值,没有 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,对性能是没有优化的;

详解setState 方法相关推荐

  1. [深入浅出Cocoa]之消息(二)-详解动态方法决议(Dynamic Method Resolution)

    [深入浅出Cocoa]之消息(二)-详解动态方法决议(Dynamic Method Resolution) 罗朝辉 (http://www.cnblogs.com/kesalin/) 本文遵循&quo ...

  2. Java网络编程 Socket、ServerSocket 详解,方法介绍及完整代码示例

    Java网络编程 Socket.ServerSocket 详解,方法介绍及完整代码示例 概念 什么是网络编程? 网络编程是指编写运行在多个设备(计算机)的程序,这些设备通过网络连接起来.当这些通过网络 ...

  3. Swoole WebSocket服务使用Task任务详解 (面向对象方法)

    Swoole WebSocket服务使用Task任务详解 (面向对象方法) 作者: 绝爱七八丶 博客: https://blog.csdn.net/qq_40193451 日期: 2018.8.24 ...

  4. smali语言详解之方法的调用

    smali语言详解之方法的调用 一.方法调用关键字介绍 smali语言方法调用关键字主要有以下五种 invoke-virtual主要用于非私有实例方法的调用.实例方法指不是构造方法.父类方法等的属于这 ...

  5. 【数据结构】共享栈详解 判断共享栈满条件栈顶指针变化详解记忆方法例题

    摘要:简单易懂,详细地介绍共享栈概念,指针,判断共享栈栈满条件以及记忆方法等 目录 共享栈概念 栈顶指针&变化详解 栈顶指针种类的记忆方法 判断栈满条件 判断栈满条件的记忆方法 例题 解题思路 ...

  6. Day13-Java方法详解,方法的定义、重载,命令行传参,可变参数与递归

    Java方法详解 什么是方法? Java的方法是语句的集合,他们在一起执行一个功能 方法是解决一类问题的步骤的有序组合 方法包含于类或对象中 方法再程序中被创建,在其他地方被引用 [方法原子性]一个方 ...

  7. 面向对象 详解笔记 方法 类 对象 封装 继承 重写 多态 接口

    文章目录 面向过程&面向对象 面向过程 面向对象 什么是面向对象 回顾方法及加深 方法的定义 修饰符 返回类型 break:跳出switch,结束循环和return的区别 方法名:注意规范就o ...

  8. 详解java方法与递归

    目录 一 方法(类似于C语言中的函数) 1 方法的基本语法: 2 注意事项 3 方法调用的执行过程 4 方法的重载 重载的规则(满足以下三个特点): 5 方法的意义 二 递归 前提条件: 概念: 递归 ...

  9. python详解enumerate()方法;

    介绍 用法 既然是列举, 该方法一般常用于 数组, 配合for循环来使用 效果: 例一: nums = [1,2,3,1,1,3] for inx, i in enumerate(nums) prin ...

最新文章

  1. 7.Deep Interest Network for Click-Through Rate Prediction论文详解
  2. 欧盟正研究用三种标准技术应对DNS漏洞
  3. C 过渡 C++ 1
  4. 有没有必要开项目周会
  5. 在香蕉派 Banana Pi BPI-M1上使用 开源 OxOffice Impress
  6. python添加模块
  7. 消息称苹果正探索更大尺寸iPad 屏幕分别为14英寸、16英寸
  8. Web前端开发培训就业前景好不好?
  9. 发布一个jquery插件--在Web下输入密码时提示大写锁定键(2012-05-03 10:20最后修改)...
  10. Linux 安装 nginx 服务教程
  11. zabbix3.0 监控mysql服务器性能实现过程
  12. js中对于json判断的总结(持续整理中~)
  13. 第十一课:(2)在MINST-FASHION上实现神经网络的学习流程
  14. 运用SQL工具还原数据库
  15. python(摄氏度和华氏度的转换)
  16. 计算机表格增加,电脑中给Excel表格添加水印效果的方法
  17. Scala 将时间字符串转为时间戳
  18. zzun 2076 : 三花聚顶神功
  19. flink遇到的错误以及处理
  20. VUE实现checkBox至少选择一个并提示

热门文章

  1. Wordpress搬家换域名后如何更改后台的域名设置?
  2. 基于flask的YOLO目标检测web服务及接口
  3. parzen窗概率密度估计 matlab,核密度估计(parzen窗密度估计)
  4. 分享一下韩寒的经典语录
  5. go语言后端框架之简单探索
  6. JNI Log 日志输出
  7. 用Python的turtle和matplotlib画出圆满和爱心
  8. nginx映射静态资源linux,nginx静态资源映射
  9. 宝塔修改mysql密码
  10. 《儿女英雄传》不符合国情的地方