React(四) 事件总线,setState的原理,PureComponent优化React性能,ref获取类组件与函数组件

ops/2024/10/17 22:15:19/

文章目录

  • 一、全局事件总线
  • 二、setState的原理
    • 1. 为什么要使用setState修改数据
    • 2. setState的三种用法
      • (1) 基本使用
      • (2) 传入回调函数
      • (3) setState是一个异步调用
    • 3. setState为什么要设置成异步
  • 二、PureComponent优化性能
    • 1. React的diff算法以及Key的优化(扩展)
      • (1) diff算法
      • (2) 列表中的key属性
    • 2. 引出问题:render函数的优化
    • 3. shouldComponentUpdate
    • 4. PureComponent与memo
      • (1) 类组件
      • (2) 函数式组件
    • 5. PureComponent浅层监测
    • 6. 实现PureComponent深层检测
  • 三、ref获取元素或组件实例
    • 1. ref获取原生DOM的三种方式
    • 2. ref获取类组件实例
    • 2. ref获取函数式组件里的元素

一、全局事件总线

安装第三方库npm install hy-event-store
发送数据的组件触发事件emit('事件名',参数)

javascript">// Son.jsxsendData () {// 触发事件,"tom", 100, 7.5是传递的参数eventBus.emit('getData', "tom", 100, 7.5)}render () {return (<div><h2>Son组件</h2><button onClick={this.sendData}>传递数据</button></div>)}

接收数据的组件绑定事件
绑定:xxx.on('事件名',绑定的函数,[this指向的值]) (this指向的值是可选的)
解绑:xxx.off('事件名',绑定的函数)

javascript">// App.jsxcomponentDidMount () {// 绑定事件,当getData事件被触发时,调用函数showDataeventBus.on('getData', this.showData)}componentWillUnmount () {// 解绑eventBus.off('getData', this.showData)}showData (name, nums, score) {console.log('showData', name, nums, score,);this.setState({ name, nums, score }) // 此时this指向undefined}

这里同样需要注意this的指向问题。这里有三种方式确定this指向

javascript"> componentDidMount () {// 绑定事件// eventBus.on('getData', this.showData)// 方式一: on的第三个参数可指定this指向eventBus.on('getData', this.showData, this)// 方式二: 箭头函数eventBus.on('getData', (name, nums, score) => this.showData(name, nums, score))}// 方式三:es6的class filedsshowData = (name, nums, score) => {this.setState({ name, nums, score })}

二、setState的原理

1. 为什么要使用setState修改数据

Vue和React数据管理与渲染界面的区别:

  因为Vue做了数据劫持,当数据变化时,Vue能够监听到数据的变化,然后底层的set方法调用了render()函数重新渲染页面。所以Vue用起来感觉是会自动渲染,不用我们手动调用render()函数。

  而React没有数据劫持,如果通过this.state.msg = 'xxx'来修改数据,Reac并不知道该数据发生变化,也就不会刷新页面。
如何让React得知数据发生变化?就是调用setState()来修改数据,调用这个函数就相当于通知React数据发生了更新,需要重新渲染界面,React就会调用render()函数。

总结:
  React并没有实现类似于Vue2中的Object.defineProperty或者Vue3中的Proxy的方式来监听数据的变化;我们必须通过setState来告知React数据已经发生了变化;

问:在组件中并没有实现setState的方法,为什么可以调用呢?
答:因为setState方法是从Component中继承过来的。

2. setState的三种用法

(1) 基本使用

setState({....})

javascript">  this.state = {msg: 'Hello World',counter: 0}...// 点击按钮,调用changeText函数,修改msgchangeText () {this.setState({msg: 'Hello Money'})}

setState里创建了一个新对象赋给state。从内存的角度来看是这样的:
在这里插入图片描述
新对象里没有counter,为什么新对象没有把旧对象覆盖掉呢?
底层其实是用了Object.assign(this.state,setState的新对象),把两个对象做了合并。然后在合适的时机再调用render()渲染。

(2) 传入回调函数

好处一: 可以在回调函数中编写对新state处理的逻辑
好处二: 当前的回调函数会将之前的state和props传递进来

javascript">changeText () {this.setState((state, props) => {console.log(state.msg, props) //打印 Hello World,空数组(因为props没值)return {msg: "你好啊, 李银河"}})
}

(3) setState是一个异步调用

javascript">changeText () {this.setState({ msg: "你好啊, 李银河" })console.log("------:", this.state.msg) // 打印的是Hello World,而不是新值
}

