從零開始的react入門教程(二),從react組件說到props/state的聯系與區別


壹 ❀ 引

從零開始的react入門教程(一)一文中,我們搭建了第一個屬於自己的react應用,並簡單學習了jsx語法。jsx寫法上與dom標簽高度一致,當然我們也知道,本質上這些react元素都是React.createElement()的語法糖,通過編譯,bable會將其還原成最原始的樣子,比如如下代碼效果相同:

<div class="echo"></div>
// 等同於
React.createElement(
  'div',
  {className: 'echo'}
)

至少從書寫上,jsx為我們提供了極大便利。在文章結尾,我們也敲到了react元素並非組件,它可能是一個單一的標簽,也可能是一個代碼塊,在react中有專門的方式來創建組件,那么本文就從組件說起。

貳 ❀ 組件

貳 ❀ 壹 函數組件

react中的組件分為函數組件class組件,兩者有一定區別,但都非常好理解。函數組件很簡單,比如我們現在想復用一段簡單的dom結構,但它的文本內容可能會不同,這種情況我們想到的就是文本內容是一個變量,這樣就能做到dom復用的目的了,所以函數組件就是做了這樣一件事:

// 這是一個函數組件,它接受一些props,並返回組合后的dom結構
function Echo(props) {
  return <div>聽風是風又叫{props.name}</div>;
};

ReactDOM.render(<Echo name="聽風是風" />, document.getElementById("root"));

需要注意的是,函數組件的函數名是大寫的(class組件也是如此),我們在render中使用了組件Echo,並傳遞了一個name屬性,所有在組件上傳遞的屬性都會被包裹在props對象中,所以通過props參數我們能訪問到每一個傳遞給組件的屬性。通過打印props可以看到它是一個對象:

function Echo(props) {
  console.log(props);
  return <div>聽風是風又叫{props.name}</div>;
};

傳遞的數據格式除了字符,數字,它當然也支持對象傳遞,比如下面這個例子運行結果與上方相同:

const myName = {
  name:'echo'
};
function Echo(props) {
  console.log(props);
  return <div>聽風是風又叫{props.name.name}</div>;
};
// 
ReactDOM.render(<Echo name= {myName}/>, document.getElementById("root"));

我們來解讀下props.name.name,首先我們是將myName作為name的值傳遞給了組件,所以要訪問到myName得通過props.name拿到,之后才是name取到了具體的值。其次需要注意的是傳遞對象需要使用{}包裹,如果不加花括號會有錯誤提示,jsx這里只支持加引號的文本或者表達式,而{myName}就是一個簡單的表達式。

我們在上文中,也有將react元素賦予給一個變量的寫法,比如:

const ele = <div>我的名字是聽風。</div>
ReactDOM.render(ele, document.getElementById("root"));

其實組件也能像這樣賦予給一個變量,所以看到下面這樣的寫法也不要奇怪:

function Echo(props) {
  return <div>我的名字是{props.name}</div>;
};
// 這里將組件賦予給了一個變量,所以render時直接用變量名
const ele = <Echo name="聽風是風"/>
ReactDOM.render(ele, document.getElementById("root"));

同理,react元素可以組合成代碼塊,組件同樣可以組合成一個新組件,比如:

function Echo(props) {
  return <div>我的名字是{props.name}</div>;
};
function UserList() {
  return (
    // 注意,只能有一個父元素,所以得用一個標簽包裹
    <div>
      <Echo name="echo" />
      <Echo name="聽風" />
    </div>
  );
};
ReactDOM.render(<UserList />, document.getElementById("root"));

在這個例子中,組件Echo作為基礎組件,重新構建出了一個新組建UserList,所以到這里我們可以發現,組件算自定義的react元素,它可能是由react元素組成,也可能是由組件構成。

貳 ❀ 貳 class組件

除了上面的函數組件外,我們還可以通過class創建組件,沒錯,就是ES6的class,看一個簡單的例子:

class Echo extends React.Component {
  render() {
    return <div>我的名字是{this.props.name}</div>;
  }
};
ReactDOM.render(<Echo name="聽風" />, document.getElementById("root"));

