React學習筆記(三)—— 組件高級

一、列表和keys

1.1、Lists and Keys (列表和鍵)

首先,我們回顧一下在javascript中怎麼去變換列表。

下面了的代碼,我們用到了數組函數的map方法來實現數組的每一個值變成它的2倍,同時返回一個新數組,最後打印出了這個數組:

const numbers = [1,2,3,4,5];
const doubled = numbers.map(number=>number * 2);
console.log(doubled);

最終在控制檯的打印結果是:[2,4,6,8,10]。

在React中,轉換一個數組到列表,幾乎是相同的。

下面,我們依次通過調用數組的map方法,並返回一個用li標籤包含數組值當元素,最後分配它們到listItems數組裏面:

const numbers = [1,2,3,4,5];
const listItems = numbers.map(number => <li>{number}</li>);

現在我們把完整的listItems用一個ul標籤包起來,同時render it to the DOM

ReactDOM.render(
<ul>{listItems}</ul>,
document.getElementById('root')
);

這樣就會在頁面中顯示一個帶列表符號的ul列表,項目編號是從1到5。

1.2、Basic List Component(一個基礎的列表組件)

我們經常會在一個組件裏面輸出一個列表elements。

好,我們來寫一個組件實現前面同樣的功能,這個組件接受一個數字數組,最後返回一個無序列表。

    function NumberList(props){
        const numbers = props.numbers;
        const listItems = numbers.map(number => <li>{number}</li>);
        return (
            <ul>
                {listItems}
            </ul>
        );
    };
    const numbers = [1,2,3,4,5];
    ReactDOM.render(
        <NumberList numbers={numbers}/>,
        document.getElementById('root')
    );

如果你運行上面的代碼,你將會得到一個警告:你需要爲每一個li元素提供一個key屬性,這個“Key”字符串屬性當你創建一個列表元素的時候必須添加。我們將在接下來討論一下它爲什麼這麼重要。

讓我們在numbers.map()分配一個key屬性到列表裏面的li標籤裏來解決上面給出的警告提示問題,代碼如下:

    function NumberList(props){
        const numbers = props.numbers;
        const listItems = numbers.map(number => <li key={number.toString()}>{number}</li>);
        return (
            <ul>{listItems}</ul>
        );
    };
    const numbers = [1,2,3,4,5];
    ReactDOM.render(
        <NumberList numbers={numbers} />,
        document.getElementById('root')
    );

1.3、Keys(如何設置的key屬性值)

那我們爲什麼非要需要這個key屬性呢?其實這個key屬性可以幫助React確定一下那個列表選項改變了、是新增加的、或者被刪除了,反正這個key屬性就是用來讓react跟蹤列表在過去的時間發生了什麼變化。key屬性值應該在數組裏面指定,這樣就能保證列表元素就能有一個穩定的身份驗證值。

    const numbers = [1,2,3,4,5];
    const listItems = numbers.map(number => <li key={number.toString()}>{number}</li>);

最好的方法設置key值就是指定一個獨一無二的字符串值來把當前列表元素同它的兄弟列表元素分離開來。但是通常情況下,你的後臺給你的接口數據中都應該有一個當前數據爲一個的”id”值,那麼你就可以用這個id值來設置key屬性值。代碼大概像這樣子:

const todoItems = todos.map(todo => <li key={todo.id}>{todo.text}</li>);

如果你的接口沒有這樣的一個id值來確定列表元素的key屬性,那麼最後的辦法就是把當前列表的元素的索引值設置爲key屬性值了。如:

    const todoItems = todos.map((todo,index) => (
        //只有在todo數據裏面沒有獨一無二的id值的情況才這麼做
        <li key={index}>
            {todo.text}
        </li>
    ));

我們不推薦你使用索引值來作爲key屬性值,因爲你的列表重新排序的時候,這樣會嚴重使程序變得很慢。如果你願意的話,可以在這裏(in-depth explanation about why keys are necessary)得到更多有關的信息!

1.4、Extracting Components with Keys(當我們提取一個組件到另一個組件的時候,需要注意怎麼管理key)

key屬性只有在數組數據環境中才有意義,其它地方是沒有意義的。

例如,當我們實現一個ListItem組件的時候,這個組件封裝了一個li元素,那麼我們不應該在li元素上直接設置key屬性,因爲沒有意義,key是用來跟蹤數組纔有意義,於是我們在NumberList組件使用到ListItem組件的時候,在數組方法裏面設置key屬性纔有意義。好,我們先來看一個錯誤設置key屬性的版本:

    function ListItem(props){
        const value = props.value;
        return (
            //這裏是錯誤的,因爲這裏不需要指定key屬性
            <li key={value.toString()}>
                {value}
            </li>
        );
    };
    function NumberList(props){
        const numbers = props.numbers;
        const listItems = numbers.map(number => (
            //這裏也是錯誤的,因爲這裏纔是真的需要指定key屬性值的地方
            //記住一個要點就是:key屬性只會在用到有關js處理數組有關的環境中用到
            <ListItem value={number} />
        ));
        return (
            <ul>
                {listItems}
            </ul>
        );
    };
    const numbers = [1,2,3,4,5];
    ReactDOM.render(
        <NumberList numbers={numbers} />,
        document.getElementById('root')
    );

