React(四)
- 一、事件总线
- 二、关于setState的原理
- 1.setState的三种使用方式
- (1)基本使用
- (2)传入一个回调
- (3)第一个参数是对象,第二个参数是回调
- 2.为什么setState要设置成异步
- (1)提升性能,减少render次数
- 2.1.1多个state合并的小问题
- (2)避免state和props数据不同步
- 3.获取异步修改完数据的结果
- 三、PureComponent监测数据的原理
- 1.先来看一个问题
- 2.sholdComponentUpdate()
- 3.引出PureComponent
- (1)类组件
- (2)函数组件
- 4.PureComponent只监测第一层
- 5.PureComponent如何监测深层数据的改变
- 四、ref获取元素或组件实例
- 1.ref的三种用法
- 2.ref获取类组件实例
- 3.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
,分别对应的是上一个修改状态
的state
和props
的值们。
注意是上一个修改状态!如果在一个回调中多次执行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、如果子组件Son1
、Son2
里没有数据改变,是不是子组件的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
是如何监测state
和props
的变化从而执行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>
)
}
}
- 第一种:标签绑定
ref
属性,通过this.refs.属性名
拿到(目前已经废弃了,一般不用) - 第二种:导入
createRef
函数,把函数调用结果作为ref属性值(提前保存结果对象,然后把对象给ref),最后通过结果对象的current属性拿到 - 第三种: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}/>
拿到子组件内的某个标签。