React(四):事件总线、setState的细节、PureComponent、ref

一、事件总线

这里的事件总线和vue中基本一个思路。

在React中可以通过第三方库来进行任意组件通信,安装:

npm install hy-event-store

使用:

在这里插入图片描述
1、在某个地方新建jsx文件对外暴露事件总线

// 创建事件总线
import { HYEventBus } from 'hy-event-store';
const eventBus = new HYEventBus();

export default eventBus;

2、在需要接收值的组件中,在挂在完毕的生命周期函数中绑定事件和被触发时的回调,最好写上销毁的代码:

//事件的回调
 getData(name,age) {
     console.log(name,age,this);
     this.setState({
         name: name, age: age
     })
 }
 //1.挂载完毕后绑定事件接收别的地方传过来的值
 componentDidMount() {
     eventBus.on('getData', this.getData.bind(this))
 }
 //3.销毁的时候解绑
 componentWillUnmount() {
     eventBus.off('getData', this.getData)
 }

3、另一个组件触发,并传值

sendData() {
        //2.某个组件中触发事件并传值
        eventBus.emit('getData', 'zzy', 18)
    }
    render() {
        return (
            <div>
                <h1>GrandSon组件</h1>
                <button onClick={() => this.sendData()}>点击传值给App</button>
            </div>
        )
    }

二、关于setState的原理

开发中我们并不能直接通过修改state的值来让界面发生更新:

因为我们修改了state之后,希望React根据最新的State来重新渲染界面,但是这种方式的修改React并不知道数据发生了变化;

React并没有实现类似于Vue2中的Object.defineProperty或者Vue3中的Proxy的方式来通过数据劫持监听数据的变化;

我们必须通过setState来告知React数据已经发生了变化;
源码先简单lou一眼:

在这里插入图片描述

1.setState的三种使用方式

我们基于以下组件进行操作

export class Son extends React.Component {
  constructor() {
    super();
    this.state = {
      name: 'zzy',
      age: 18,
    }
  }

  changeName() {
    this.setState(...)
  }

  render() {
    return (
      <div>
        <h1>{this.state.name}</h1>
        <button onClick={() => this.changeName()}>点击修改名字</button>
      </div>

    )
  }
}

(1)基本使用

我们之前用的最多的就是直接传入一个配置对象,然后给state中数据重新赋值。这里的原理是借助了Object.assign(state, newState)state和传入的对象进行合并,如果key重复那么就进行值的覆盖,没改的继续保留

//1.基本使用,传入配置对象,不是覆盖原来的state,而是进行对象的合并
this.setState({
  name: 'ht'  //原理:对象的合并Object.assign(state, newState)
})

(2)传入一个回调

setState的参数除了可以传配置对象外,还可以传入一个回调函数,通过return一个对象,对象中包含我们要修改的值,也可以实现数据的更新和页面的重新渲染。

这个回调可以接收两个参数:state和props,分别对应的是上一个修改状态stateprops的值们。

注意是上一个修改状态!如果在一个回调中多次执行setState更改数据,那么参数state保存的是上一个修改状态的值!如果不明白请看本节2.1.1部分

//2.传入一个回调,可以接收修改之前的state和props
this.setState((state,props) => {
  console.log(state,props);
  return {
    name: 'ht' //这里也可以进行更改
  }
})

(3)第一个参数是对象,第二个参数是回调

setState是一个异步调用。

如果在setState下面使用name,我们会发现拿到的是原来的name,这就证明了setState是一个异步调用,那么如果我们想在数据变化后再基于数据进行一些操作怎么办?这时候可以传入第二个参数:一个回调函数,该回调函数执行的时机就是数据更新完且render调用完毕后。

//3.setState是一个异步调用
//如果想等数据更新后做一些操作,可以传入第二个参数:回调
//第二个参数执行的时机就是数据更新完之后
this.setState({ name: 'ht' }, () => {
  console.log('数据已更新,值为', this.state.name)
})
console.log('数据未更新:', this.state.name); //zzy而不是ht

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