第三行比第二行先执行,说明setState是一个异步调用。

如果希望在数据更新之后(数据合并), 获取到对应的结果并执行一些逻辑代码
那么可以在setState中传入第二个参数: callback函数

javascript">changeText () {this.setState({ msg: "你好啊, 李银河" }, () => {console.log("++++++:", this.state.msg)})console.log("------:", this.state.msg)}

在这里插入图片描述

3. setState为什么要设置成异步

(1) setState设置为异步,可以显著提升性能
如果每次调用setState都进行一次更新,意味着render函数会被频繁调用,界面重新渲染,效率很低;最好的办法是获取到多个更新,之后进行批量更新

javascript"> changeCounter () {this.setState((state, props) => {console.log('第一次修改之前', state.count);return {count: state.count + 1}})this.setState((state, props) => {console.log(' 第二次修改之前', state.count);return {count: state.count + 1}})this.setState((state, props) => {console.log('第三次修改之前', state.count);return {count: state.count + 1}})}
render () {console.log('render函数被执行');...
}

三次setState的调用,只调用了一次render函数。
在这里插入图片描述
  如果发送的三个网络请求几乎同时返回结果,修改状态。则此时进行批量更新,只调用一次render,可显著提升性能。

(2) 如果同步更新了state,但未执行render函数,则state和props不能保持同步

state和props不能保持一致性,会在开发中产生很多问题。
在这里插入图片描述
加入18行代码是同步的,调用18行之后,12行的msg内容已变。但此时render函数还未调用,或者没执行完,导致传给子组件的props仍未更新。出现state和props不一致的情况。

二、PureComponent优化性能

1. React的diff算法以及Key的优化(扩展)

(1) diff算法

React的更新流程:

React在props或state发生改变时,会调用React的render方法,进而创建一颗不同的DOM树,然后进行新旧虚拟DOM的对比(diff算法):

 如果新旧两棵虚拟DOM树进行完全比较,(也就是左侧div与右侧的所有节点比较,左侧h2与所有节点进行比较)。则算法的复杂度为O(n^2) (n是树中元素个数)
在这里插入图片描述
React对该算法的优化:

  • 同层节点之间相互比较,不会跨层比较。
    (左侧div只与右侧div进行比较,不会与下一层的h2,button进行比较)
  • 不同类型的节点,产生不同的树结构(后代元素全部做替换)
    (如果左侧的div节点,与右侧同层的节点不一致,则以该节点为根节点的dom树,全部都进行更新,也就是div,h2,button都更换)
  • 开发中,通过key来指定哪些节点在不同的渲染下保持稳定。

(2) 列表中的key属性

和vue一样,key作为一个标识。

  • 当在列表最后位置插入数据时,这种情况,有无key意义并不大

  • 在前面插入数据

    • 在没有key的情况下,所有的li都需要进行修改;
    • 当子元素(这里的li)拥有 key 时,React 使用 key 来匹配原有树上的子元素以及最新树上的子元素;
  • key的注意事项
    (1) key应该是唯一的;
    (2) key不要使用随机数(随机数在下一次render时,会重新生成一个数字)
    (3) 使用index作为key,对性能是没有优化的;

2. 引出问题:render函数的优化

现有App、Son1、Son2三个组件

javascript">// 只关注render函数
class App extends Component {
...render () {console.log('App render');let { name, age } = this.statereturn (<div><h2>App组件---{name}---{age}</h2><Son1 /><Son2 /><button onClick={() => this.changeName()}>修改名字</button></div>)}
...

在这里插入图片描述
当App修改变量name时,App组件重新调用render函数,而其所有子组件的render函数也被重新调用了。
在这里插入图片描述

只修改App组件数据,所有组件却都需要重新render,重新新旧虚拟DOM对比(diff),性能必然是很低的。

子组件调用render的情况应该是:自己所依赖的数据发生改变时,再调用自己的render方法。

问题 :如何控制render是否被调用呢?

3. shouldComponentUpdate

生命周期函数shouldComponentUpdate(简称SCU)

  • 该函数有两个参数
    参数一:nextProps ,最新的props属性
    参数二:nextState ,最新的state属性