正確地設置key屬性版本的例子在下面:

    function ListItem(props){
        //正確的,這兒不需要設置key屬性
        return (
            <li>{props.value}</li>
        );
    };
    function NumberList(props){
        const numbers = props.numbers;
        const listItems = numbers.map((number) => (
            //正確,這兒纔是真的需要設置key屬性的地方
            <ListItem key={number.toString()} value={number}/>
        ));
        return (
            <ul>
                {listItems}
            </ul>
        );
    };
    const numbers = [1,2,3,4,5];
    ReactDOM.render(
        <NumberList numbers={numbers} />,
        document.getElementById('root')
    );

爲了幫你這裏理解這一層,我自己的理解就是:並不是渲染到頁面中的li標籤需要key屬性,(同時li標籤也是沒有關係的,我們在這裏之所有用到li標籤,只是更形象的說明問題,其實你也可以用div等等其它標籤)之所要設置key屬性,是React內部用來方便管理一個數組數據,跟蹤數組裏面的每一個數據!所以一個最好的法則就是,凡是需要調用map方法的時候你使用key屬性就對了!

1.5、Keys Must Only Be Unique Among Siblings(key屬性值相對於兄弟數據來說是獨一無二的,記住只是相對於兄弟數據,其它數據沒有關係)

key屬性值只會跟當前(同一個)數組數據之間是獨一無二的,而不用是全局獨一無二的,例如,有兩個數組,那麼它們的key就可以是一樣的。如:

    function Blog(props){
        const sidebar = (
            <ul>
                {props.posts.map((post) => (
                    <li key={post.id}>
                        {post.title}
                    </li>
                ))}
            </ul>
        );
        const content = props.posts.map((post) => (
            <div key={post.id}>
                <h3>{post.title}</h3>
                <p>{post.content}</p>
            </div>
        ));
        return (
            <div>
                {sidebar}
                <hr />
                {constent}
            </div>
        );
    };
    const posts = [
      {id: 1, title: 'Hello World', content: 'Welcome to learning React!'},
      {id: 2, title: 'Installation', content: 'You can install React from npm.'}
    ];
    ReactDOM.render(
      <Blog posts={posts} />,
      document.getElementById('root')
    );

key屬性只是給react一個用於跟蹤數據的線索而已,並不是傳遞給組件的,如果你需要個組件設置一樣一個屬性,那麼可以用不同的屬性名代替:

    const content = posts.map((post) => (
        <Post key={post.id} id={post.id} title={post.title} />
    ));

在這個例子中,Post組件可以讀id屬性,但是不能讀key屬性。

二、受控組件與非受控組件

2.1、受控組件

如果一個表單元素的值是由React 來管理的,那麼它就是一個受控組件。React 組件渲染表單元素,並在用戶和表單元素髮生交互時控制表單元素的行爲,從而保證組件的 state 成爲界面上所有元素狀態的唯一來源對於不同的表單元素, React 的控制方式略有不同,下面我們就來看一下三類常用表單元素的控制方式。

2.1.1、文本框

文本框包含類型爲text 的input 無素和 textarea元素。它們受控的主要原理是,通過表單元素的 value屬性設置表單元素的值,通過表單元素的onChange 事件監聽值的變化,並將變化同步到React 組件的 state中。

LoginForm.js

import React, { Component } from "react";

export default class LoginForm extends Component {
  constructor(props) {
    super(props);
    this.state = {
      username: "",
      password: "",
    };
  }

  changeHandle = (e) => {
    let target = e.target;
    this.setState({ [target.name]: target.value });
  };

  handleSubmit = (e) => {
    console.log(
      "username:" + this.state.username + " password:" + this.state.password
    );
    e.preventDefault();
  };

  render() {
    return (
      <div>
        <div>
          <h2>用戶登錄</h2>
          <form onSubmit={this.handleSubmit}>
            <fieldset>
              <legend>用戶信息</legend>
              <p>
                <label>帳號:</label>
                <input
                  type="text"
                  name="username"
                  value={this.state.username}
                  onChange={this.changeHandle}
                />
              </p>
              <p>
                <label>密碼:</label>
                <input
                  type="password"
                  name="password"
                  onChange={this.changeHandle}
                  value={this.state.password}
                />
              </p>
              <p>
                <button>登錄</button>
              </p>
            </fieldset>
          </form>
        </div>
        <div>
          uid:{this.state.username} - pwd:{this.state.password}
        </div>
      </div>
    );
  }
}

運行結果:

用戶名和密碼兩個表單元素的值是從組件的 state中獲取的,當用戶更改表單元素的值時,onChange事件會被觸發,對應的 handleChange處理函數會把變化同步到組件的 state,新的 state又會觸發表單元素重新渲染,從而實現對錶單元素狀態的控制。

這個例子還包含一個處理多個表單元素的技巧:通過爲兩個 input元素分別指定name屬性,使用同一個函數 handleChange處理元素值的變化,在處理函數中根據元素的name屬性區分事件的來源。這樣的寫法顯然比爲每一個 input元素指定一個處理函數簡潔得多。textarea的使用方式和input幾乎一致,這裏不再贅述。

2.1.2、列表

列表select元素是最複雜的表單元素,它可以用來創建一個下拉列表:

<select>
<option value="react">React</option>
<option value="redux">Redux</option>
<option selected value="mobx">MobX</ option>
</select>

通過指定selected屬性可以定義哪一個選項(option)處於選中狀態,所以上面的例子中,Mobx這一選項是列表的初始值,處於選中狀態。在React中,對select的處理方式有所不同,它通過在select上定義 value屬性來決定哪一個option元素處於選中狀態。這樣,對select的控制只需要在select 這一個元素上修改即可,而不需要關注 option元素。下面是一個例子:

import React, { Component } from "react";

export default class SelectForm extends Component {
  constructor(props) {
    super(props);
    this.state = {
      value: "mobx",
    };
  }

  changeHandle = (e) => {
    let target = e.target;
    this.setState({ value: target.value });
  };

  handleSubmit = (e) => {
    console.log("value:" + this.state.value);
    e.preventDefault();
  };

  render() {
    return (
      <div>
        <div>
          <h2>使用技術</h2>
          <form onSubmit={this.handleSubmit}>
            <fieldset>
              <legend>技術信息</legend>
              <p>
                <label>技術列表:</label>
                <select value={this.state.value} onChange={this.changeHandle}>
                  <option value="react">React</option>
                  <option value="redux">Redux</option>
                  <option value="mobx">MobX</option>
                </select>
              </p>
              <p>
                <button>提交</button>
              </p>
            </fieldset>
          </form>
        </div>
        <div>value:{this.state.value}</div>
      </div>
    );
  }
}

運行

 

 2.1.3、複選框與單選框

複選框是類型爲checkbox的input元素,單選框是類型爲 radio的input元素,它們的受控方式不同於類型爲text 的 input元素。通常,複選框和單選框的值是不變的,需要改變的是它們的checked 狀態,因此React 控制的屬性不再是value屬性,而是checked屬性。例如:

import React, { Component } from "react";

export default class SelectForm extends Component {
  constructor(props) {
    super(props);
    this.state = {
      react: false,
      redux: false,
      mobx: false,
    };
  }

  handleChange = (e) => {
    let target = e.target;
    this.setState({ [e.target.name]: target.checked });
  };

  handleSubmit = (e) => {
    console.log(JSON.stringify(this.state));
    e.preventDefault();
  };

  render() {
    return (
      <div>
        <div>
          <h2>使用技術</h2>
          <form onSubmit={this.handleSubmit}>
            <fieldset>
              <legend>技術信息</legend>
              <p>
                <label>技術列表:</label>
                <input
                  type="checkbox"
                  name="react"
                  value="react"
                  checked={this.state.react}
                  onChange={this.handleChange}
                />
                React
                <input
                  type="checkbox"
                  name="redux"
                  value="redux"
                  checked={this.state.redux}
                  onChange={this.handleChange}
                />
                Redux
                <input
                  type="checkbox"
                  name="mobx"
                  value="mobx"
                  checked={this.state.mobx}
                  onChange={this.handleChange}
                />
                Mobx
              </p>
              <p>
                <button>提交</button>
              </p>
            </fieldset>
          </form>
        </div>
        <div>{JSON.stringify(this.state)}</div>
      </div>
    );
  }
}

運行結果:

2.1.4、升級後的BBS

讓每個帖子支持編輯功能:

PostItem.js

import React, { Component } from "react";
import like from "./like.png";

export default class PostItem extends Component {
  constructor(props) {
    super(props);
    this.state = {
      editing: false,
      post: props.post,
    };
  }

  static getDerivedStateFromProps(props, state) {
    if (props.post.vote !== state.post.vote) {
      return { post: props.post };
    }
    return null;
  }

  handleVote = () => {
    this.props.onVote(this.props.post.id);
  };

  handleTitleChange = (e) => {
    const newPost = { ...this.state.post, title: e.target.value };
    this.setState({ post: newPost });
  };

  handleEditPost = (e) => {
    if (this.state.editing) {
      this.props.onSave({
        ...this.state.post,
        date: new Date().toLocaleString(),
      });
    }
    this.setState({
      editing: !this.state.editing,
    });
  };

  render() {
    const { post } = this.state;
    return (
      <li className="item">
        <div className="title">
          {this.state.editing ? (
            <form>
              <textarea
                value={post.title}
                onChange={this.handleTitleChange}
                cols="30"
                rows="3"
              />
            </form>
          ) : (
            post.title
          )}
        </div>
        <div>
          創建人:<span>{post.author}</span>
        </div>
        <div>
          創建時間:<span>{post.date}</span>
        </div>
        <div>
          <img src={like} alt="點贊" onClick={this.handleVote}></img>
          {post.vote}
        </div>
        <div>
          <button onClick={this.handleEditPost}>
            {this.state.editing ? "保存" : "編輯"}
          </button>
        </div>
      </li>
    );
  }
}

PostList.js

import React, { Component } from "react";

import PostItem from "./PostItem";

/**
 * 有狀態組件定義
 */
export class PostList extends Component {
  constructor(props) {
    super(props);
    //狀態初始化
    this.state = {
      posts: [], //所有帖子
    };
    this.timer = null; //時鐘,模擬後臺加載數據
    //將voteHandle函數中的this指向組件對象
    this.voteHandle = this.voteHandle.bind(this);
  }

  data = [
    {
      id: 1001,
      title: "百度蘿蔔快跑超百臺無人車落地武漢,訂單量突破200",
      author: "小明",
      date: "2023-03-01 12:12:18",
      vote: 0,
    },
    {
      id: 1002,
      title: "我國自主研製空間站雙光子顯微鏡首獲航天員皮膚三維圖",
      author: "小軍",
      date: "2022-12-15 23:15:26",
      vote: 0,
    },
    {
      id: 1003,
      title: "清華大學一教授團隊爲村民“打印”一棟住宅!",
      author: "小華",
      date: "2022-11-26 18:17:44",
      vote: 0,
    },
  ];

