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的實現原理:


- 通過is函數對兩個參數進行比較,判斷是否相同,相同直接返回true:基本數據類型值相同,同一個引用對象都表示相同
- 如果兩個參數不相同,判斷兩個參數是否至少有一個不是引用類型,存在即返回false,如果兩個都是引用類型對象,則繼續下面的比較;
- 判斷兩個不同引用類型對象是否相同,先通過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官方指南

按需載入的配置
你可以理解為是懶載入,就是在需要的時候才載入組件插件。配置步驟如下:
- 安裝react-app-rewired取代react-scripts,這是可以擴展webpack的配置 ,類似vue.config.jsnpm install [email protected] babel-plugin-import –save npm install customize-cra less less-loader –save 複製程式碼
- 新建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 }], ) ); 複製程式碼
- 修改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 這種模式下的兩個角色,Provider
和Consumer
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 複製程式碼