  • 返回值是布尔类型
    返回值为true,调用render方法;
    返回值为false,不调用render方法
    默认返回true

(1) 问题1:如果修改后的值和修改前一样,则不需要调用render函数

javascript">// App组件中:shouldComponentUpdate (nextProps, nextState) {if (this.state.name !== nextState.name || this.state.age!== nextState.age) {return true}//nextState.name还是tom,nextState.age还是10,则返回flasereturn false}

(2) 问题2:子组件没用到父组件的数据,则父组件更新时,子组件无需再调用render函数。

现将App中的name传给子组件Son1,age传给子组件Son2

javascript">  render () {console.log('App render');let { name, age } = this.statereturn (<div><h2>App组件---{name}---{age}</h2><Son1 name={name} /><Son2 age={age} /><button onClick={() => this.changeName()}>修改名字</button><button onClick={() => this.changeAge()}>修改年龄</button></div>)}

Son1和Son2分别设置SCU
在这里插入图片描述
当点击修改名字时,Son2的render不被调用。修改年龄时,Son1的render不被调用
在这里插入图片描述

4. PureComponent与memo

如果所有的类,都需要手动来实现 shouldComponentUpdate,工作量很多,而且如果需要判断的数据很多,if语句也会很长。

此时我们可以使用React提供的PureComponent 和memo。这两个分别用于类组件和函数式组件

(1) 类组件

对于类组件,继承PureComponent即可,而不是继承Component,
在这里插入图片描述

(2) 函数式组件

函数式组件无法继承,使用memo包裹即可

javascript">import { memo } from "react";
const Son3 = memo(function (props) {return (< div ><h3>Son3:{props.age}</h3></div >)
})
export default Son3

5. PureComponent浅层监测

PureComponent的底层是浅层监测数据是否发生变化。

比如在这个页面中,点击添加按钮,需要添加一本书
在这里插入图片描述

javascript">   this.state = {books: [{ name: "你不知道JS", price: 99, count: 1 },{ name: "JS高级程序设计", price: 88, count: 1 },{ name: "React高级设计", price: 78, count: 2 },{ name: "Vue高级设计", price: 95, count: 3 },],msg: 'HelloWorld'}
// 添加按钮的回调函数为addNewBook () {const newBook = { name: "Vue高级设计", price: 95, count: 1 }// 方式一:当类组件继承自Component时可以,(虽然可以,但不推荐)// 但继承于Purecomponent时,这种修改方式行不通的this.state.books.push(newBook)this.setState({ books: this.state.books })}

因为books是引用数据类型,它的值是地址值,虽然该数组确实添加了一个元素,但是books地址值未变,所以PureComponent监测不到。
正确打开方式是:

javascript">  addNewBook () {const newBook = { name: "Angular高级设计", price: 85, count: 1 }// 方式二: 浅拷贝let books = [...this.state.books]books.push(newBook)this.setState({ books: books })}

在这里插入图片描述
浅拷贝之后的books地址值和this.state.books的地址值不一样(内容一样); 所以 this.setState({ books: books })相当于给state里的books赋值了新值,PureComponent就能监测到了。

6. 实现PureComponent深层检测

如果要修改books里面的count值,也需要进行依次浅拷贝然后再修改。浅拷贝的目的是让books的地址值改变,从而让组件能够监测的到数据变化,调用render函数。

javascript"> changeCount (index) {// this.state.books[index] += 1let books = [...this.state.books]books[index].count += 1this.setState({ books: books })}

结合5里画的内存图,其实可以看出第2行与第5行改的是同一块内存。

三、ref获取元素或组件实例

1. ref获取原生DOM的三种方式

方式一:在元素上用ref打标识:<h1 ref='title'>
方式二:调用createRef()函数,先创建一个ref标识,然后再元素上绑定这个标识。
方式三:在标签上通过ref传递一个回调函数,参数值就是当前元素。

javascript">import React, { createRef, PureComponent } from 'react'
export class App extends PureComponent {// 获取原生dom的三种方式constructor() {super()// 方式二:先创建一个标识this.hwRef = createRef()// 方式三this.getRef = null}getDOM (el) {// 方式一:被废弃console.log(this.refs.title);// 方式二:.current获取到当前的元素,但是若在很多标签上都标识hwRef,//       .current还是只能获取到一个元素console.log(this.hwRef.current);// 方式三console.log(this.getRef);}render () {return (<div>{/* 方式一: */}<h1 ref='title'>App组件</h1>{/* 方式二:绑定事先创建好的标识 */}<h2 ref={this.hwRef}>HelloWorld</h2>{/* 方式三:这里的el就是dom元素 */}<h3 ref={el => this.getRef = el}>身体健康</h3><button onClick={e => this.getDOM()}>点击获取Dom元素</button></div>)}
}

2. ref获取类组件实例

创建子类Son:

javascript">export class Son extends PureComponent {// 实例方法showInfo () {console.log('I am 子组件');}render () {return (<h2>Son组件</h2>)}
}