  //當組件掛載完成後
  componentDidMount() {
    //模擬後臺AJAX加載
    this.timer = setTimeout(() => {
      this.setState({
        posts: this.data,
      });
    }, 1000);
  }

  //當組件將被卸載時
  componentWillUnmount() {
    clearTimeout(this.timer);
  }

  //根據id點贊
  voteHandle(id) {
    //如果當前帖子的id是我們要投票的帖子,則生成一個新對象,並更新投票數
    //如果不是要找的帖子,則直接返回
    let newPosts = this.state.posts.map((item) =>
      item.id === id ? { ...item, vote: item.vote + 1 } : item
    );
    //更新狀態,刷新UI
    this.setState({ posts: newPosts });
  }

  saveHandle = (newPost) => {
    const newPosts = this.state.posts.map((item) =>
      item.id === newPost.id ? newPost : item
    );
    this.setState({ posts: newPosts });
  };

  render() {
    return (
      <div>
        <h2>帖子列表:</h2>
        {this.state.posts.map((item) => (
          <PostItem
            key={item.id}
            post={item}
            posts={this.state.posts}
            onVote={this.voteHandle}
            onSave={this.saveHandle}
          />
        ))}
      </div>
    );
  }
}

export default PostList;

運行結果:

2.2、非受控組件

2.2.1、使用非受控組件

在大多數情況下,我們推薦使用 受控組件 來處理表單數據。在一個受控組件中,表單數據是由 React 組件來管理的。另一種替代方案是使用非受控組件,這時表單數據將交由 DOM 節點來處理。

要編寫一個非受控組件,而不是爲每個狀態更新都編寫數據處理函數,你可以 使用 ref 來從 DOM 節點中獲取表單數據。

例如,下面的代碼使用非受控組件接受一個表單的值:

class NameForm extends React.Component {
  constructor(props) {
    super(props);
    this.handleSubmit = this.handleSubmit.bind(this);
    this.input = React.createRef();
  }

  handleSubmit(event) {
    alert('A name was submitted: ' + this.input.current.value);
    event.preventDefault();
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          Name:
          <input type="text" ref={this.input} />
        </label>
        <input type="submit" value="Submit" />
      </form>
    );
  }
}

因爲非受控組件將真實數據儲存在 DOM 節點中,所以在使用非受控組件時,有時候反而更容易同時集成 React 和非 React 代碼。如果你不介意代碼美觀性,並且希望快速編寫代碼,使用非受控組件往往可以減少你的代碼量。否則,你應該使用受控組件。

2.2.2、默認值

在 React 渲染生命週期時,表單元素上的 value 將會覆蓋 DOM 節點中的值。在非受控組件中,你經常希望 React 能賦予組件一個初始值,但是不去控制後續的更新。 在這種情況下, 你可以指定一個 defaultValue 屬性,而不是 value。在一個組件已經掛載之後去更新 defaultValue 屬性的值,不會造成 DOM 上值的任何更新。

render() {
  return (
    <form onSubmit={this.handleSubmit}>
      <label>
        Name:
        <input
          defaultValue="Bob"
          type="text"
          ref={this.input} />
      </label>
      <input type="submit" value="Submit" />
    </form>
  );
}

同樣,<input type="checkbox"> 和 <input type="radio"> 支持 defaultChecked<select> 和 <textarea> 支持 defaultValue

2.2.3、文件輸入

在 HTML 中,<input type="file"> 可以讓用戶選擇一個或多個文件上傳到服務器,或者通過使用 File API 進行操作。

<input type="file" />

在 React 中,<input type="file" /> 始終是一個非受控組件,因爲它的值只能由用戶設置,而不能通過代碼控制。

您應該使用 File API 與文件進行交互。下面的例子顯示瞭如何創建一個 DOM 節點的 ref 從而在提交表單時獲取文件的信息。

class FileInput extends React.Component {
  constructor(props) {
    super(props);
    this.handleSubmit = this.handleSubmit.bind(this);
    this.fileInput = React.createRef();
  }
  handleSubmit(event) {
    event.preventDefault();
    alert(
      `Selected file - ${this.fileInput.current.files[0].name}`
    );
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          Upload file:
          <input type="file" ref={this.fileInput} />
        </label>
        <br />
        <button type="submit">Submit</button>
      </form>
    );
  }
}

const root = ReactDOM.createRoot(
  document.getElementById('root')
);
root.render(<FileInput />);

三、React新特性

3.1、render新的返回類型

React16之前render方法必須返回單個元素,現在render可以返回多種不同的元素:

render() 方法是 class 組件中唯一必須實現的方法。

當 render 被調用時,它會檢查 this.props 和 this.state 的變化並返回以下類型之一:

  • React 元素。通常通過 JSX 創建。例如,<div /> 會被 React 渲染爲 DOM 節點,<MyComponent /> 會被 React 渲染爲自定義組件,無論是 <div /> 還是 <MyComponent /> 均爲 React 元素。
  • 數組或 fragments。 使得 render 方法可以返回多個元素。欲瞭解更多詳細信息,請參閱 fragments 文檔。
  • Portals。可以渲染子節點到不同的 DOM 子樹中。欲瞭解更多詳細信息,請參閱有關 portals 的文檔。
  • 字符串或數值類型。它們在 DOM 中會被渲染爲文本節點。
  • 布爾類型或 null。什麼都不渲染。(主要用於支持返回 test && <Child /> 的模式,其中 test 爲布爾類型。)