由於ReactDOM.render這一塊代碼相同,我們把目光放在class創建上,事實上這里使用了extends繼承了Component類,得到了一個新組件Echo。由於此時的Echo並不是函數,也不能接受函數形參,但事實上我們可以通過this.props直接訪問到當前組件所傳遞的屬性,讓人心安的是,與函數組件相比,我們同樣有方法獲取外部傳遞的props。

extends中的render方法是固定寫法,它里面包含的是此組件需要渲染的dom結構,如果你了解過ES6的class類,除了render固有方法外,其實我們可以在這個類中自定義任何我們想要的屬性以及方法,比如:

const o = {
  a: 1,
  b: 2,
};
class Echo extends React.Component {
  // 這是一個自定義方法
  add(a, b) {
    return a + b;
  }
  // 這是固定方法
  render() {
    return <div>{this.add(this.props.nums.a, this.props.nums.b)}</div>;
  }
}
ReactDOM.render(<Echo nums={o} />, document.getElementById("root"));

看着似乎有點復雜,我們來解釋做了什么,首先我們在外部定義了一個包含2個數字的對象o,並將其作為nums屬性傳遞給了組件Echo,在組件內除了render方法外,我們還自定義了一個方法add,最終渲染的文本由此方法提供,所以我們在返回的標簽中調用了此方法。前面說了可以通過this.props訪問到外部傳遞的屬性,所以這里順利拿到了函數的兩個實參並參與了計算。

那么到這里我們知道,除了一些組件固有方法屬性外,我們也可以定義自己的方法用於處理渲染外的其它業務邏輯。

舉個很常見的情景,在實際開發中,有時候我們處理的組件結構會相對龐大和復雜,這時候我們就能通過功能拆分,將一個大組件在內部拆分成單個小的功能塊,比如下面這段代碼:

class Echo extends React.Component {
  handleRenderTop() {
    return "我是頭部";
  }
  // 自定義的render方法
  renderTop() {
    return <div>{this.handleRenderTop()}</div>;
  }
  handleRenderMiddle() {
    // dosomething
  }
  renderMiddle() {
    return <div>我是中間部分</div>;
  }
  handleRenderBottom() {
    // dosomething
  }
  renderBottom() {
    return <div>我是底部</div>;
  }
  // 官方提供的固定render方法
  render() {
    return (
      <div>
        {this.renderTop()}
        {this.renderMiddle()}
        {this.renderBottom()}
      </div>
    );
  }
}
ReactDOM.render(<Echo />, document.getElementById("root"));

在上述代碼中,假設這個組件結構和邏輯比較復雜,通過拆分我們將其分為了上中下三個部分,並創建了對應的處理方法,最終在render中我們將其組成在一起,這樣寫的好處是可以讓組件的結構更清晰,也利於后期對於代碼的維護。當然這里也只是提供了一種思路和可能性,具體做法還需要自行探索。

其實在class組件中除了固有render方法外,還有ES6的constructor,以及組件生命周期函數,這些都是固定寫法,不過我們現在不急,后面會展開說明。

肆 ❀ props與State

肆 ❀ 壹 基本概念與區別

與vue雙向數據綁定不同,react提供的是單向數據流,我們可以將react的數據流動理解成一條瀑布,水流(數據)從上往下流動,傳遞到了瀑布中的每個角落(組件),而這里的水流其實就是由props和State構成,數據能讓看似靜態的組件換發新生,所以現在我們來介紹下組件中的數據props與State,並了解它們的關系以及區別。

先說props,其實通過前面的例子我們已經得到,props就是外部傳遞給組件的屬性,在函數組件中,可以直接通過形參props訪問,而在class組件中,我們一樣能通過this.props訪問到外部傳遞的屬性。

那么什么是State呢,說直白點,State就是組件內部定義的私有屬性,這就是兩者最直觀的區別。

State在react中更官方的解釋是狀態機,狀態的變化會引起視圖的變化,所以我們只需要修改狀態,react會自動幫我們更新視圖。比如下面這個例子:

class Echo extends React.Component {
  constructor(props) {
    // 參照ES6,extends時,constructor內使用this必須調用super,否則報錯
    super(props);
    this.state = { name: "echo" };
  }
  render() {
    return (
      <div>
        我的名字是{this.state.name},年齡{this.props.age}
      </div>
    );
  }
}
ReactDOM.render(<Echo age="27" />, document.getElementById("root"));

在上述例子中,外部傳遞的age就是props,所以在內部也是通過this.props訪問,而內部定義的屬性則是通過this.state聲明,一個在里一個在外,它們共同構成了組件Echo的數據。

上述例子中constructor內部調用了super方法,這一步是必要的,如果你想在繼承類的構造方法constructor中使用this,你就一定得調用一次,這也是ES6的規定,簡單復習下ES6的繼承:

class Parent {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
}

class Child extends Parent {
  constructor(x, y, z) {
    // 本質上就是調用了超類
    super(x, y);
    this.z = z; // 正確
  }

  say() {
    console.log(this.x, this.y, this.z);
  }
}

const o = new Child(1, 2, 3);
console.log(o);
o.say(); //1,2,3

首先我們定義了一個父類Parent,在它的構造方法中定義了x,y兩個屬性,之后我們通過extends讓Child類繼承了Parent,並在Child的構造方法中執行了super,這里本質上其實就是調用了父類Parent的構造函數方法,只是在執行superthis指向了Child實例,這也讓Child實例o順利繼承了Parent上定義的屬性。我們可以輸出實例o,可以看到它確實繼承了來着Parent的屬性。

super本意其實就是超類,所有被繼承的類都可以叫超類,也就是我們長理解的父類,它並不是一個很復雜的概念,這里就簡單復習下。

肆 ❀ 貳 不要修改props以及如何修改props

在上文中,我們介紹了props與State的基本作用與區別,一個組件可以在內部定義自己需要的屬性,也可以接受外部傳遞的屬性。事實上,比如父子組件結構,父組件定義的State也能作為props傳遞給子組件使用,只是對於不同組件它的含義不同,這也對應了上文瀑布的比喻,水流由props與State構成就是這個意思了。

我們說State是私有屬性,雖然它可以傳遞給其它組件作為props使用,但站在私有的角度,我雖然大方的給你用,那你就應該只使用而不去修改它,這就是props第一准則,props應該像純函數那樣,只使用傳遞的屬性,而不去修改它(想改也改不掉,改了就報錯)。

為什么這么說,你想想,react本身就是單向數據流,父傳遞數據給子使用,如果在子組件內隨意修改父傳遞的對象反過來影響了父,那這不就亂套了嗎。

那么問題來了,如果父傳了屬性給子,子真的要改怎么辦?也不是沒辦法,第一我們可以在父提供props同時,也提供一個修改props的方法過去給子調用,子雖然是調用點,但本質執行的是父的方法,這是可行的。第二點,將傳遞進來的props復制一份,自己想怎么玩就怎么玩,也不是不可以,比如:

function Child(props) {
  // 拷貝一份自己玩
  let num = props.num;
  num++;
  return <div>{num}</div>;
}

class Parent extends React.Component {
  constructor(props) {
    super(props);
    this.state = { num: 1 };
  }
  render() {
    return (
      <div>
        <Child num={this.state.num} />
      </div>
    );
  }
}
ReactDOM.render(<Parent />, document.getElementById("root"));

當然如果子組件也是class組件也可以,還是這個例子,只是修改了Child部分:

class Child extends React.Component {
  constructor(props) {
    super(props);
    // 將傳遞進來的props賦予子組件的state
    this.state = {
      num:props.num
    }
  }
  render() {
    return <div>{++this.state.num}</div>;
  }
}

再或者直接賦值成this上的一條屬性:

class Child extends React.Component {
  constructor(props) {
    super(props);
  }
  // 將傳遞進來的props賦予給this
  num = this.props.num;
  render() {
    return <div>{++this.num}</div>;
  }
}

以上便是復制一份的常規操作,我們再來看看父提供修改方法的做法:

class Child extends React.Component {
  render() {
    return (
      <div>
        <div>{this.props.num}</div>
        <button type="button" onClick={() => this.props.onClick()}>
          點我
        </button>
      </div>
    );
  }
}