父类获取到子组件的组件实例后,可以调用子组件的实例方法:

javascript">export class App extends PureComponent {constructor() {super()// 1. 创建ref对象this.childRef = createRef()}getDOM () {// 3. 获取子组件实例,并调用子组件的实例方法console.log(this.childRef.current);this.childRef.current.showInfo()}render () {return (<div>{/* 2. 创建的ref对象绑定在子组件上 */}<Son ref={this.childRef} /><button onClick={e => this.getDOM()}>点击获取Dom元素</button></div>)}
}

在这里插入图片描述

2. ref获取函数式组件里的元素

因为函数式组件没有组件实例,上述的方式获取不到函数组件

javascript">function Son2 () {return (<h2>Son2组件</h2>)
}<Son2 ref={this.childRef} />
console.log(this.childRef.current); // 打印出来为null

对于函数式组件,我们可以通过ref获取到组件里的某个元素,比如:<h2>Son2组件</h2>
需要借助forwardRef ,这样函数组件可以接收两个参数,一个是props,一个是ref

javascript">import React, { createRef, PureComponent, forwardRef } from 'react'
// 这里的ref
const Son2 = forwardRef(function (props, ref) {return (<h2 ref={ref}>Son2组件</h2>)
})

在这里插入图片描述

javascript">console.log(this.childRef.current); // <h2>Son2组件</h2>

http://www.ppmy.cn/ops/126312.html

相关文章

多仓多门店库存管理与系统设计

库存是供应链之魂。 在新零售模式下&#xff0c;仓库和门店遍布全国甚至全球&#xff0c;如果库存管理不到位&#xff0c;就没法给企业赋能&#xff0c;无法给客户带来极致购物体验。 商品的库存数是整个供应链业务的核心&#xff0c;是业务能顺利流转的基础&#xff0c;如何…

Axure PR 9 滑动条 设计交互

​大家好&#xff0c;我是大明同学。 这期内容&#xff0c;我们来探讨Axure中滑动条设计与交互技巧。 滑动条 创建滑动条所需的元件 1.打开一个新的 RP 文件并在画布上打开 Page 1。 2.在元件库中拖出一个矩形元件。 3.选中矩形元件&#xff0c;样式窗格中&#xff0c;将宽…

双调TSP问题最牛逼的解法,不接受所有人反驳

为什么我取这个标题呢&#xff1f;因为我的解 又简单 又好写 我找遍了许多答案&#xff0c;却没发现一个满意的&#xff0c;通过询问GPT&#xff0c;再通过自己的改善&#xff0c;总算得到正确的解了&#xff01;&#xff01;&#xff01; 首先你得明白是如何递推的。 我们规…

基础sql

在执行删除操作之前&#xff0c;建议先运行一个 SELECT 查询来确认你要删除的记录。这可以帮助你避免误删数据。 删除字段id默认值为空字符串的所有数据 delete from users where id ; 删除字段id默认值为null的所有数据 delete from users where id is null; 删除字段upd…

【firefox】火狐浏览器、火狐浏览器驱动、selenium版本号对应关系

火狐浏览器、火狐浏览器驱动、selenium版本号对应关系 链接地址:geckodriver、firefox、selenium版本号对应关系

【C语言】算术运算、关系运算、逻辑运算

算术运算&#xff1a;常见的数字运算&#xff0c;加减乘除等 关系运算&#xff1a;数值之间大小多少的关系 逻辑运算&#xff1a;逻辑与、或、非 #include <stdio.h> /* 功能&#xff1a;算术运算、关系运算、逻辑运算 时间&#xff1a;2024年10月 地点&#xff1a;贤者…

Wireshark数据包分析教程

Wireshark数据包分析教程 本教程将基于Wireshark工具捕获的数据包&#xff0c;逐步讲解网络数据帧中的各项信息&#xff0c;帮助你了解每个字段的含义及其作用。我们将从最基础的帧&#xff08;Frame&#xff09;信息开始&#xff0c;逐层解释包括以太网、IP、TCP、HTTP和JSON…

uniAPP如何开发?PHP语言的书写该如何制作

开发一个基于uni-app的项目以及与之交互的PHP后端涉及多个步骤和技术栈。以下是一个简要的指南&#xff0c;帮助你理解如何开始这两个部分的开发。 一、uni-app开发 1. 环境准备 Node.js&#xff1a;确保你已经安装了Node.js&#xff0c;这是构建和运行uni-app项目的基础。H…