render() 函數應該爲純函數,這意味着在不修改組件 state 的情況下,每次調用時都返回相同的結果,並且它不會直接與瀏覽器交互。

如需與瀏覽器進行交互,請在 componentDidMount() 或其他生命週期方法中執行你的操作。保持 render() 爲純函數,可以使組件更容易思考。

注意

如果 shouldComponentUpdate() 返回 false,則不會調用 render()

3.1.1、返回數組

export default class Hi extends Component {
  render() {
    return [<span>Hello</span>, <span>React</span>, <span>render()!</span>];
  }
}

3.1.2、fragments

  • 可以將子列表分組,而無需向DOM添加額外節點
  • 簡單理解:空標籤
  • <React.Fragment></React.Fragment> 或 <></>
  • render() {
      return (
        <React.Fragment>
          <ChildA />
          <ChildB />
          <ChildC />
        </React.Fragment>
      )
    }
    • 以下面的代碼爲例,如果Columns組件返回多個td元素才能實現效果,但是如果我們在Columns組件中使用了div父元素,則會使td元素失效。Fragment則可以解決這個問題。
    • //table.js
      const Table = () => {
        render() {
          return (
            <table>
              <tr>
                <Columns />
              </tr>
            </table>
          )
        }
      }
      //columns.js
      const Columns = () => {
       render() {
          return (
            <div>
              <td>Hello</td>
              <td>World</td>
            </div>
          )
        }
      }
      //以上代碼輸出:
      <table>
        <tr>
          <div>
            <td>Hello</td>
            <td>World</td>
          </div>
        </tr>
      </table>
      //此時 td 是失效的,可以使用Fragemengt解決這個問題
      //用法:
      //columns.js
      const Columns = () => {
       render() {
          return (
            <React.Fragment>
              <td>Hello</td>
              <td>World</td>
            </React.Fragment>
          )
        }
      }
      //通過上面的方法我們就可以正確的輸出table啦:
      <table>
        <tr>
          <td>Hello</td>
          <td>World</td>
        </tr>
      </table>

      短語法

    • 可以使用一種新的,且更簡短的類似空標籤的語法來聲明 Fragments
    • <> </>
    • 不支持 key 或屬性
    • const Cloumns = () => {
      render() {
          return (
            <>
              <td>Hello</td>
              <td>World</td>
            </>
          )
        }
      }

      帶key 的Fragments

    • 使用顯式 <React.Fragment> 語法聲明的片段可能具有 key
    • key 是唯一可以傳遞給 Fragment 的屬性
    • function Glossary(props) {
        return (
          <dl>
            {props.items.map(item => (
              // 沒有`key`,React 會發出一個關鍵警告
              <React.Fragment key={item.id}>
                <dt>{item.term}</dt>
                <dd>{item.description}</dd>
              </React.Fragment>
            ))}
          </dl>
        )
      } 

3.1.3、Portals

Portal 提供了一種將子節點渲染到存在於父組件以外的 DOM 節點的優秀的方案。

ReactDOM.createPortal(child, container)

第一個參數(child)是任何可渲染的 React 子元素,例如一個元素,字符串或 fragment。第二個參數(container)是一個 DOM 元素。

通常來講,當你從組件的 render 方法返回一個元素時,該元素將被掛載到 DOM 節點中離其最近的父節點:

render() {
  // React 掛載了一個新的 div,並且把子元素渲染其中
  return (
    <div>      {this.props.children}
    </div>  );
}

然而,有時候將子元素插入到 DOM 節點中的不同位置也是有好處的:

render() {
  // React 並*沒有*創建一個新的 div。它只是把子元素渲染到 `domNode` 中。
  // `domNode` 是一個可以在任何位置的有效 DOM 節點。
  return ReactDOM.createPortal(
    this.props.children,
    domNode  );
}

一個 portal 的典型用例是當父組件有 overflow: hidden 或 z-index 樣式時,但你需要子組件能夠在視覺上“跳出”其容器。例如,對話框、懸浮卡以及提示框:

注意:

當在使用 portal 時, 記住管理鍵盤焦點就變得尤爲重要。

對於模態對話框,通過遵循 WAI-ARIA 模態開發實踐,來確保每個人都能夠運用它。

示例:

import React, { Component } from "react";
import ReactDOM from "react-dom";

export default class Hi extends Component {
  constructor(props) {
    super(props);
    this.container = document.createElement("div");
    document.body.appendChild(this.container);
  }

  render() {
    return ReactDOM.createPortal(<h2>Portal</h2>, this.container);
  }
}

運行結果:

卸載時需要移除

  componentWillUnmount() {
    document.body.removeChild(this.container);
  }

3.2、錯誤邊界

部分 UI 的異常不應該破壞了整個應用。爲了解決 React 用戶的這一問題,React 16 引入了一種稱爲 “錯誤邊界” 的新概念。 錯誤邊界是用於捕獲其子組件樹 JavaScript 異常,記錄錯誤並展示一個回退的 UI 的 React 組件,而不是整個組件樹的異常。錯誤組件在渲染期間,生命週期方法內,以及整個組件樹構造函數內捕獲錯誤。
componentDidCatch(error, info)

此生命週期在後代組件拋出錯誤後被調用。 它接收兩個參數:

  1. error —— 拋出的錯誤。
  2. info —— 帶有 componentStack key 的對象,其中包含有關組件引發錯誤的棧信息

