欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 科技 > IT业 > React(四) 事件总线,setState的原理,PureComponent优化React性能,ref获取类组件与函数组件

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

2024/10/24 1:53:57 来源:https://blog.csdn.net/qq_44285582/article/details/142868211  浏览:    关键词: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.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('事件名',绑定的函数)

// 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指向

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

  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传递进来

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

// 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

  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包裹即可

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] += 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传递一个回调函数,参数值就是当前元素。

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>

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com