我们参考一下React的开发者Dan Abramov老哥的回答:

(1)提升性能,减少render次数

试想一下,如果我们写了下面这样的代码:

  changeName() {
    this.setState({
      name: this.state.name + 'ht'
    })
    this.setState({
      name: this.state.name + 'ht'
    })
    this.setState({
      name: this.state.name + 'ht'
    })
  }

如果是同步,那么每次调用 setState都进行一次更新,那么意味着render函数会被频繁调用(源码貌似在setState后会执行render),界面重新渲染无数次,这样效率是很低的;

最好的办法应该是获取到多个更新,之后进行批量更新,那么内部是怎么实现的呢?

这里其实用到的是队列,我们把每一个setState的调用放到队列里,然后依次取出每一个更改,对改变的属性依次进行对象的合并,直到都合并完,再去执行render函数,这样的话render函数只执行一次就欧了。

2.1.1多个state合并的小问题

除此之外,这里还有个问题,上面代码执行完毕后,页面结果是'zzyht',不应该是'zzyhththt'吗?其实是这样的,在执行的时候就统一先读出来this.state.name,所以其实上面的代码相当于做了三步同样的操作:

  changeName() {
    this.setState({
      name: 'zzy' + 'ht'
    })
    this.setState({
      name: 'zzy' + 'ht'
    })
    this.setState({
      name: 'zzy' + 'ht'
    })
  }

这样的话每次合并,合并的值都是'zzyht',而不是累加。

那如果传入回调可以解决这个问题吗?

  changeName() {
    this.setState(() => {
      return {
        name: this.state.name + 'ht'
      }
    })
    this.setState(() => {
      return {
        name: this.state.name + 'ht'
      }
    })
    this.setState(() => {
      return {
        name: this.state.name + 'ht'
      }
    })
  }

上面这个写法仍然不能解决,和传入对象是一样的,但是下面这种写法就可以。原因是:这里回调的参数state是上一个合并状态的state,所以是可以在上一个的基础上做操作的!

  changeName() {
    this.setState((state) => {
      return {
        name: state.name + 'ht'
      }
    })
    this.setState((state) => {
      return {
        name: state.name + 'ht'
      }
    })
    this.setState((state) => {
      return {
        name: state.name + 'ht'
      }
    })
  }

(2)避免state和props数据不同步

如果同步更新了state,但是还没有执行render函数,那么state和props不能保持同步,这样会在开发中产生很多的问题;什么意思呢?举个例子

父组件:

export class Father extends React.Component {
  constructor() {
    super();
    this.state = {
      age: 18,
    }
  }
  changeAge() {
    this.setState({
      age: 100
    })
    //一大坨代码,要执行一年
  }
  render() {
    return (
      <div>
        <button onClick={(() => this.changeAge())}>点击修改年龄</button>
        <Son age={this.state.age}/>
      </div>
    )
  }
}

子组件:

export class Son extends React.Component {
  render() {
    let {age} = this.props;
    return (
      <div>
        <h1>{age}</h1>
      </div>
    )
  }
}

父组件把state中的age传给子组件,我们假设setState是一个同步的任务,那么如果此时在changeAge这个函数里有一大坨代码,要执行一年,那么要等执行完之后再去render,那在这一年里这state是更新成100了,但是子组件的props的age值始终都是18,这数据就不一样了。

所以意思就是让数据更新完后立马render,所以把setState设置成异步。

3.获取异步修改完数据的结果

两种方式:
1、刚才提到的,第一个参数是对象,第二个参数是回调,在第二个回调中,可以获取
2、在生命周期钩子componentDidUpdate

在React18之前,如果在setState外边包个setTimeout这种宏任务,那么setState会变成同步,但是在React18之后就没用了,怎么搞都是异步

三、PureComponent监测数据的原理

1.先来看一个问题

一般情况下,只要调用setState,就会重新调用render函数,但这样是不太合理的。下面的三个组件,App为父组件,Son1、Son2分别为两个子组件。

export class App extends React.Component {
    constructor() {
        super();
        this.state = {
            name: 'zzy',
            age: 18,
        }
    }