componentDidCatch() 會在“提交”階段被調用,因此允許執行副作用。 它應該用於記錄錯誤之類的情況:

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    // 更新 state 使下一次渲染可以顯示降級 UI
    return { hasError: true };
  }

  componentDidCatch(error, info) {    // "組件堆棧" 例子:    //   in ComponentThatThrows (created by App)    //   in ErrorBoundary (created by App)    //   in div (created by App)    //   in App    logComponentStackToMyService(info.componentStack);  }
  render() {
    if (this.state.hasError) {
      // 你可以渲染任何自定義的降級 UI
      return <h1>Something went wrong.</h1>;
    }

    return this.props.children;
  }
}

React 的開發和生產構建版本在 componentDidCatch() 的方式上有輕微差別。

在開發模式下,錯誤會冒泡至 window,這意味着任何 window.onerror 或 window.addEventListener('error', callback) 會中斷這些已經被 componentDidCatch() 捕獲的錯誤。

相反,在生產模式下,錯誤不會冒泡,這意味着任何根錯誤處理器只會接受那些沒有顯式地被 componentDidCatch() 捕獲的錯誤。

注意

如果發生錯誤,你可以通過調用 setState 使用 componentDidCatch() 渲染降級 UI,但在未來的版本中將不推薦這樣做。 可以使用靜態 getDerivedStateFromError() 來處理降級渲染。

特別注意:

  • 事件處理 (比如調用了一個不存在的方法this.abc(),並不會執行componentDidCatch)
  • 異步代碼 (例如 setTimeoutrequestAnimationFrame 回調函數)
  • 服務端渲染
  • 錯誤邊界自身拋出來的錯誤 (而不是其子組件)

當render()函數出現問題時,componentDidCatch會捕獲異常並處理

此時,render()函數裏面發生錯誤,則 componentDidCatch 會進行調用,在裏面進行相應的處理
render() {
  let a = [1,2,3]
  let value = a[3].toString()   對 undefined 進行操作
  return (......)
}

防止 頁面 級別的崩潰~

示例:

import ErrorCom from "./ErrorCom";

const vnode = (
  <div>
    <ErrorCom />
    <Hi />
  </div>
);

默認情況:

 發生錯誤時整個應用崩潰

 改進,定義ErrorBoundary組件

import React, { Component } from "react";

export default class ErrorBoundary extends Component {
  state = { error: null };

  componentDidCatch(error, info) {
    this.setState({ error });
  }

  render() {
    if (this.state.error) {
      return (
        <div>
          <h2>發生了錯誤</h2>
          <div>{this.state.error && this.state.error.toString()}</div>
        </div>
      );
    }
    return this.props.children;
  }
}

修改index.js文件

const vnode = (
  <div>
    <ErrorBoundary>
      <ErrorCom />
    </ErrorBoundary>
    <Hi />
  </div>
);

發生錯誤時,僅當前控制失效了:

 3.3、自定義DOM屬性

React 16 之前會忽略不是把的HTML和SVG屬性,現在React會把不識別的屬性傳遞給DOM。

React16之前:

  <div cust-attr="someting"></div>
會被渲染成:

  <div></div>

React 16渲染出來的節點:

  <div cust-attr="someting"></div>

 3.4、組件的state

3.4.1、組件state

1,設計合適的state

state必須能代表一個組件UI呈現的完整狀態集,代表一個組件UI呈現的最小狀態集。

state必須能代表一個組件UI呈現的完整狀態集又可以分成兩類數據:用作渲染組件時使用到的數據的來源,用作組件UI展現形式的判斷依據:

class Hello extends Component {
    constructor(props) {
        super(props);
        this.state = {
            user: 'react', //用作渲染組件時使用到的數據的來源
            display: true //用作組件UI展現形式的判斷依據
        }
    }
    render() {
        return (
            <div>
                {
                    this.state.display ? <h1>{this.state.user}</h1> : <></>
                }
            </div>
        )
    }
}
export default Hello;

普通屬性:

在es6中,可以使用this.屬性名定義一個class的屬性,也可以說屬性是直接掛載在this下的一個變量。因此,state和props實際上也是組件的屬性,只不過是react在Component class中預定義好的屬性。除了state和props以外的其他組件屬性稱爲組件的普通屬性。

class Hello extends Component {
    constructor(props) {
        super(props);
        this.timer = null; //普通屬性
        this.state = {
            date: new Date()
        }
        this.updateDate = this.updateDate.bind(this);
    }
    componentDidMount(){
        this.timer = setInterval(this.updateDate, 1000);
    }
    componentWillUnmount(){
        clearInterval(this.timer);
    }
    updateDate(){
        this.setState({
            date: new Date()
        })
    }
    render() {
        return (
            <div>
                <h1>{this.state.date.toString()}</h1>
            </div>
        )
    }
}
export default Hello;

組件中用到的一個變量是否應該作爲state可以通過下面4條依據判斷:

  1. 這個變量是否通過props從父組件中獲取?如果是,那麼它不是一個狀態
  2. 這個變量是否在生命週期中都保持不變?如果是,那麼它不是一個狀態
  3. 這個變量是否可以通過其他狀態(state)或者屬性(props)計算得到?如果是,那麼它不是一個狀態
  4. 這個變量是否在組件的render方法中使用?如果不是,那麼它不是一個狀態,這種情況更適合定義爲組件的一個普通屬性

3.4.2、正確修改state

①不能直接修改state,需要使用setState()

②state的更新是異步的

React會將多次setState的狀態合併成一次狀態修改,不能依賴當前的state計算下一個state(props也是異步的)。

例如:連續兩次點擊加入購物車,實際數量只會加1,在React合併多次修改爲1次的情況下,相當於執行了:

Object.assign(
    previousState,
    {quantity: this.state.quantity + 1},
    {quantity: this.state.quantity + 1}
)

示例:

import React, { Component } from "react";

export default class ErrorCom extends Component {
  state = { n: 0 };

  clickHandle = () => {
    for (let i = 0; i < 100; i++) {
      this.setState(
        {
          n: this.state.n + 1,
        },
        () => {
          if (this.state.n === 300) {
            throw new Error("發生了錯誤");
          }
        }
      );
    }
  };

  render() {
    return (
      <div>
        <h2>組件</h2>
        點擊3次就異常了
        <button onClick={this.clickHandle}>{this.state.n}</button>
      </div>
    );
  }
}

上面的代碼雖然循環了100次,實際每次只增加了1。

這種情況下,可以使用另一個接收一個函數作爲參數的setState,這個函數有兩個參數,第一個是當前修改後的最新狀態的前一個狀態preState,第二個參數是當前最新的屬性props:

this.setState((preState,props) => ({
    quantity: preState.quantity + 1;
}))

3.4.3、state的更新是一個合併的過程

後設置的state會覆蓋前面的狀態,如果不存在則添加。

3.4.4、state與不可變對象

直接修改state,組件不會render;state包含的所有狀態都應該是不可變對象,當state中某個狀態發生變化時,應該重新創建這個狀態對象,而不是直接修改原來的狀態。創建新的狀態有以下三種方法:

狀態的類型是不可變類型(數字、字符串、布爾值、null、undefined):因爲狀態是不可變類型,所以直接賦一個新值即可
狀態的類型是數組:可以使用數組的concat或者es6的擴展語法,slice方法、filter方法。不能使用push、pop、shift、unshift、splice等方法修改數組類型的狀態,因爲這些方法會在原數組基礎上修改。

this.setState((preState) => ({
    arr: [...preState.arr,'react'];
}))
this.setState((preState) => ({
    arr: preState.arr.concat(['react'])
}))

狀態的類型是普通對象(不包含字符串、數組):使用ES6的Object.assgin方法或者對象擴展語法

Object.assign({},preState.owner,{name:"tom"});

或者

{...preState.owner,name:"tom"}

3.5、Axios

Axios 是一個基於 promise 的 HTTP 庫,可以用在瀏覽器和 node.js 中。

源代碼與英文幫助:https://github.com/axios/axios

3.5.1、特性

  • 從瀏覽器中創建 XMLHttpRequests
  • 從 node.js 創建 http 請求
  • 支持 Promise API
  • 攔截請求和響應
  • 轉換請求數據和響應數據
  • 取消請求
  • 自動轉換 JSON 數據
  • 客戶端支持防禦 XSRF

3.5.2、瀏覽器支持

3.5.3、安裝

使用 npm:

$ npm install axios

使用 bower:

$ bower install axios

使用 cdn:

<script src="https://unpkg.com/axios/dist/axios.min.js"></script>

3.5.4、案例

執行 GET 請求

// 爲給定 ID 的 user 創建請求
axios.get('/user?ID=12345')
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
});

// 上面的請求也可以這樣做
axios.get('/user', {
params: {
ID: 12345
}
})
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
});

執行 POST 請求

axios.post('/user', {
firstName: 'Fred',
lastName: 'Flintstone'
})
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
});

執行多個併發請求

function getUserAccount() {
return axios.get('/user/12345');
}

function getUserPermissions() {
return axios.get('/user/12345/permissions');
}

axios.all([getUserAccount(), getUserPermissions()])
.then(axios.spread(function (acct, perms) {
// 兩個請求現在都執行完成
}));

3.5.5、axios API

可以通過向 axios 傳遞相關配置來創建請求

axios(config)
// 發送 POST 請求
axios({
method: 'post',
url: '/user/12345',
data: {
firstName: 'Fred',
lastName: 'Flintstone'
}
});
// 獲取遠端圖片
axios({
method:'get',
url:'http://bit.ly/2mTM3nY',
responseType:'stream'
})
.then(function(response) {
response.data.pipe(fs.createWriteStream('ada_lovelace.jpg'))
});
axios(url[, config])
// 發送 GET 請求(默認的方法)
axios('/user/12345');

3.5.6、請求方法的別名

爲方便起見,爲所有支持的請求方法提供了別名

axios.request(config)
axios.get(url[, config])
axios.delete(url[, config])
axios.head(url[, config])
axios.options(url[, config])
axios.post(url[, data[, config]])
axios.put(url[, data[, config]])
axios.patch(url[, data[, config]])
注意

在使用別名方法時, urlmethoddata 這些屬性都不必在配置中指定。

3.5.7、併發

處理併發請求的助手函數

axios.all(iterable)
axios.spread(callback)

3.5.8、創建實例

可以使用自定義配置新建一個 axios 實例

axios.create([config])
const instance = axios.create({
baseURL: 'https://some-domain.com/api/',
timeout: 1000,
headers: {'X-Custom-Header': 'foobar'}
});

3.5.9、實例方法

以下是可用的實例方法。指定的配置將與實例的配置合併。

axios#request(config)
axios#get(url[, config])
axios#delete(url[, config])
axios#head(url[, config])
axios#options(url[, config])
axios#post(url[, data[, config]])
axios#put(url[, data[, config]])
axios#patch(url[, data[, config]])

