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>