02基礎語法--005--React 技術詳解

目錄

(一)React 簡介

  • 主要作用是為 MVC 模式中的視圖(view)層構建界面視圖
  • 還可以以插件的形式作用于 Web 應用程序的非視圖部分,實現與其他 JavaScript 框架的整合

DOM,文檔對象模式

W3C組織推薦的處理可擴展標志語言的標準編程接口

DOM

1. 傳統的HTML 網頁開發

  1. 直接操作 DOM,需要非常大的開銷;
  2. 更新頁面內容或元素,需要將整個頁面重新繪制;

2. React 性能優化方案(刷新邏輯)

  1. React 底層設計了一個虛擬的 DOM;
  2. 虛擬的 DOM 與頁面真實的 DOM 進行映射;
  3. 當數據變化時
    1. React 會重新構建 DOM 樹;
    2. 通過底層的 diff 算法找到 DOM 的差異部分;
    3. 瀏覽器只需要更新變化的部分;

3. React 的跨平臺方案

借助虛擬的 DOM 技術來實現服務端應用、Web 應用和移動手機應用的跨平臺開發

4. React 數據的單向流向

  1. 數據默認從父節點傳到子節點;
  2. 父節點數據通過 props 傳遞到子節點,如果父節點的 props 值發生改變,那么其所有子節點也會執行重新渲染操作;
  3. 好處:使得組件足夠扁平,更加便于維護。

(二)React 組件詳解

2.1 React 組件基礎知識

組件定義

  • 組件是 React 的核心內容
  • 組件是視圖頁面的重要組成部分
  • 每一個視圖頁面都由一個或多個組件構成
  • 組件是 React 應用程序的基石

組件分類:無狀態組件

沒有狀態的組件,只做純靜態展示

  • 無狀態組件是最基本的組件存在形式
  • 構成:由 props屬性 和 render渲染函數 構成
  • 好處:由于不涉及狀態的更新,復用性最強

組件分類:有狀態組件

在無狀態的組件的基礎上增加了組件內部狀態管理

  • 生命周期:有狀態組件有生命周期,在不同的時刻觸發組件狀態的更新
  • 用處:有狀態組件被大量使用在業務邏輯開發中

組件的創建

  • ES5 的 React.createClass 方式,逐漸被下面的 ES6 代替
  • ES6 的 React.Component 方式
import React, { Component } from 'react';

class TextView extends Component {
    constructor(props) {
        super(props);
        this.state = {};
    }

    render() {
        return (
            <div>Text</div>
        );
    }
}
  • 無狀態的函數組件方式(箭頭函數格式)
const Todo = (props) => {
    <li 
        onClick={props.onClick}
        style={{textDecoration: props.complete ? "line-through" : "none"}}>
        {props.text}
    </li>
}
const Todo = ({ onClick, complete, text, props }) => {
    <li 
        onClick={props.onClick}
        style={{textDecoration: props.complete ? "line-through" : "none"}}>
        {props.text}
    </li>
}

【注意】

  • 無狀態組件一般會搭配高階組件(OHC)一起使用
  • 高階組件主要用來托管 state
  • Redux 框架就是通過 store 來管理數據源和組件的所有狀態,其中所有負責展示的組件都使用無狀態函數式寫法
  • 無狀態組件被大規模應用在大型應用程序中
  • 缺點:無狀態組件在被 React 調用之前,組件還沒有實例化,所以它不支持 ref 特性

2.2 props

  • props 是組件對外的接口,一般情況下,props是不變的
  • state 是組件對內的接口

props 的使用方式

{this.props.key}

props 是父子組件交互的唯一方式:super(props);

const { Component } = require("react");

class HelloMessage extends Component {
    constructor(props) {
        super(props);
        this.state = {
            name = 'Jack'
        };
    }

    render() {
        return (
            <h1>Hello {this.props.name}</h1>
        )
    }
}

export default HelloMessage;
  • 通過 {this.props.name} 方式獲取 props 中的值
  • ES5的語法中,通過 getDefaultProps() 方法中設置默認值

在子類中定義props

const { Component } = require("react");

export default class Child extends Component {
    constructor(props) {
        super(props);
        this.state = {
            counter:props.age||0
        };
    }

    render() {
        return (
            <h1>Hello {this.props.name}</h1>
        )
    }
}

Child.PropTypes = {
    name: PropTypes.string.isRequired,
    age: PropTypes.number
}

Child.defaultProps = {
    age: 0
}

在父類中使用 props

export default class Father extends Comment {
    render() {
        return (
            <div>
                <Child name="Jack" age={20} />
                <Child name="Tom" age={30} />
            </div>
        )
    }
}
  • 如果父類需要向子組件傳遞數據,只需要在組件中引入子組件,然后使用組件提供的props屬性,即可向子組件傳遞數據
  • 子組件 props 接受的數據格式由 PropTypes 進行檢測

2.3 state

  • props 是組件對外的接口,一般情況下,props是不變的,props對使用它的組件來說是只讀的,如果要修改props,只能通過組件的父類組件修改
  • state 是組件對內的接口,是組件的私有屬性,只能被本組件訪問和修改

組件“狀態機”

通過與用戶交互實現不同狀態,進而渲染界面,讓用戶界面與數據保持一致

在 React 中,如果需要使用 state,就需要在組件的 constructor 中初始化相關的 state

constructor(props) {
    super(props);
    this.state() = {
        key:value,
        ...
    };
}

setState():更新組件的state

this.setState({
    key:value,
});

setState() 異步操作

  1. 更新的狀態不會立馬刷新,而是將修改的狀態放入一個隊列中;
  2. React 可能會對多次 setState 狀態修改進行合并修正
  3. {this.state} 獲取的狀態可能會不準確;
  4. 也不能依賴 props 來計算組件的下一個狀態;

setState 淺合并過程

在調用 setState 修改組件狀態時,只需要傳入需要改變的狀態變量即可,不需要傳入組件完整的 state

title 和 content 屬性

this.state = {
    title: 'Jack',
    content: 'Welcome to React',
}

當只需要修改 title 屬性時,只在 setState() 中修改 title 即可

this.setState({
    title: 'Tom',
});
  • 修改了title
  • content 保持原有狀態

淺合并之后的結果是

{
    title: 'Tom',
    content: 'Welcome to React',
}

2.4 ref

  • 本質就是調用 ReactDOM.render()返回的組件實例,用來表示對組件真正實例的引用。
  • 具體使用時,可以將它綁定到組件的 render() 上,然后就可以用它輸出組件的實例。
  • 可以使用 ref 方式來修改子組件。
  • ref 不僅可以掛在到組件上,還可以作用于具體的 DOM 元素。掛載到 DOM 元素時,ref 可以表示具體的 DOM 元素節點。
  • ref 表示對組件實例的引用時,不能再函數式組件內上使用 ref 屬性。
  • ref 調用方式:設置回調函數字符串的方式,官方推薦回調函數。

ref 調用方式:回調函數

class Demo extends Component {
    constructor(props) {
        super(props);
        this.state = {
            isInputShow: false  // 控制 input 是否渲染
        };
    }

    inputRefcb(instance) {
        if (instance) {
            instance.focus();
        }
    }

    render() {
        {
            this.state.isInputShow ?
            <div>
                <input ref={this.inputRefcb} type="text"/>
            </div>
            : null
        }
    }
}

觸發回調函數的時機

  • 組件被渲染后,回調參數 instance 作為 input 的組件實例的引用,可以立即使用該組件
  • 組件被卸載后,回調參數 instance 此時為 null,這樣可以確保內存不被泄露
  • ref 屬性本身發生改變,原有的 ref 會再次被調用,此時的回調參數instance 變成具體的組件實例