    changeName() {
        this.setState({
            name: 'zzy'
        })
    }

    render() {
        console.log('App的render执行')
        return (
            <div>
                <h1>{this.state.name}</h1>
                <button onClick={() => this.changeName()}>点击修改名字</button>
                <Son1 />
                <Son2 age={this.state.age} />
            </div>
        )
    }
}
export class Son1 extends React.Component {
  render() {
    console.log('Son1的render执行')
    return (
      <div>
        <h2>Son1</h2>
      </div>
    )
  }
}
export class Son2 extends React.Component {
  render() {
    console.log('Son2的render执行')
    let {age} = this.props;
    return (
      <div>
        <h1>{age}</h1>
      </div>
    )
  }
}

点击修改名字按钮时,把名字改成了和原来一样的值,你会发现控制台输出:
在这里插入图片描述
这里涉及到两个问题:
1、如果修改后的数据和原来的值一样,是不是就不用改了
2、如果子组件Son1Son2里没有数据改变,是不是子组件的render不需要每次都跟着父组件的render执行一遍?

2.sholdComponentUpdate()

还记得之前生命周期的那张图吗?
sholdComponentUpdate()是在数据修改前执行(此钩子内数据还没修改),先判断一下是否要执行render,它有两个参数:
参数一:newProps 修改之后,最新的props属性
参数二:newState 修改之后,最新的state属性

该方法返回值是一个boolean类型
返回值为true,那么就需要调用render方法;
返回值为false,那么久不需要调用render方法;
默认返回的是true,也就是只要state发生改变,就会调用render方法;
在这里插入图片描述
1、第一个问题,如果修改后的值和原来的相同,我们可以通过sholdComponentUpdate(newProps, newState)钩子优化。

就是在数据更新之前判断一下新值和旧值是否相等,如果相等就不执行render,如果不相等就执行render。

App中:
shouldComponentUpdate(newProp, newState) {
    console.log(newState); //{name: 'zzy', age: 18}
    if(this.state.name != newState.name) {
        return true;
    } else {
        return false;
    }
}

2、第二个问题,如果子组件没有数据的改变,那么就不需要跟着父组件重新执行render函数了,我们同样可以用sholdComponentUpdate钩子优化。

shouldComponentUpdate(newProps, newState) {
 console.log(newProps, newState);
 if (this.props.age != newProps.age || this.props.name != newProps.name) {
   return true;
 } else {
   return false;
 }

 //如果子组件有自己的state,也要判断一下
 // if (this.state.xxx != newState.xxx) {
 //   return true;
 // } else {
 //   return false;
 // }
}

这样就解决了问题2

3.引出PureComponent

上面解决了这两个问题,但是真的是非常的麻烦,如果有多个数据,往下层也传了多个数据,那么我们要对每一个数据都写一个判断吗?那真是麻烦的一塌糊涂。

不过不用担心,React给我们封装好了解决这两个问题的东西,那就是PureComponent

(1)类组件

直接继承PureComponent而不是Component

class App extends React.PureComponent {...}

(2)函数组件

函数组件没有PureComponent,我们使用memo包裹实现相同效果

import React,{memo} from 'react';

const Son1 = memo(function() {
  console.log('Son1的render执行')
  return (
    <div>Son1</div>
  )
})

export default Son1

4.PureComponent只监测第一层

PureComponent是如何监测stateprops的变化从而执行render函数的呢?源码中PureComponent只能监视第一层数据的改变,也就是复杂数据只看第一层地址变没变。

如果我们继承Component,那么往数组里push新对象并把数组重新赋值,只要执行setState就render,其实重新赋值的这个地址是没变的;

在这里插入图片描述

但是PureComponent的话不会render,因为监测不到第二层数据的改变,数组的地址没变就默认没变

所以我们一般不要直接去修改state中的数据,要修改内层数据的话,最好整个替换掉,给个新地址。可以回去看看我们的案例:购物车案例修改数组中某个对象的属性、删除数组中整个对象

