七天接手react項目 —— state&事件處理&ref

state&事件處理&ref

react 起步 一文中,我們學習了 react 相關知識:jsx組件props。本篇將繼續研究 state事件處理ref

state

State 與 props 類似,但是 state 是私有的,並且完全受控於當前組件 —— 官網

react 中的 props 用來接收父組件傳來的屬性,並且是只讀的。

由此,我們能猜測 state 就是組件自身屬性

Tip:是否感覺像 vue 組件中的 data,請接著看!

var app = new Vue({
    ...
    // 狀態
    data: {
        message: 'Hello Vue!',
        seen: true
    }
})

初步體驗

這是一個官方示例:

class Clock extends React.Component {
    constructor(props) {
        super(props) // 這裡等於 super()
        this.state = { date: new Date() };
    }

    render() {
        return (
            <div>
                <h1>Hello, world!</h1>
                <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
            </div>
        );
    }
}
ReactDOM.render(
    <Clock />,
    document.getElementById('root')
);

網頁顯示:

Hello, world!
// 當前時間
It is 11:20:26.

TiptoLocaleTimeString() 方法返回該日期對象時間部分的字元串,該字元串格式因不同語言而不同

如果不初始化 state 或不進行方法綁定,則不需要為 React 組件實現構造函數 —— 官網_constructor

由此可以猜測 state 能在構造函數中初始化。那麼必須調用 super() 不?請看示例:

constructor() {
    // super()
    console.log('this', this)
    this.state = { date: new Date() };
}

瀏覽器控制台報錯如下:

Uncaught ReferenceError: this hasn't been initialised - super() hasn't been called

Tip:在構造函數中訪問 this 之前,一定要調用 super(),它負責初始化 this,如果在調用 super() 之前訪問 this 會導致程式報錯 —— 類 (class)_繼承

不要直接修改 State

不要直接修改 State ——官網-正確地使用 State

假如我們直接修改 state 會發生什麼?請看示例:

class Clock extends React.Component {
    constructor() {
        super()
        this.state = { date: new Date() }
        setInterval(() => {
            // 直接修改 state
            this.state.date = new Date()
            console.log(this.state.date)
        }, 1000)
    }

    render() {
        // ... 不變
    }
}

我們期望每過一秒,時間都能更新。但現實是,頁面內容靜止不變,但控制台輸出的時間卻在改變:

// 頁面輸出
Hello, world!
It is 16:07:00.
// 控制台輸出
Mon Mar 14 2022 16:07:01 GMT+0800 (中國標準時間)
Mon Mar 14 2022 16:07:02 GMT+0800 (中國標準時間)
Mon Mar 14 2022 16:07:03 GMT+0800 (中國標準時間)
Mon Mar 14 2022 16:07:04 GMT+0800 (中國標準時間)
...

Tip:我們可以通過 forceUpdate() 方法來強制更新,但我們通常不會這樣使用。vue 也有一個類似的方法 vm.$forceUpdate()

setInterval(() => {
    this.state.date = new Date()
    console.log(this.state.date)
    // 強制更新。通常不用
  + this.forceUpdate()
}, 1000)

setState

通過 setState() 修改 state

繼續上面的例子,讓 Clock 組件在頁面中每過一秒都會自動更新時間:

class Clock extends React.Component {
    constructor(props) {
        super(props)
        this.state = { date: new Date() }
        // bind() 方法會返回一個新的函數,裡面綁定 this,否則 tick() 報錯如下:
        // Uncaught TypeError: this.setState is not a function
        setInterval(this.tick.bind(this), 1000)
    }
    tick() {
        // 通過 setState 修改 state
        this.setState({
            date: new Date()
        })
    }
    render() {
        // ... 不變
    }
}

頁面顯示:

Hello, world!
It is 15:07:06.

// 一秒後顯示
Hello, world!
It is 15:07:07.
合併還是替換

當你調用 setState() 的時候,React 會把你提供的對象合併到當前的 state —— 官網_「State 的更新會被合併」

這個不難證明。請看示例:

