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

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

文章目录

  • 一、全局事件总线
  • 二、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('事件名',参数)

// Son.jsx
  sendData () {
    // 触发事件,"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('事件名',绑定的函数)

// App.jsx
  componentDidMount () {
    // 绑定事件,当getData事件被触发时,调用函数showData
    eventBus.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指向

 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 fileds
  showData = (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({....})

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

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

(2) 传入回调函数

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

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

(3) setState是一个异步调用

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

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

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

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

在这里插入图片描述

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

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

 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三个组件

// 只关注render函数
class App extends Component {
...
  render () {
    console.log('App render');
    let { name, age } = this.state
    return (
      <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函数

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

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

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

  render () {
    console.log('App render');
    let { name, age } = this.state
    return (
      <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包裹即可

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

5. PureComponent浅层监测

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

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

   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监测不到。
正确打开方式是:

  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函数。

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

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

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

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

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

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:

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

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

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获取函数式组件里的元素

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

function Son2 () {
  return (
    <h2>Son2组件</h2>
  )
}

<Son2 ref={this.childRef} />
console.log(this.childRef.current); // 打印出来为null

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

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

在这里插入图片描述

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

http://www.kler.cn/news/350845.html

相关文章:

  • 基于HEC-Ras及ArcGIS的泥石流数值模拟与灾害风险评估典型案例
  • C#基础知识-枚举
  • 计算机毕业设计PySpark+大模型高考推荐系统 高考分数线预测 高考爬虫 协同过滤推荐算法 Vue.js Django Hadoop 大数据毕设
  • C语言初阶七:C语言操作符详解(1)
  • NLP--一起学习Word Vector【实践】
  • Java项目-基于springboot框架的智慧外贸系统项目实战(附源码+文档)
  • cisco网络安全技术第3章测试及考试
  • excel如何把年龄转换为日期
  • HTML5_标签_各类表格的实现
  • 【排序】——1.冒泡排序法(含优化)
  • 嵌套之美:广义表,在数据层层叠叠之间,展现信息的层次
  • RT-Thread线程的定义和属性
  • 【星闪开发连载】WS63E模组的速度测试
  • 3D 数字人与 2D 数字人的区别
  • 代码随想录算法训练营第八天(1)|哈希表理论基础
  • 线程简单的用例
  • Vue3动态组件component不生效问题解决方法
  • Linux的GDB学习与入门
  • RabbitMQ是什么?
  • 通用数据库对象设计
  • Python酷库之旅-第三方库Pandas(155)
  • chat_gpt回答:python从bin文件里读四字节整型
  • Android启动第三方App的服务
  • HDFS单元测试
  • 曲线的弧长与曲率
  • 1.3.ReactOS系统宏函数ASSERT的实现