5.PureComponent如何监测深层数据的改变

上面已经提到,我们如果想要在PureComponent下改变第二层第三层的深层数据,我们需要整个替换掉,给个新地址。具体来说就是对原来的state来一个浅拷贝newState,然后修改newState中的相应数据,最后把newState放到setState里,这样地址变了,PureComponent就能监视到,从而执行render。

比如之前的购物车案例:

class App extends React.PureComponent {
	......
	changeCount(index, count) {
	    //1.对原来的数组进行浅拷贝(内部元素地址还是指向原来)
	    const newBooks = [...this.state.books];
	    //2.修改浅拷贝后的里面的值
	    newBooks[index].count += count;
	    //3.此时我们输出books会发现books里面对应的值也变了
	    console.log(this.state.books[index].count);
	    //4.最后调用setState执行render函数更新视图,把newBooks的地址给它
	    this.setState({
	        books: newBooks,
	    })
	}
	......
}

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

1.ref的三种用法

import React,{createRef} from 'react';
export class App extends React.PureComponent {
    constructor() {
        super();
        this.state = {
            name: 'zzy',
            age: 18,
        }

        this.myRef = createRef(); //第二种
        this.getRef = null;//第三种
    }

    getDOM() {
        //1.第一种:标签绑定ref属性,通过this.refs.属性名拿到
        console.log(this.refs.title);
        //2.第二种:提前创建ref对象,createRef(),把创建好的对象绑定到元素上
        console.log(this.myRef.current);//current以最后一个为主
        //3.第三种:通过函数拿到
        console.log(this.getRef);
    }

    render() {
        return (
            <div>
                {/* 1.第一种 */}
                <h1 ref='title'>h1标题</h1>
                {/* 2.第二种 */}
                <h2 ref={this.myRef}>h2标题</h2>
                <h3 ref={this.myRef}>h3标题</h3>
                {/* 3.第三种 */}
                <h4 ref={(el) => {this.getRef = el}}>h4标题</h4>
                
                <button onClick={() => this.getDOM()}>点击获取DOM</button>
            </div>
        )
    }
}
  1. 第一种:标签绑定ref属性,通过this.refs.属性名拿到(目前已经废弃了,一般不用)
  2. 第二种:导入createRef函数,把函数调用结果作为ref属性值(提前保存结果对象,然后把对象给ref),最后通过结果对象的current属性拿到
  3. 第三种:ref属性传入一个回调,回调的参数就是当前元素,可以保存起来,然后拿到。

2.ref获取类组件实例

子组件定义一个方法sayHi

class Son extends PureComponent {
    sayHi() {
        console.log('Hi,I am son');
    }
    render() {
        return (
            <div>
                <h1>Son组件</h1>
            </div>
        )
    }
}

父组件点击按钮获取子组件实例,并调用它的sayHi方法:

export class App extends PureComponent {
    constructor() {
        super();
        this.state = {
            name: 'zzy',
            age: 18,
        }

        this.myRef = createRef(); //第1步.创建ref对象
    }

    getDOM() {
        //第3步.通过this.myRef.current拿到当前东西
        console.log(this.myRef.current);//current以最后一个为主
        this.myRef.current.sayHi(); //拿到子组件并调用其中的方法Hi,I am son
    }

    render() {
        return (
            <div>
                {/* 第2步:把创建的对象给ref属性 */}
                <Son ref={this.myRef}/>
                <button onClick={() => this.getDOM()}>点击获取组件</button>
            </div>
        )
    }
}

3.ref获取函数组件内的某个元素

如果我们的子组件定义为了函数呢?那函数组件哪来的实例,那我们就没办法获取组件实例了,只能获取函数组件的内的某个React元素。

这时候需要用到React.forwardRef

import {forwardRef} from 'react';
const Son = forwardRef(function(props, ref) {
    return (
        <div>
            <h1 ref={ref}>我是函数儿子</h1>
        </div>
    )
})

这样我们可以通过在子组件实例<Son ref={this.myRef}/>拿到子组件内的某个标签。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值