class Clock extends React.Component {
    constructor() {
        super()
        this.state = { date: new Date(), name: 'pengjili' }
        setInterval(this.tick.bind(this), 1000)
    }
    tick() {
        // state 初始化時是兩個屬性,現在是一個屬性
        this.setState({
            date: new Date()
        })
    }
    render() {
        return (
            <div>
                <h1>Hello, world! {this.state.name}</h1>
                <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
            </div>
        );
    }
}

頁面顯示:

// 每秒時間都會改變,但 `pengjiali` 一直顯示
Hello, world! pengjili
It is 15:20:24.

倘若是替換,pengjiali 就為空了。

State 的更新可能是非同步的

出於性能考慮,React 可能會把多個 setState() 調用合併成一個調用

例如,調整購物車商品數:

this.setState({quantity: 2})

在同一周期內會對多個 setState 進行批處理,如果在同一周期內多次設置商品數量增加,則相當於:

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

後調用的 setState() 將覆蓋同一周期內先調用 setState 的值,因此商品數僅增加一次。

因此,如果後續狀態取決於當前狀態,建議使用函數的形式代替:

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

這個函數用上一個 state 作為第一個參數,將此次更新被應用時的 props 做為第二個參數。

render() 執行幾次

修改 Clock 組件的 render() 方法:

render() {
  + console.log(1)
    return (
        <div>
            <h1>Hello, world!</h1>
            <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
        </div>
    );
}

控制台輸出:

1
⑨ 1

組件一掛載就得渲染,輸出第一行的 1,然後每過一秒就會在第二行輸出,這裡統計到第 9 秒。

所以,render() 執行 1 + N 次。N 在這裡表示狀態更改了 9 次。

公有實例欄位優化 state 初始化

請看示例:

<script>
    class Dog {
        age = 18
        constructor(name) {
            this.name = name
        }
    }

    let d = new Dog('peng')
    console.log('d: ', d);
</script>

控制台輸出:d: Dog {age: 18, name: 'peng'}

公有實例欄位 age = 18 等價於給實例定義了一個屬性 age。於是我們可以將通過次語法來優化 state 的初始化。

優化前:

class Clock extends React.Component {
    constructor() {
        super()
        this.state = { date: new Date() }
        setInterval(this.tick.bind(this), 1000)
    }
}

優化後:

class Clock extends React.Component {
    state = { date: new Date() }
    constructor() {
        super()
        
        setInterval(this.tick.bind(this), 1000)
    }
}

TipsetInterval() 通常會移出構造函數,例如放在某生命鉤子函數中,所以整個構造函數 constructor() 都可以省略

在函數組件中使用 state

Hook 是 React 16.8 的新增特性。它可以讓你在不編寫 class 的情況下使用 state 以及其他的 React 特性 —— 官網-Hook API 索引

前面我們一直是在 class 組件中使用 state。就像這樣:

class Clock extends React.Component {
    state = { date: new Date(), name: 'pjl' }
    constructor() {
        super()
        setInterval(this.tick.bind(this), 1000)
    }
    tick() {
        this.setState({
            date: new Date()
        })
    }
    render() {
        return (
            <div>
                <h1>Hello, world! {this.state.name}</h1>
                <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
            </div>
        );
    }
}

而通過 useState Hook 能讓我們在函數組件使用 state。實現上述相同的功能:

 function Clock() {
    // 一個 state 就得調用一次 useState()
    const [name] = React.useState('pjl')
    // 解構賦值
    const [date, setDate] = React.useState(new Date())

    setInterval(() => {
        // 更新 state
        setDate(new Date())
    }, 1000)

    return (
        <div>
            <h1>Hello, world! {name}</h1>
            <h2>It is {date.toLocaleTimeString()}.</h2>
        </div>
    );
}
  • 一個 state 就得調用一次 useState(initialState)
  • React.useState 返回一個 state,以及更新 state 的函數
  • 在初始渲染期間,返回的狀態 (state) 與傳入的第一個參數 (initialState) 值相同
  • 在後續的重新渲染中,useState 返回的第一個值將始終是更新後最新的 state

Tip:請問控制台輸出幾次 a:

function Clock() {
    console.log('a')
    // ... 不變
}

答案是:1 + N 次。Clock 函數被反覆的調用,但 useState() 返回的第一個值始終是更新後最新的 state,所以能猜出 react 做了特殊處理。