class Parent extends React.Component {
  constructor(props) {
    super(props);
    this.state = { num: 1 };
  }
  // 傳遞給子組件使用的方法
  handleClick() {
    // 拷貝了state中num
    let num_ = this.state.num;
    // 自增
    num_ += 1;
    // 更新state中的num
    this.setState({ num: num_ });
  }
  render() {
    return (
      <div>
        <Child num={this.state.num} onClick={() => this.handleClick()} />
      </div>
    );
  }
}
ReactDOM.render(<Parent />, document.getElementById("root"));

我們來解釋下這段代碼,我們在Parent中定義了state,其中包含num變量,以及定義了handleClick方法,在

<Child num={this.state.num} onClick={() => this.handleClick()} />這行代碼中,我們將state中的numhandleClick分別以numonClick這兩個變量名傳遞進去了。

對於事件定義react有個規則,比如我們傳遞給子組件的變量名是on[Click],那么具體方法定義名一般以handle[click]來命名,簡單點說,on[event]與handle[event]配對使用,event就是代表你事件具體含義的名字,有一個統一的規則,這樣也利於同事之間的代碼協作。

在子組件內部,我們通過props能訪問到傳遞的num與onClick這兩個屬性,我們將其關聯到dom中,當點擊按鈕就會執行父組件中的handleClick方法。有同學可能注意到handleClick中更新num的操作了,按照我們常規理解,直接this.state.num++不就行了嗎,很遺憾,這是react需要注意的第二點,我們無法直接修改state,比如如下行為都不被允許:

// 直接修改不允許
this.state.num = 2;
// 同理,這也是直接修改了state,也不被允許
this.setState({ num: this.state.num++ });

官方推薦做法,同樣也是將state中你要修改的部分拷貝出來,操作完成,再利用setState更新。

如果你了解過vue,在深入響應式原理一文中,也有類似的要求,比如請使用Vue.set(object, propertyName, value)去更新某個對象中的某條屬性,而不是直接修改它,否則你的修改可能並不會觸發視圖更新,其實都是差不多的道理,這里就順帶一提了。

OK,到這里我們對於這一塊知識點做個小結,props與state構成了react單向數據流的數據部分,同為屬性,只是一個私有一個是從外部傳遞的而已。其次,props只讀不可修改,若要修改請使用類似拷貝的折中方法,state除了拷貝外還得通過setState重新賦值。前面也說了,props就是外部傳遞的state,所以兩者都不能直接修改也不是不無道理,記住這點就好了。

伍 ❀ 總

現在是凌晨1點半(封面圖也是應景了),其實寫到這里,第二部分知識我想說的也都差不多了,看了眼篇幅,四千多字,再長一些知識點可能也有點多了,所以這篇就先介紹到這里。怎么說呢,關於文章的編寫我心里其實還是會有遺憾的,我畢竟只是個初學者,實戰項目經驗還遠遠不足,很多東西還不能從根源去解釋清楚,比如setState可能是異步行為,所以不要用state變化作為你部分邏輯的執行判斷條件,舉個例子:

class Parent extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      bol: true,
    };
  }
  // 這是生命周期函數
  componentDidMount(){
    for (let i = 0; i < 5; i++) {
      if (this.state.bol) {
        console.log(i);
        this.setState({ bol: false });
      }
    }
    console.log(this.state.bol);//true
  }
  // 這也是生命周期函數
  componentDidUpdate(){
    console.log(this.state.bol);//false
  }

  render() {
    return null;
  }
}
ReactDOM.render(<Parent />, document.getElementById("root"));

我們預期的是在輸出i為0之后,就修改bol狀態,之后循環無法再次進入這段代碼,但很遺憾,for循環會完整執行完畢並輸出0-1-2-3-4,直到在生命周期componentDidUpdate中我們才捕獲到修改成功的狀態。遺憾的是我目前的經驗還不足以將這塊知識吃透,沒吃透的東西我不會寫,所以這里算留個坑吧,之后一定會單獨寫一篇文章介紹state的問題,把這塊弄情況,那么這篇文章就先說到這里了,本文結束。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM