React.js 新手快速入門 – 進階篇

  • 2019 年 11 月 26 日
  • 筆記

react.js中的組件化

容器組件 VS 展示組件

在 react 中組件化是普遍存在的,但是組件化也是分類型的,接下來將介紹容器組件與展示組件,這2者同為組件,卻有着不同的功效,記住下面這段話,有助於幫你理解容器組件與展示組件:

基本原則:容器組件負責數據獲取與處理;展示組件負責根據全局props展示信息

import React, { Component } from "react";  // 容器組件  export default class CommentVs extends Component {      constructor(props) {          super(props);          this.state = {              comments: []          };      }      // 生命周期函數      componentDidMount() {          setTimeout(() => {              this.setState({                  comments: [                      { body: "react is very good", author: "facebook" },                      { body: "vue is very good", author: "youyuxi" }                  ]              });          }, 1000);      }      render() {          return (              <div>              {this.state.comments.map((c, i) => (                  <Comment key={i} data={c} />              ))}              </div>          );      }  }    // 展示組件  function Comment({ data }) {      return (          <div>          <p>{data.body}</p>          <p> --- {data.author}</p>          </div>      );  }  複製代碼

PureComponent 組件

什麼是PureComponent呢,其實就是定製化後的shouldComponentUpdate的加強Component(內部實現淺比較)

import React, { Component, PureComponent } from "react";    // shouldComponentUpdate的加強版  class PureComponentTest extends PureComponent {      constructor(props) {          super(props);          this.state = {              comments: [                  { body: "react is very good", author: "facebook" },                  { body: "vue is very good", author: "youyuxi" }              ]          };      }      shouldComponentUpdate(nextProps) {          if (              nextProps.data.body === this.props.data.body &&              nextProps.data.author === this.props.data.author          ) {              return false;          }          return true;      }        render() {          console.log("render");          return (              <div>                  <p>{this.props.body}</p>                  <p>------{this.props.author}</p>              </div>          );      }  }    export default PureComponentTest;  複製代碼

React.PureComponent的實現原理:

  1. 通過is函數對兩個參數進行比較,判斷是否相同,相同直接返回true:基本數據類型值相同,同一個引用對象都表示相同
  2. 如果兩個參數不相同,判斷兩個參數是否至少有一個不是引用類型,存在即返回false,如果兩個都是引用類型對象,則繼續下面的比較;
  3. 判斷兩個不同引用類型對象是否相同,先通過Object.keys獲取到兩個對象的所有屬性,具有相同屬性,且每個屬性值相同即兩個對相同(相同也通過is函數完成)

所謂的淺比較,指的就是這行代碼!is(objA[keysA[i]], objB[keysA[i]])。可以看到,在比較兩個對象中屬性的屬性值的時候,是直接採用Object.is的方式進行的比較,如果對應屬性值恰好是基本類型值當然沒有問題,但是如果,恰好對象中的該屬性的屬性值是引用類型的值,那麼比較的仍舊是引用,而不是對象的外形。於是可能出現這種情況,當ObjA和objB的對象外形一致,按道理說不需要更新,但是由於其中某個相同屬性的屬性值是引用類型,而他們雖然外形也是一致的,但是引用不同,那麼!is(objA[keysA[i]], objB[keysA[i]])仍舊會返回true,最終導致shallowEqual函數返回false(這樣shouldComponentUpdate方法會返回true),從而導致組件出現無意義的更新。

那麼為什麼這裡會採用「淺比較」呢?這其實也是出於對於性能的考量。我們都知道,在js中,對引用類型外形的比較,實際上是需要通過遞歸比較才能完成(深複製引用類型也需要通過遞歸完成)。而在組件更新判斷的生命周期中不斷執行遞歸操作去比較先後的props和state對象,毫無疑問會產生較大的性能開銷。所以這裡只能折中,採用淺比較的方式。當然副作用就是,仍可能出現沒有必要的重新渲染(也就是兩個對象的外形一致,但其中的某些屬性是引用類型,這樣即使引用類型屬性值的外形也是一致的,淺比較依舊判定這兩個對象不同,從而導致多餘的重新渲染)。

React.memo 函數式組件

React.memo是 React v16.6.0 之後的版本,可以使用 React.memo 讓函數式的組件也有PureComponent的功能

const Joke = React.memo(() => (      <div>      {/* ||如果value為空,則顯示 loding*/}      {this.props.value || 'loading...' }      </div>  ));  複製代碼

ant-design組件庫的使用

首先給對應項目工程安裝依賴,執行命令 npm install antd --save

簡單示例,button按鈕的使用

import React, { Component } from 'react'  // 導入antd 按鈕組件  import Button from 'antd/lib/button'  // 導入antd 樣式表  import "antd/dist/antd.css"  class ButtonTest extends Component {      render() {          return (<div className="App">              {/*使用antd button 組件*/}              <Button type="primary">Button</Button>          </div>          )      }  }  export default ButtonTest  複製代碼

更多內容請參考ant design官方指南

按需加載的配置

你可以理解為是懶加載,就是在需要的時候才加載組件插件。配置步驟如下:

  1. 安裝react-app-rewired取代react-scripts,這是可以擴展webpack的配置 ,類似vue.config.jsnpm install [email protected] babel-plugin-import –save npm install customize-cra less less-loader –save 複製代碼
  2. 新建config-overrides.js文件,內容為const { override, fixBabelImports,addBabelPlugins } = require("customize-cra"); module.exports = override( // antd按需加載 fixBabelImports( "import", { libraryName: "antd", libraryDirectory: "es", style: "css" } ), addBabelPlugins( ['@babel/plugin-proposal-decorators', { legacy: true }], ) ); 複製代碼
  3. 修改package.json內的scripts內容如下:"scripts": { "start": "react-app-rewired start", "build": "react-app-rewired build", "test": "react-app-rewired test", "eject": "react-app-rewired eject" }, 複製代碼

同學們可以自己對着項目修改調試。

什麼是高階組件

React里已經有了HOC(Higher-Order Components)的概念,也就是高階組件,高階組件其實是返回另外一個組件,產生的新的組件可以對屬性進行包裝,甚至重寫部分生命周期。看個例子

const jpsite = (Component) => {      const NewComponent = (props) => {          return <Component {...props} name="開課吧高階組件" />;      };      return NewComponent;  };  複製代碼

上面 jpsite 組件,其實就是代理了一個Component,只是給原 Component 多傳遞了一個name參數

高階組件的鏈式調用

import React, { Component } from 'react'  import {Button} from 'antd'    const withName = (Component) => {      const NewComponent = (props) => {          return <Component {...props} name="開課吧高階組件" />;      };      return NewComponent;  };    const withLog = Component=>{      class NewComponent extends React.Component{          render(){              return <Component {...this.props} />;          }          componentDidMount(){              console.log('didMount',this.props)          }      }      return NewComponent  }    class App extends Component {      render() {          return (              <div className="App">              <h2>hi,{this.props.name}</h2>              <Button type="primary">Button</Button>              </div>          )      }  }    export default withName(withLog(App))  複製代碼

withName(withLog(App))就是鏈式調用的方式

高階組件的裝飾器寫法

ES6裝飾器可用於簡化高階組件寫法,首先安裝插件 npm install --save-dev @babel/plugin-proposal-decorators 然後config-overrides.js添加如下內容

實例Demo如下:

import React, { Component } from "react";    // 高階組件  const withName = Comp => {    // 甚至可以重寫組件聲明周期    class NewComponent extends Component {      componentDidMount() {        console.log("do something");      }      render() {        return <Comp {...this.props} name="高階組件試用介紹" />;      }    }      // 假設通過某種特殊手段獲取了本節課名字    return NewComponent;  };    const withLog = Comp => {    console.log(Comp.name + "渲染了");    return props => <Comp {...props} />;  };    @withLog  @withName  @withLog  class Jpsite extends Component {    render() {      return (        <div>          {this.props.stage} - {this.props.name}        </div>      );    }  }    export default Jpsite;    複製代碼

@withName @withLog三者連在一起的寫法就是裝飾器寫法了,效果等同於withName(withLog(App))這種鏈式調用的方式

組件跨層級的上下文通信

組件跨層級通信可使用Context 這種模式下的兩個角色,ProviderConsumer Provider為外層組件,用來提供數據;內部需要數據時用Consumer來讀取

import React, { Component } from "react";    // 1. 創建上下文  const Context = React.createContext();    const store = {    // 指定6611尾號用戶中獎    name: "恭喜你中到了一等獎",    sayHi() {      console.log(this.name);    }  };    const withProvider = Comp => props => (    <Context.Provider value={store}>      <Comp {...props} />    </Context.Provider>  );    const withConsumer = Comp => props => (    <Context.Consumer>      {/* 必須內嵌一個函數 */}      {value => <Comp {...props} value={value} />}    </Context.Consumer>  );    @withConsumer  class Inner extends Component {    render() {      console.log('Inner');      return <div>{this.props.value.name}</div>;    }  }    @withProvider  class ContextSample extends Component {    render() {      console.log('ContextSample');      return <div><Inner></Inner></div>;    }  }    export default ContextSample  複製代碼

React.js 新特性Hook

Hook是React16.8 的一個新增項,它可以讓你在不編寫 class的情況下使用 state內部狀態以及其他的 React特性。

State Hook – 狀態鉤子

// 使用State Hook  import React, { useState } from "react";      export default function HooksTest() {      // useState(initialState),接收初始狀態,返回一個狀態變量和它的更新函數,屬性名自定義      // 聲明一個叫 "count" 的 state 變量      const [count, setCount] = useState(0);      return (          <div>          <p>You clicked {count} times</p>          {/*調用setCount修改狀態count*/}          <button onClick={() => setCount(count + 1)}>Click me</button>          </div>      );  }  複製代碼

Effect Hook – 副作用鉤子

數據獲取,設置訂閱,以及手動更改React 組件中的 DOM, 都屬於副作用。

// 使用 useEffect 副作用鉤子  import React, { useState, useEffect } from "react";      useEffect(() => {          // Update the document title using the browser API          document.title = `您點擊了 ${count} 次`;  });  複製代碼

除了以上類型的Hook外,還有自定義Hook其他Hook,更多內容可以參考=>Hook 新特性指南

React.js 新特性Context

從之前的學習中,我們知道在一個典型的 React 應用中,數據是通過 props 屬性自上而下(由父及子)進行傳遞的,但這種做法對於某些類型的屬性而言是極其繁瑣的(例如:地區偏好,UI 主題),這些屬性是應用程序中許多組件都需要的。Context提供了一種在組件之間共享此類屬性值的方式,而不必顯式地通過組件樹的逐層傳遞 props

更多內容可以參考=>Context新特性指南

自己設計與實現一個組件

設計想法來源於 Ant Design Form表單

實現功能如下:

  • 擁有Form表單的布局與提交功能
  • FormItem收集錯誤信息
  • Input輸入框增加前綴圖標
  • 提供Input輸入控件提供事件處理、表單校驗功能
import React, { Component } from "react";  import { Icon } from "antd";    // hoc:包裝用戶表單,增加數據管理能力、校驗  function kFormCreate(Comp) {    return class NewComp extends Component {      constructor(props) {        super(props);        this.options = {}; //字段選項設置        this.state = {}; //各字段值      }        // 處理表單項輸入事件      handleChange = e => {        const { name, value } = e.target;        this.setState(          {            [name]: value          },          () => {            // 數值變化後再校驗            this.validateField(name);          }        );      };        // 表單項校驗      validateField = field => {        const rules = this.options[field].rules;        //只要任何一項失敗就失敗        const ret = rules.some(rule => {          if (rule.required) {            //僅驗證必填項            if (!this.state[field]) {              // 校驗失敗              this.setState({                [field + "Message"]: rule.message              });              return true; // 若有校驗失敗,返回true            }          }        });        if (!ret) {          // 沒失敗,校驗成功          this.setState({ [field + "Message"]: "" });        }        return !ret;      };        // 校驗所有字段      validate = cb => {        const rets = Object.keys(this.options).map(field =>          this.validateField(field)        );        // 如果校驗結果數組中全部為true,則校驗成功        const ret = rets.every(v => v === true);        cb(ret);      };        getFieldDec = (field, option, InputComp) => {        this.options[field] = option;          return (          <div>            {React.cloneElement(InputComp, {              name: field, //控件name              value: this.state[field] || "", //控件值              onChange: this.handleChange, //change事件處理              onFocus: this.handleFocus // 判斷控件是否獲得焦點            })}            {/* {this.state[field + "Message"] && (              <p style={{ color: "red" }}>{this.state[field + "Message"]}</p>            )} */}          </div>        );      };        //      handleFocus = e => {        const field = e.target.name;        this.setState({          [field + "Focus"]: true        });      };      // 判斷組件是否被用戶點過      isFieldTouched = field => !!this.state[field + "Focus"];        getFieldError = field => this.state[field + "Message"];        render() {        return (          <Comp            {...this.props}            getFieldDec={this.getFieldDec}            value={this.state}            validate={this.validate}            isFieldTouched={this.isFieldTouched}            getFieldError={this.getFieldError}          />        );      }    };  }    class FormItem extends Component {    render() {      return (        <div className="formItem">          {this.props.children}          {this.props.validateStatus === "error" && (            <p style={{ color: "red" }}>{this.props.help}</p>          )}        </div>      );    }  }    class KInput extends Component {    render() {      return (        <div>          {/* 前綴圖標 */}          {this.props.prefix}          <input {...this.props} />        </div>      );    }  }    @kFormCreate  class KFormSample extends Component {    onSubmit = () => {      this.props.validate(isValid => {        if (isValid) {          alert("校驗成功,提交登錄");          console.log(this.props.value);        } else {          alert("校驗失敗");        }      });    };      render() {      const { getFieldDec, isFieldTouched, getFieldError } = this.props;        const userNameError = isFieldTouched("uname") && getFieldError("uname");      const passwordError = isFieldTouched("pwd") && getFieldError("pwd");        return (        <div>          <FormItem            validateStatus={userNameError ? "error" : ""}            help={userNameError || ""}          >            {getFieldDec(              "uname",              {                rules: [{ required: true, message: "請填寫用戶名" }]              },              <KInput type="text" prefix={<Icon type="user" />} />            )}          </FormItem>          <FormItem            validateStatus={passwordError ? "error" : ""}            help={passwordError || ""}          >            {getFieldDec(              "pwd",              {                rules: [{ required: true, message: "請填寫用戶名" }]              },              <KInput type="password" prefix={<Icon type="lock" />} />            )}          </FormItem>            <button onClick={this.onSubmit}>登錄</button>        </div>      );    }  }    export default KFormSample  複製代碼