天天看點

ReactP9_不可變資料的力量_事件總線_ref_受控/非受控元件不可變資料的力量(The Power Of Not Mutating Data)事件總線關于ref受控/非受控元件

ReactP9_不可變資料的力量_事件總線_ref_受控/非受控元件

  • 不可變資料的力量(The Power Of Not Mutating Data)
  • 事件總線
  • 關于ref
  • 受控/非受控元件
    • 受控元件
    • 非受控元件

這邊是react學習記錄,期間加入了大量自己的了解,用于加強印象,若有錯誤之處還請多多指出

不可變資料的力量(The Power Of Not Mutating Data)

不可變資料的力量,代表的就是不可變資料設計原則。

React的生命周期中每次調用ComponentShouldUpdate()會擷取props/state,利用現有的資料跟将要改變的資料進行比較,更新變化的資料并進行渲染。此舉最大限度減少不必要的更新,達到性能優化的目的。是以,使用時不建議直接更改state裡面的資料,而是通過setState去改變參數。

用一個簡單案例來說清楚:

ReactP9_不可變資料的力量_事件總線_ref_受控/非受控元件不可變資料的力量(The Power Of Not Mutating Data)事件總線關于ref受控/非受控元件
  • 每次單擊“加入新資料”,在底部會加入一個設定好的新數組以及對應的“單價+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,被判定為資料沒有發生改變,也就不會執行更新操作了。

ReactP9_不可變資料的力量_事件總線_ref_受控/非受控元件不可變資料的力量(The Power Of Not Mutating Data)事件總線關于ref受控/非受控元件

這張圖比較形象的描繪了變量在記憶體中的表現。其中名稱上方指的是記憶體目前位址,名稱下方是指記憶體指向的位址。關于記憶體的基本原理請學習計算機組成。(途中所有的位址是編的,主要強調說明的是記憶體之間的關系,而非在記憶體中的表現一定如此)

正确代碼如下:

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

ReactP9_不可變資料的力量_事件總線_ref_受控/非受控元件不可變資料的力量(The Power Of Not Mutating Data)事件總線關于ref受控/非受控元件

事件總線,可以了解成一個能夠被所有元件調用的“全局資料”。舉個例子:

首先構造一個簡單的,具有兄弟關系元件的一個頁面,其中父元件是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