3.5.10、請求配置

這些是創建請求時可以用的配置選項。只有 url 是必需的。如果沒有指定 method,請求將默認使用 get 方法。

{
// `url` 是用於請求的服務器 URL
url: '/user',

// `method` 是創建請求時使用的方法
method: 'get', // default

// `baseURL` 將自動加在 `url` 前面,除非 `url` 是一個絕對 URL。
// 它可以通過設置一個 `baseURL` 便於爲 axios 實例的方法傳遞相對 URL
baseURL: 'https://some-domain.com/api/',

// `transformRequest` 允許在向服務器發送前,修改請求數據
// 只能用在 'PUT', 'POST' 和 'PATCH' 這幾個請求方法
// 後面數組中的函數必須返回一個字符串,或 ArrayBuffer,或 Stream
transformRequest: [function (data, headers) {
// 對 data 進行任意轉換處理
return data;
}],

// `transformResponse` 在傳遞給 then/catch 前,允許修改響應數據
transformResponse: [function (data) {
// 對 data 進行任意轉換處理
return data;
}],

// `headers` 是即將被髮送的自定義請求頭
headers: {'X-Requested-With': 'XMLHttpRequest'},

// `params` 是即將與請求一起發送的 URL 參數
// 必須是一個無格式對象(plain object)或 URLSearchParams 對象
params: {
ID: 12345
},

// `paramsSerializer` 是一個負責 `params` 序列化的函數
// (e.g. https://www.npmjs.com/package/qs, http://api.jquery.com/jquery.param/)
paramsSerializer: function(params) {
return Qs.stringify(params, {arrayFormat: 'brackets'})
},

// `data` 是作爲請求主體被髮送的數據
// 只適用於這些請求方法 'PUT', 'POST', 和 'PATCH'
// 在沒有設置 `transformRequest` 時,必須是以下類型之一:
// - string, plain object, ArrayBuffer, ArrayBufferView, URLSearchParams
// - 瀏覽器專屬:FormData, File, Blob
// - Node 專屬: Stream
data: {
firstName: 'Fred'
},

// `timeout` 指定請求超時的毫秒數(0 表示無超時時間)
// 如果請求話費了超過 `timeout` 的時間,請求將被中斷
timeout: 1000,

// `withCredentials` 表示跨域請求時是否需要使用憑證
withCredentials: false, // default

// `adapter` 允許自定義處理請求,以使測試更輕鬆
// 返回一個 promise 並應用一個有效的響應 (查閱 [response docs](#response-api)).
adapter: function (config) {
/* ... */
},

// `auth` 表示應該使用 HTTP 基礎驗證,並提供憑據
// 這將設置一個 `Authorization` 頭,覆寫掉現有的任意使用 `headers` 設置的自定義 `Authorization`頭
auth: {
username: 'janedoe',
password: 's00pers3cret'
},

// `responseType` 表示服務器響應的數據類型,可以是 'arraybuffer', 'blob', 'document', 'json', 'text', 'stream'
responseType: 'json', // default

// `responseEncoding` indicates encoding to use for decoding responses
// Note: Ignored for `responseType` of 'stream' or client-side requests
responseEncoding: 'utf8', // default

// `xsrfCookieName` 是用作 xsrf token 的值的cookie的名稱
xsrfCookieName: 'XSRF-TOKEN', // default

// `xsrfHeaderName` is the name of the http header that carries the xsrf token value
xsrfHeaderName: 'X-XSRF-TOKEN', // default

// `onUploadProgress` 允許爲上傳處理進度事件
onUploadProgress: function (progressEvent) {
// Do whatever you want with the native progress event
},

// `onDownloadProgress` 允許爲下載處理進度事件
onDownloadProgress: function (progressEvent) {
// 對原生進度事件的處理
},

// `maxContentLength` 定義允許的響應內容的最大尺寸
maxContentLength: 2000,

// `validateStatus` 定義對於給定的HTTP 響應狀態碼是 resolve 或 reject promise 。如果 `validateStatus` 返回 `true` (或者設置爲 `null` 或 `undefined`),promise 將被 resolve; 否則,promise 將被 rejecte
validateStatus: function (status) {
return status >= 200 && status < 300; // default
},

// `maxRedirects` 定義在 node.js 中 follow 的最大重定向數目
// 如果設置爲0,將不會 follow 任何重定向
maxRedirects: 5, // default

// `socketPath` defines a UNIX Socket to be used in node.js.
// e.g. '/var/run/docker.sock' to send requests to the docker daemon.
// Only either `socketPath` or `proxy` can be specified.
// If both are specified, `socketPath` is used.
socketPath: null, // default

// `httpAgent` 和 `httpsAgent` 分別在 node.js 中用於定義在執行 http 和 https 時使用的自定義代理。允許像這樣配置選項:
// `keepAlive` 默認沒有啓用
httpAgent: new http.Agent({ keepAlive: true }),
httpsAgent: new https.Agent({ keepAlive: true }),

// 'proxy' 定義代理服務器的主機名稱和端口
// `auth` 表示 HTTP 基礎驗證應當用於連接代理,並提供憑據
// 這將會設置一個 `Proxy-Authorization` 頭,覆寫掉已有的通過使用 `header` 設置的自定義 `Proxy-Authorization` 頭。
proxy: {
host: '127.0.0.1',
port: 9000,
auth: {
username: 'mikeymike',
password:
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章