ReactP9_不可变数据的力量_事件总线_ref_受控/非受控组件
- 不可变数据的力量(The Power Of Not Mutating Data)
- 事件总线
- 关于ref
- 受控/非受控组件
-
- 受控组件
- 非受控组件
这边是react学习记录,期间加入了大量自己的理解,用于加强印象,若有错误之处还请多多指出
不可变数据的力量(The Power Of Not Mutating Data)
不可变数据的力量,代表的就是不可变数据设计原则。
React的生命周期中每次调用ComponentShouldUpdate()会获取props/state,利用现有的数据跟将要改变的数据进行比较,更新变化的数据并进行渲染。此举最大限度减少不必要的更新,达到性能优化的目的。因此,使用时不建议直接更改state里面的数据,而是通过setState去改变参数。
用一个简单案例来说清楚:

- 每次单击“加入新数据”,在底部会加入一个设置好的新数组以及对应的“单价+1”按钮
- 每次单击“单价+1”,水果名字后面的数字+1
- 每次单击按钮页面立即更新
错误代码如下:
import React, { PureComponent } from 'react'
export default class App extends PureComponent {
constructor(props){
super(props);
this.state = {
fruits:[
{name:'apple',price:10},
{name:'orange',price:20},
{name:'watermelon',price:30},
]
}
}
render() {
return (
<div>
<ul>
{
this.state.fruits.map((item,index)=>{
return (<li>
{item.name}
{item.price}
<button onClick={ e=>{this.priceAddtion(index)}}>单价+1</button></li>
)
})
}
</ul>
<button onClick={ e => {this.insertData()}}>加入新数据</button>
</div>
)
}
insertData(){
const newData = {name:"grapes",price:40};
//错误的数据更新方法 ShouldComponentUpdate——SCU优化失效
this.state.fruits.push(newData);
this.setState({
fruits:this.state.fruits
})
}
priceAddtion(index){
//错误的数据更新方法
this.state.fruits[index].price++;
this.setState({
fruits:this.state.fruits
})
}
}
此处insertData()中看似进行了一次setState的操作,但是实际上数据并不会发生任何改变。这牵扯到JS的语言特性,JS语言是一种标记语言,每个参数所保存的内容不是内容本身,而是存放对应内容的内存首地址。代码中fruits:this.state.fruits执行的时候,是把原本fruits数组的首地址赋给了setState去执行数据更新。虽然前面的push方法已经在数组的后面插入了一个新的数据,但是由于数组的首地址并没有发生改变,fruits的首地址是D,而this.state.fruits表明的数组和fruits是同一个数组,因此首地址相同,同为D。那么fruits就等于this.state.fruits,被判定为数据没有发生改变,也就不会执行更新操作了。
这张图比较形象的描绘了变量在内存中的表现。其中名称上方指的是内存当前地址,名称下方是指内存指向的地址。关于内存的基本原理请学习计算机组成。(途中所有的地址是编的,主要强调说明的是内存之间的关系,而非在内存中的表现一定如此)
正确代码如下:
import React, { PureComponent } from 'react'
export default class App extends PureComponent {
constructor(props){
//省略
}
render() {
return (
//省略
)
}
insertData(){
const newData = {name:"grapes",price:40};
//正确的数据更新方法
const newState = [...this.state.fruits,newData];
this.setState({fruits:newState});
}
priceAddtion(index){
//正确的数据更新方法
const newData = [...this.state.fruits];
newData[index].price += 1;
this.setState({
fruits:newData
});
}
}
该大致思想是创建一个新的数组,这样系统就会分配一个新的空间,该空间有着和原本数组不同的内存地址,通过ES6拆分数组元素的语法,并在末尾追加一个新的元素的方法,构建一个数据结构相同,但是包含了新的元素的数组。最后再用setState方法来更新数据。避免由于内存地址赋值原因导致的更新失败bug。
关于单击+1,总体思路大致相同,不多赘述。
事件总线
在研究事件总线之前,首先需要安装相关插件
终端执行代码:yarn add events
事件总线,可以理解成一个能够被所有组件调用的“全局数据”。举个例子:
首先构造一个简单的,具有兄弟关系组件的一个页面,其中父组件是App,子组件Home和Profile互为兄弟组件关系包含于App之中
import React, { PureComponent } from 'react'
class Home extends PureComponent{
render(){
return(
<div>Home</div>
)
}
}
class Profile extends PureComponent{
render(){
return(
<div>
profile
<button>hello</button>
</div>
)
}
}
export default class App extends PureComponent {
render() {
return (
<div>
<Home/>
<Profile/>
</div>
)
}
}
作为一个“全局的数据”,根据使用的逻辑,可以大致分为:
- 引用
- 声明
- 监听
- 执行
- 取消监听
接下来针对每一步给出具体代码:
- 引用
import {EventEmitter} from 'events'
//event bus
const eventBus = new EventEmitter();
安装了events模块插件之后,可以调用到events包中的一个类EventEmitter,这是一个用于创建事件总线的类
针对EventEmitter创建一个对象实例,取名eventBus(bus含有总线的意思)
- 声明
emitEvent(){
//需要在btn中添加对该函数的调用
eventBus.emit("sayhello","hello home",123);
}
emit需要输入两个以上的参数:
第一个参数是事件的key,可以理解成需要调用总线事件时需要的“口令”
其余参数则是用于进行共享的数据(会被依次作为参数放入到后面需要执行的函数中)
- 监听
componentDidMount(){
//用于事件监听
eventBus.addListener("sayhello",this.handleSayHelloListener)//此处函数后不能加()
}
componentWillUnmount(){
//取消事件监听
eventBus.removeListener("sayhello",this.handleSayHelloListener);
}
针对事件总线调用需要添加监听事件,根据官方指定的监听规则,需要在钩子函数componentDidMount()中添加对总线事件的监听;而在componentWillUnmount()中,也就是组件即将销毁之前,取消对总线事件的监听。
这里有一个需要注意的地方,添加监听事件,引用对应的执行函数时,函数名后面不能添加小括号,不然会被视作是调用该函数,所在的地方会被填充的是函数执行之后return返回的值,而在此处仅仅是为了表明监听触发后需要执行的指定函数,而非调用该函数
- 执行
handleSayHelloListener(num,message){
console.log(num,message);
}
监听到了“口令”“sayhello”之后,调用并执行了函数handleSayHelloListener,其中,一个事件总线的监听执行流程就走完了。
总结一下:
- 应用并创建用于事件总线的应用实例后,
- 在组件profile中添加满足触发条件就会emit的事件“口令”和执行“口令”对应调用函数所需的参数
- 需要共享数据的组件添加监听事件,监听时间需要提供事件的“口令”以及引用需要执行的函数
- 触发事件,发出“口令”和参数,根据“口令”和参数执行指定函数
- 取消监听事件
关于ref
如果需要对DOM进行操作可以使用ref
当前有三种ref获取DOM的方式
-
第一种,字符串获取
使用时通过 this.refs.传入的字符串获取对应元素
-
第二种,传入对象获取
通过 React.createRef() 创建对象
使用时获取创建的对象,其中有一个current属性就是对应的元素
-
第三种,传入函数获取
该函数会在DOM被挂载时进行回调,函数会传入一个元素对象,可以自己保存
使用时,直接使用保存的元素对象即可
import React, { createRef, PureComponent } from 'react'
export default class App extends PureComponent {
constructor(props){
super(props);
//第二种获取方式
this.secondRef = createRef();
//第三种获取方式
this.thirdEl = createRef();
}
render() {
return (
<div>
<h1 className="firstDOM" ref="first">Hello baby</h1>
<h1 className="secondDOM" ref={this.secondRef}>Hello doggy</h1>
<h1 className="thirdDOM" ref={args => this.thirdEl = args}>Hello piggy</h1>
<hr/>
<button onClick={e=>{this.changeText()}}>what?</button>
</div>
)
}
changeText(){
//通过字符串获取DOM
this.refs.first.innerHTML = "Come on baby";
console.log(this.refs.first);
//通过创建对象获取DOM
this.secondRef.current.innerHTML = "Come on doggy";
console.log(this.secondRef.current);
//回调函数赋值获取DOM
this.thirdEl.innerHTML = "Come on piggy"
console.log(this.thirdEl);
}
}
根据官方更新的进程,第一种字符串方式可能在未来被移除,所以这边优先推荐使用第二、第三种方式来获取要操作的DOM
ref类型的值根据节点的类型而有所不同
- 当 ref 属性用于 HTML 元素时,构造函数中使用 React.createRef() 创建的 ref 接收底层 DOM 元素作为其 current 属性
- 当 ref 属性用于自定义 class 组件时,ref 对象接收组件的挂载实例作为其 current 属性
- 不能在函数组件上使用 ref 属性,因为他们没有实例
因此,可以在一个组件内通过ref调用另外一个组件的方法(挺像vue中this.$ref来调用其他组件中方法的)
受控/非受控组件
受控组件
React中表单元素交由框架内部的state中处理
个人理解:判断是否是受控组件,主要看表单元素是否把state作为唯一数据源
import React, { PureComponent } from 'react'
export default class App extends PureComponent {
constructor(props){
super(props);
this.state = {
username:""
}
}
render() {
return (
<div>
<form onSubmit={e=>{this.formSubmit(e)}}>
<label htmlFor="">
user:
{/*受控组件*/}
<input type="text"
id="username"
onChange={e=>{this.formChange(e)}}
value={this.state.username}/>
</label>
<input type="submit" value="submit"/>
</form>
</div>
)
}
formSubmit(e){
e.preventDefault();
console.log(this.state.username);
}
formChange(e){
this.setState({
username:e.target.value
})
}
}
在这个用例中,先通过onChange监听获取input中的value,再将value赋给state,state发生改变后,主动再将state的数据重新赋给一次input。这种通过state作为组件唯一数据源并且时刻保持state和value同步的组件,就是受控组件,该案例是受控组件的一种基本使用形式。
该数据交互并非双向数据绑定,而是一种单向数据流
非受控组件
表单数据交由DOM节点来处理
官方不建议使用非受控组件来处理表单数据
一般由ref方式来获取表单数据,例如:
constructor(props){
super(props);
this.usernameRef = createRef();
}
render() {
return (
<div>
<form onSubmit={e=>{this.formSubmit(e)}}>
<label htmlFor="">
user:
<input type="text"
id="username"
ref={this.usernameRef}/>
</label>
<input type="submit" value="submit"/>
</form>
</div>
)
}
formSubmit(e){
e.preventDefault();
console.log(this.usernameRef.current.value);
}
此处通过this.[refObject].current.value来获取表单中的数据。
感谢coderwhy(王红元老师)的课程
ASH,AZZ