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