ref 調用方式:字符串

class Demo extends Component {
    constructor(props) {
        super(props);
    }

    onfocus() {
        this.refs.inputRef.focus()
    }
    
    render() {
        {
            this.state.isInputShow ?
            <div>
                <input ref="inputRef" type="text"/>
                <input type="button" value="Focus" onClick={this.onfocus}/>
            </div>
            : null
        }
    }
}
  • 通過 this.refs.inputRef 來獲取組件實例
  • 不能在函數式聲明組件中使用 ref,因為他們不能獲取組件的實例

父組件訪問子組件的 DOM 節點

function TextInput(props) {
    return (
        <div>
            <input ref={props.inputRef} />
        </div>
    )
}

class Father extends Component {
    render() {
        return (
            <TextInput inputRef={
                e => this.inputElement = e
            } />
        );
    }
}

訪問過程:

  1. 在父組件 Father 中引用子組件 TextInput
  2. 子組件 TextInput 通過 ref 傳入 inputRef 函數
  3. 子組件 TextInput 又將這個回調函數作為 input 元素的 ref 屬性
  4. 父組件 Father 可以通過 {this.inputElement} 獲取子組件的 input 對應的 DOM 元素。

(三)React 高階組件

3.1 定義與實現

  • 高階組件是接受 React組件 作為參數,并返回一個新的 React組件 的組件。
  • 高階組件可以看做是對傳入的 React組件 經過一系列處理,最后返回一個相對增強的 React組件
  • 高階組件本質上一個函數,不是組件,可以參考鏈式編程

編寫一個高階組件

  • 接受一個 WrappedComponent 組件
  • 返回一個 HOC 的 withHeader 組件
import React, {Component} from 'react';

export default function withHeader(WrappedComponent) {

    return class HOC extends Component {
        render() {
            return <div>
                <div className=" header">我是標題</div>
            </div>
        }
    }
}

高階組件也可以作為一個普通組件使用

@withHeader
export default class Demo extends Component {
    render() {
        return (
            <div>我是一個普通組件</div>
        );
    }
}

@withHeader 是 ES7 中的裝飾器語法,相當于下面的表達式

const EnhanceDemo = withHeader(Demo);

如果在某個組件中多次重復使用同一個高階組件,在調試時就會看到一大堆相同的高階組件,可以在使用時保留高階組的原有名稱來區分。

3.2 分類

高階組件的實現方式:屬性代理反向繼承

  • 屬性代理:通過返回包括原組件并添加額外功能來實現高階組件,最常見
const Container = (WrappedComponent) => class extends Components {
    render() {
        const newProps = {
            text: 'newText',
        }
        return <WrappedComponent> {...this.props} {...newProps} />
    }
}
  • 反向繼承:通過返回繼承原組件來控制 render 函數,進而實現高階組件。相對于屬性代理,反向繼承能訪問到的區域和權限更多
const Container = (WrappedComponent) => class extends WrappedComponent {
    render() {
        return super.render();
    }

通過繼承 WrappedComponent,可以使用 WrappedComponent 組件的 state、props、生命周期、render()等

3.3 命名與參數

displayName屬性:當高階組件包裹普通組件時,普通組件的名稱和靜態方法都會丟失,為了避免這種情況,給普通組件添加標組組件名稱的 displayName屬性

class HOC extends ...{
    static displayName = `HOC(${getDisplayName(WrappedComponent)})`;
}

// getDisplayName 方法
function getDisplayName(WrappedComponent) {
    return WrappedComponent.displayName ||
            WrappedComponent.name ||
            'Component';
}

柯里化:通過傳入不同的參數來得到不同的高階組件

function HOCFactoryFactory(...params) {
    // 通過改變 params 來顯示不同結果
    return class HOCFactory(WrappedComponent) {
        render() {
            return <WrappedComponent {...this.props} />
        }
    }
}

高階組件的缺點

可能會造成靜態方法丟失和 ref 屬性不能傳遞,所以在使用過程中需要遵循一下準則

  • 不要在組件的 render() 中使用高階組件,也盡量避免在組件的其他生命周期方法中使用高階組件
  • 如果需要使用被包裝組件的靜態方法,那么必須手動賦值靜態方法
  • ref 應避免傳遞給包裝組件

(四)組件通信

4.1 父子組件通信

父組件 --> 子組件

父組件通過 props 將值傳遞給子組件

  • 父組件傳值:params={this.state.params}
class Parent extends Component {
    state = {
        params: 'father send msg to child'
    };
    render() {
        return <Child params={this.state.params} />;
    }
}
  • 子組件接收值:{this.props.params}
class Child extends Component { 
    render() {
        return <p>{this.props.params}</p>
    }
}

子組件 --> 父組件

  • 回調函數【最常見】
  • 自定義事件

回調函數方式:

  1. 父組件將一個函數作為 props 傳遞給子組件;
  2. 組組件調用該回調函數便可以向父組件傳值;
class Parent extends Component {
    constructor (props) {
        super(props);
        this.state = {
            params: 'child send msg to father'
        };
    }
    
    transMsg(types) {
        console.log(type);
    }
    
    render() {
        return <Child params={this.state.params} />;
    }
}

class Child extends Component {
    constructor(props) {
        super(props);
        console.log("params :", this.props.params);
        this.props.transMsg("hi, fathre");
    }
    
    render() {
        return <p>{this.state.params}</p>
    }
}

4.2 跨級組件通信

父組件與子組件的子組件或者是更深層的子組件進行的通信。實現方式有兩種:

  • 使用組件 props 逐層傳遞
  • 使用 context 對象傳遞

使用組件 props 逐層傳遞 的缺點

  • 每一層都需要傳遞 props
  • 增加程序的復雜度
  • 實際開發中不建議使用

使用 context 對象傳遞

  • 父組件需要聲明支持 context,并提供一個函數來返回相應的 context 對象
  • 子組件聲明需要使用的 context 對象,并提供需要使用的 context 屬性的 PropTypes

代碼

export default class GrandSon extends Component {
    // 子組件聲明自己需要使用的 context
    static contextTypes = {
        color : PropTypes.string,
    };
    static propTypes = {
        value : PropTypes.string,
    };

    render() {
        const { value } = this.props;
        return (
            <li style={{background: this.context.color}}>
                <span>{value}</span>
            </li>
        );
    }
}

export default class Father extends Component {
    // 聲明自己要使用的 context
    static fatherContextTypes = {
        color : PropTypes.string,
    };
    static propTypes = {
        name : PropTypes.string,
    };

    // 提供一個函數, 用來返回 context 對象
    getFatherContext() {
        return {
            color : 'red',
        };
    }
    render () {
        const {list} = this.props;
        return (
            <div>
                <ul>
                    {
                        list.map((entry, index) =>
                            <GrandSon key={`list-${index}`} value={entry.text} />
                        )
                    }
                </ul>
            </div>
        );
    }
}

class GrandFather extends Component {
    render() {
        return(
            <div>
                <Father name='GrandFather'/>
            </div>
        );
    }
}

如果組件中使用了構造函數,為了不影響跨級組件通信,還需要在構造函數中傳入第二個參數 context

constructor(props, context) {
    super(props, context);
}

context 的缺點

因為context可以代表任何東西,所以它的類型是無法確定的,所以在使用的過程中也是需要謹慎對待

【總結】在父子組件通信模型中

  • 父組件 -> 子組件:使用變量
  • 子組件 -> 父組件:父組件提供回調函數,子組件調用回調函數

其實如果將回調函數也看成一個屬性,那么這兩個過程其實都是一樣的,都是子組件使用父組件提供的“屬性”(變量或回調函數)

4.3 非嵌套組件通信

沒有直接關系的兩個組件,例如兄弟組件(同一個父節點下的兩個節點)、完全不相干的兩個組件。

對于兄弟組件,也是不可以直接通信的,可以通過狀態提升來實現兄弟組件間的通信。提升狀態就是值通過父組件進行中轉,但是當層級較深時,中轉過程也會特別復雜,如何尋找公共父組件也是一個問題。

自定義事件

  • 發布/訂閱模型
  • 給事件對象上添加監聽器
  • 通過觸發事件來實現組件之間的通信
  • 在組件的 componentDidMount 中聲明自定義事件
  • 在組件的 componentWillUnmount 中取消訂閱事件

安裝 events 模塊

通過自定義事件的方式來實現非嵌套組件間的通信,需要借助Node的events模塊,通過以下命令安裝 events 模塊

npm install events --save

然后在 src 目錄下創建一個 events.js 文件

import { EventEmitter } from 'events';
export default new EventEmitter();

再創建一個 ComponentA.js 文件

  • 在組件ComponentA.jscomponentDidMount 中聲明自定義事件
  • 在組件ComponentA.jscomponentWillUnmount 中取消訂閱事件
export default class ComponentA extends Component {
    constructor(props) {
        super(props);
        this.state = {
            message : 'ComponentA',
        };
    }

    // 聲明一個自定義事件
    componentDidMount() {
        this.eventEmitter = events.addListener('changeMessage', (message) => {
            this.setState ({
                message,
            });
        });
    }

    // 取消事件訂閱
    componentWillUnmount() {
        events.removeListener(this.eventEmitter);
    }

    render() {
        return (
            <div>
                {this.state.message}
            </div>
        );
    }
}

再創建一個組件ComponentB

  • 添加一個點擊事件
  • ComponentA 接收到 ComponentB 發過來的消息后,刷新界面
// 組件 ComponentB
export default class ComponentB extends Component {
    handleClick = (message) => {
        events.emit('changeMessage', message);
    };
    render() {
        return(
            <div>
                <button onClick={this.handleClick.bind(this, 'ComponentB')}>點擊發送信息</button>
            </div>
        );
    }
}

創建測試用例,模擬兩個非嵌套組件的通信

// 測試用例
export default class AppTest extends Component {
    render() {
        return (
            <div>
                <ComponentA />
                <ComponentB />
            </div>
        );
    }
}

【總結】原生通知了解下

  • 組件 ComponentA 監聽通知
  • 組件 ComponentB 發送通知

(五)事件處理

5.1 事件監聽與處理

React 事件 和 HTML 事件

  • React 事件使用駝峰命名法,而非全部小寫
  • React中,可以傳遞一個函數作為事件的處理函數,而非一個簡單的字符串

為按鈕添加一個事件

  • 只需要給 React 元素添加 onClick、onKeyDown 函數即可
class demo extends Component {
    handleClick() {
        console.log('Click me')
    }

    render() {
        return(
            <button onClick={this.handleClick}>React 實戰</button>
        );
    }
}

事件攔截

HTML 中通過 return false 來攔截事件

<a href="#" onclick="console.log('The link was clicked"); return false">
    Click me
</a>

React 使用虛擬DOM基礎上實現的合成事件 SyntheicEvent

  • React 的時間處理程序接受的是 SyntheicEvent實例
  • stopPrepagation():阻止時間傳遞,目的是不讓事件分派到其他的 Document 節點,但是默認事件依然會執行
  • preventDefault():通知瀏覽器不要執行與事件關聯的默認動作,但事件依然會繼續傳遞。
function ActionLink() {
    function handleClick(e) {
        e.prevenDefault();
    }

    return (
        <a href="#" onClick={handleClick}>Click me</a>
    );
}

5.2 event 事件與 this 關鍵字

event 事件

  1. React 在虛擬 DOM 的基礎上實現的一套合成事件
  2. 處理監聽時,需要傳入一個 event 對象
  3. 完全符合 W3C 標準,所以可以完全兼容瀏覽器,并擁有和瀏覽器一樣的事件接口

案例一:輸出按鈕的 innerHTML

class Demo extends Component {
    handleClick(e) {
        console.log(e.target.innerHTML)
    }

    render() {
        return(
            <button onClick={this.handleClick}>React 實戰</button>
        );
    }
}

函數與對象方法

先來看一個例子,在上述方法中,如果輸出 this,this結果是 null 或者 undefined

handleClick(e) {
        console.log(this)   // `null` 或者 `undefined`
    }

原因handleClick 是一個函數,并非是通過對象的方法調用的,而是直接的函數調用,所以在這個函數中,就無法獲取到 this 所代表的類實例

解決辦法:將函數綁定到當前實例上

render() {
        return(
            <button onClick={this.handleClick.bind(this)}>React 實戰</button>
        );
    }

bind方法

  1. bind方式實現時間監聽非常常見;
  2. bind是React在ES5引入的事件監聽機制;
  3. bind格式:Function.prototype.bind()

bind原理

  1. 當調用函數對象的 bind() 方法時
  2. 系統會重新創建一個函數,新函數的行為和原函數一樣
  3. 因為他們是由指定的 this 和初始化參數構造的原函數

bind傳參

  • 格式:bind(this, arg1, arg2, ...)
function f() {
    return this.a;
}
var g = f.bind({a:"azertyp"});
console.log(g());   //azertyp
var h = g.bind({a:"yoo"});  // bind 只生效一次
console.log(h());   //azertyp

5.3 EventEmitter 在 React Native 中的應用

EventEmitter 是用來處理原生和 React Native 之間通信的

iOS原生和 JavaScript 層交互關系表

原始端函數 JavaScript 層接口
sendEventWithName RCTAppEventEmitter
sendDeviceEventWithName RCTDeviceEventEmitter
sendInputEventWithName RCTInputEventEmitter
  • iOS 中使用 RCTEventEmitter
  • Android 中使用 RCTDeviceEventEmitter

iOS 和 React Native 交互

iOS 中通過 eventDispatchersendAppEventWithName 方法將消息傳遞個 JavaScript

#import "CalendarManager.h"
#import "RCTEventDispatcher.h"

@implementation CalendarManager

@synchesize bridge=_bridge;

-(void)calendarEventReminderReceived:(NSNotification *)notification {
    NSString* eventName = notification.userInfo[@"name"];
    [self.bridge.eventDispatcher sendAppEventWithName"EventReminder" body:@{@"name" : eventName}];
}

@end

JavaScript 通過 addListener 訂閱該事件,注意保持 name 一致,iOS中發出來的name是 EventReminder,所以addListener監聽的也應該是 EventReminder

在事件使用完之后取消事件的訂閱,即在 conponentWillUnmount 聲明周期函數中取消事件的訂閱。

import { NativeAppEventEmitter, NativeEventEmitter } from 'react-native';

var subscription = NativeEventEmitter.addListener(
    'EventReminder', (reminder) => console.log(reminder.name)
);
...
// 取消訂閱事件
conponentWillUnmount() {
    subscription.remove();
}

Android 和 React Native 交互

Android中,通過 RCTDeviceEventEmitter 來注冊事件

getReactApplicationContext()
    .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
    .emit("EventReminder", null);

(六)React Hook

6.1 Hook 簡介

  1. React Hook 是為了解決 React 的狀態共享問題;
  2. 狀態共享也可以看成是邏輯復用的問題;
  3. 因為 React Hook 治共享數據處理邏輯,并不會共享數據本身;

在 React 應用開發中,狀態管理是組價你開發必不可少的內容。狀態管理的方式:

  • 使用類組件
  • 使用 redux 等狀態管理框架

案例【以前的做法】

class Example extends Component {
    constructor(props) {
        super(props);
        this.state = {
            count : 0
        };
    }    

    render() {
        return (
            <div>
                <p>You clicked {this.state.count} times</p>
                <button onClick={()=>this.state({ count: this.state.count+1 })}>Click me</button>
            </div>
        );
    }
}

案例【現在的做法】

  • 使用 React Hook 提供的 State Hook 來處理狀態;
  • 針對已經存在的類組件,也可以使用 State Hook 很好的進行重構;
  • Example 變成了一個函數組件,有自己的狀態,還可以更新自己的狀態;
  • useState函數 是 React 的一個 hook 函數,它的作用是聲明狀態變量。
function Hook() {
    const [count, setCount] = useState(0);
    return (
        <div>
            <p>You clicked {this.state.count} times</p>
            <button onClick={()=>this.state({ count: this.state.count+1 })}>Click me</button>
        </div>
    );
}

6.2 Hook API

Hook API 背景故事

1. 如何解決狀態組件的復用問題?

一般都是通過自上而下傳遞的數據流來將大型的視圖拆分成獨立的可復用組件。但是在實際開發中,如何復用一個帶有業務邏輯的組件讓讓是一個問題。

2. 函數組件和類組件

前面介紹了他們的一些特性:函數組件缺少組件的 狀態生命周期等特征,所以一直不受青睞

但是 Hool API 賦予了函數組件這些能力

React 提供了三個核心的 API

  • State API:狀態API
  • Effect API:聲明周期API
  • Custom API:自定義API

useState 組件

用來定義和管理本地狀態

下面看一個計數器的小案例

  • 函數組件的對象也可以是基礎類型值
  • useState 返回的是一個數組,數組的第一個對象表示當前狀態的值,第二個對象表示用于更改狀態的函數,類似于類組件的 setState
function App() {
    const [count, setCount] = useState(0);
    return (
        <div>
            <button onClick={ () => setCount(count+1)}>+</button>
            <span>{count}</span>
            <button onClick={ () => setCount(count-1)}>-</button>
        </div>
    )
}

export default App;

useState 的聲明方式

  • 單次聲明多個對象
const [count, setCount] = useState({
    count1: 0,
    coutn2: 0
});
  • 多次聲明多個對象
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);

在實際使用中,多次聲明會更加方便,因為更新函數采用的是替換而不是合并。

如果要處理嵌套多層的數據邏輯,用 useState 就顯得力不從心了,需要使用 React 提供的 useReducer 來處理這類問題

useReducer 的用法

import React, {useReducer} from 'react';

console reducer = function(state, action) {
    switch (action.type) {
        case "increment":   // 增加
            return {count: state.count+1};
        case "decrement":   // 減少
            return {count: state.count-1};
        default
            return {count: state.count};    
    }
};

function Example() {
    const [state, dispatch] = useReducer(reducer, {count:0});
    const {count} = state;
    return (
        <div>
            <button onClick={ () => dispatch({type: "increment"})}>+</button>
            <span>{count}</span>
            <button onClick={ () => dispatch({type: "decrement"})}>-</button>
        </div>
    );
}

export default Example;
  • useReducer 接受 reducer 函數和默認值兩個參數
  • 返回當前狀態 state 和 dispatch 函數的數組
  • 使用方式與 Redux 框架一致
  • useReducer 沒有采用 Redux 方式設置默認值,是因為React認為狀態的默認值可能來自于函數組件的props
function Example({initialState = 0}) {
    const {state, dispatch} = useReducer(reducer, { count: initialState} );
    ...
}

Effct Hook 管理聲明周期

import React, {useState, useEffect} from 'react';


function Example() {
    const [count, setCount]= useState(0);

    useEffect( () => {
        console.log('componentDidMount...');
        console.log('componentDidUpdate...');
        return() => {
            console.log('componentWillUnmount...');
        }
    }
        
    );

    return (
        <div>
            <button onClick={ () => setCount(count+1)}>
                Click me
            </button>
        </div>
    );
}

export default Example;

每次點擊按鈕的時候,輸出的內容為

componentDidMount...
componentDidUpdate...
componentWillUnmount...
componentDidMount...
componentDidUpdate...
...
  • 每次執行組件更新時,useEffect 中的回到函數都會被調用;
  • 在重新繪制前執行銷毀操作,避免造成內存泄漏;
  • useEffect 可以被視為 componentDidMount, componentDidUpdate, componentWillUnmount 的數組,并用它關聯函數組件的生命周期
  • 類組件的 componentDidMount, componentDidUpdate 聲明周期函數都是在 DOM 更新后同步執行的
  • useEffect 不是同步執行的,不會阻塞瀏覽器更新界面
  • 需要模擬生命周期的同步執行,可以使用 React 提供的 useLayoutEffect Hook

6.3 自定義 Hook

  1. 自定義 Hook:函數名是以 use 開頭的并調用其他 Hook 的封裝函數
  2. 自定義 Hook 的每個狀態都是獨立的

使用 axios 實現一個自定義 Hook 的案例

import React, {useState, useEffect} from 'react';
import axios from 'axios';

export const useAxios = (url, dependecies) => {
    const [isLoading, setIsLoading] = useState(false);
    const [response, setReponse] = useState(null);
    const [error, setError] = useState(null);

    useEffect(() => {
        setIsLoading(true);
        axios.get(Url).then((res) => {
            setIsLoading(false);
            setReponse(res);
        }).catch((err) => {
            setIsLoading(false);
            setError(err);
        });
    }, dependecies);

    return [isLoading, response, error];
};

在 Example 中使用 axois 自定義 Hook 函數組件

import React, {useState, useEffect} from 'react';
import axios from 'axios';

export const useAxios = (url, dependecies) => {
    const [isLoading, setIsLoading] = useState(false);
    const [response, setReponse] = useState(null);
    const [error, setError] = useState(null);

    useEffect(() => {
        setIsLoading(true);
        axios.get(Url).then((res) => {
            setIsLoading(false);
            setReponse(res);
        }).catch((err) => {
            setIsLoading(false);
            setError(err);
        });
    }, dependecies);

    return [isLoading, response, error];
};

function Example() {
    let url = 'http://api.douban.com/v2/movie/in_theaters';
    const [isLoading, response, error] = useAxios(url, []);

    return (
        <div>
            {isLoading ? <div>Loading...</div> :
            (error ? <div> There is an error happned</div> : <div>Success {response}</div>
        }
        </div>
    );
}
 export default Example;

自定義 Hook 的優勢

  1. 簡潔易讀
  2. 不會引起組件嵌套的問題

自定義 Hook 使用的注意事項

  1. 不要在循環、條件或嵌套函數中使用 Hook,并且只能在 React 函數的頂層使用 Hook,這是因為 React需要利用調用順序來正確更新相應的狀態,以及調用相應的生命周期函數。一旦在循環或條件分支語句中調用 Hook,就容易引起調用順序不一致,產生難以預料的后果
  2. 只能在 React 函數式組件或自定義Hook中使用 Hook。

eslint

避免在開發中引起低級錯誤,可以在項目中安裝一個 eslint 插件

yarn add eslint-plugin-react-hooks --dev

然后在eslint的配置文件中添加如下配置:

{
    "plugins" : [
        // ...
        "react-hooks"
    ],
    "rules" : [
        // ...
        "react-hooks/rules-of-hooks" : "error",
        "react-hooks/exhaustive-deps" : "warn",
    ]
}

借助 React 提供的 Hook API,函數組件可以實現絕大部分類組件功能,并且 Hook 在共享狀態邏輯、提高組件復用性上也有一定的優勢。可以預見的是,Hook將是 React 未來發展的重要方向。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。