事件處理

事件命名採用小駝峰式

React 事件的命名採用小駝峰式(camelCase),而不是純小寫。例如在 html 中通常都是小寫,就像這樣:

// onclick - 小寫
<button onclick="alert('Hello world!')">click</button>

下面這個組件,每點擊一次 button,控制台就會輸出一次 lj

class EventDemo1 extends React.Component {
    handleClick() {
        console.log('lj')
    }
    render() {
        return (
            <button onClick={this.handleClick}>
                click
            </button>
        );
    }
}

Tip:使用 JSX 語法時你需要傳入一個函數作為事件處理函數,而不是一個字元串 —— 官網

倘若將 onClick 改成 onclick,瀏覽器控制台將報錯如下:

Warning: Invalid event handler property `onclick`. Did you mean `onClick`?

事件中的 this

假如我們在 EventDemo1 中讀取狀態。就像這樣:

class EventDemo1 extends React.Component {
    state = { name: 'lj' }
    handleClick() {
        console.log(typeof this)
        // 讀取狀態
        console.log(this.state.name)
    }
    render() {
        return (
            <button onClick={this.handleClick}>
                click
            </button>
        );
    }
}

控制報錯:

undefined
Uncaught TypeError: Cannot read properties of undefined (reading 'state')

我們根據錯誤資訊能推測出在 handleClick() 方法中沒有 this

Tip:現在有一個事實:即我們自己的方法中沒有 this,而 render() 方法中卻有 this。猜測 react 只幫我們處理了 render() 方法中的 this。

所以,我們需要處理一下自定義方法中的 this。請看實現:

class EventDemo1 extends React.Component {
    state = { name: 'lj' }
   
    handleClick = () => {
        console.log(typeof this)
        console.log(this.state.name)
    }
    render() {
        // ...
    }
}

每次點擊 button,都會輸出:

object
lj

處理 this 的方法有兩點:

  • 將原型中的方法移到實例上來
  • 使用箭頭函數。由於箭頭函數沒有 this,而將箭頭函數中的 this 輸出來,卻正好就是實例

Tip:還可以只使用箭頭函數來綁定 this,就像這樣:

class EventDemo1 extends React.Component {
    state = { name: 'lj' }
    handleClick() {
        console.log(typeof this)
        // 讀取狀態
        console.log(this.state.name)
    }
    render() {
        return (
            // 箭頭函數
            <button onClick={() => this.handleClick()}>
                click
            </button>
        );
    }
}

使用 preventDefault 阻止默認行為

在 html 中我們阻止默認行為可以通過 return false。就像這樣:

// 每次點擊 button,控制將輸出 'You clicked submit.',而不會提交
<body>
    <form onsubmit="console.log('You clicked submit.'); return false">
        <button type="submit">Submit</button>
    </form>
</body>

但是在 react 卻不能通過返回 false 的方式阻止默認行為。而必須顯式的使用 preventDefault。就像這樣:

function Form() {
  function handleSubmit(e) {
    // 阻止默認行為
    e.preventDefault();
    console.log('You clicked submit.');
  }

  return (
    <form onSubmit={handleSubmit}>
      <button type="submit">Submit</button>
    </form>
  );
}

ref

我們首先回憶一下 vue 中的 ref:

ref 被用來給元素或子組件註冊引用資訊 —— vue 官網

引用資訊將會註冊在父組件的 $refs 對象上。請看示例:

<!-- `vm.$refs.p` will be the DOM node -->
<p ref="p">hello</p>

<!-- `vm.$refs.child` will be the child component instance -->
<child-component ref="child"></child-component>

如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子組件上,引用就指向組件實例。

那麼 react 中的 ref 是否也是這個作用?我們可以從其用法上去做判斷。

React 支援一個特殊的、可以附加到任何組件上的 ref 屬性。此屬性可以是一個由 React.createRef() 函數創建的對象、或者一個回調函數、或者一個字元串(遺留 API) —— 官網-ref

於是我們知道在 react 中 ref 屬性可以是一個對象、回調函數,亦或一個字元串。

String 類型的 Refs

下面這個例子將 ref 分別應用在 dom 元素子組件中:

class ASpan extends React.Component {
    render() {
        return <span>click</span>
    }
}

class EventDemo1 extends React.Component {
    handleClick() {
        console.log(this.refs)
        console.log(this.refs.aButton.innerHTML)
    }
    render() {
        return (
            // 箭頭函數
            <button ref="aButton" onClick={() => this.handleClick()}>
                <ASpan ref="aSpan" />
            </button>
        );
    }
}

點擊按鈕,控制台輸出:

{aSpan: ASpan, aButton: button}
<span>click</span>

Tip:用法上和 vue 中的 vm.$refs 非常相似。

:如果你目前還在使用 this.refs.textInput 這種方式訪問 refs ,我們建議用回調函數createRef API 的方式代替 —— 官網-過時 API:String 類型的 Refs

回調 Refs

React 也支援另一種設置 refs 的方式,稱為「回調 refs」。它能助你更精細地控制何時 refs 被設置和解除 —— 官網-回調 Refs

將字元串式 Refs 示例改成回調式。請看示例:

class EventDemo1 extends React.Component {
    handleClick() {
        console.log(this.refs)
        console.log(this.button.innerHTML)
    }
    setButtonRef = (element) => {
        this.button = element
    }
    render() {
        return (
            // 使用 `ref` 的回調函數將按鈕 DOM 節點的引用存儲到 React
            // 實例上(比如 this.button)
            <button ref={this.setButtonRef} onClick={() => this.handleClick()}>
                click
            </button>
        );
    }
}

點擊按鈕,控制台輸出:

{}
click

回調函數中接受 React 組件實例或 HTML DOM 元素作為參數,以使它們能在其他地方被存儲和訪問。

內聯函數

可以將 refs 回調函數直接寫在 ref 中。就像這樣:

// 與上面示例效果相同
<button ref={element => this.button = element} onClick={() => this.handleClick()}>
    click
</button>
回調次數

如果 ref 回調函數是以內聯函數的方式定義的,在更新過程中它會被執行兩次,第一次傳入參數 null,然後第二次會傳入參數 DOM 元素 —— 官網-關於回調 refs 的說明

請看示例:

class EventDemo1 extends React.Component {
    state = { date: new Date() }
    constructor() {
        super()
        setInterval(() => {
            this.setState({ date: new Date() })
        }, 3000)
    }
    render() {
        return (
            <button ref={element => { this.button = element; console.log('ref'); }}>
                click {this.state.date.toLocaleTimeString()}
            </button>
        );
    }
}

首先輸出 ref,然後每過 3 秒就會輸出 2 次 ref

Tip:大多數情況下它是無關緊要的 —— 官網

createRef API

將回調 refs 的例子改成 createRef 形式。請看示例:

class EventDemo1 extends React.Component {
    constructor() {
        super()
        this.button = React.createRef()
        // this.textInput = React.createRef()
    }
    handleClick() {
        // dom 元素或子組件可以在 ref 的 current 屬性中被訪問
        console.log(this.button.current.innerHTML)
    }
    render() {
        return (
            <button ref={this.button} onClick={() => this.handleClick()}>
                click
            </button>
        )
    }
}

每點擊一下 button,控制台將輸出一次 click

Refs 是使用 React.createRef() 創建的,並通過 ref 屬性附加到 React 元素。在構造組件時,通常將 Refs 分配給實例屬性,以便可以在整個組件中引用它們 —— 官網-創建 Refs

如果需要在增加一個 ref,則需要再次調用 React.createRef()

在函數組件中使用 ref

你不能在函數組件上使用 ref 屬性,因為他們沒有實例 —— 官網-訪問 Refs

而通過 useRef Hook 能讓我們在函數組件使用 ref。重寫 class 組件 EventDemo1:

function EventDemo1() {
    const button = React.useRef(null)

    function handleClick() {
        console.log(button.current.innerHTML)
    }
    return (
        <button ref={button} onClick={() => handleClick()}>
            click
        </button>
    )
}

每點擊一下 button,控制台將輸出一次 click

const refContainer = useRef(initialValue);

useRef 返回一個可變的 ref 對象,其 .current 屬性被初始化為傳入的參數(initialValue) —— 官網-useref

Tags: