ReactP9_不可变数据的力量_事件总线_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

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值