七天接手react項目 系列 —— 生命周期&受控和非受控組件&Dom 元素&Diffing 算法


其他章節請看:

七天接手react項目 系列

生命周期&受控和非受控組件&Dom 元素&Diffing 算法

生命周期

首先回憶一下 vue 中的生命周期:

vue 對外提供了生命周期的鈎子函數,允許我們在 vue 的各個階段插入一些我們的邏輯,比如:createdmountedbeforeDestroy等。

vue-生命周期圖

react 中的生命周期是否也類似?請接着看:

每個組件都包含 “生命周期方法”,你可以重寫這些方法,以便於在運行過程中特定的階段執行這些方法 —— react 官網-組件的生命周期

請看一張 react 的生命周期圖譜

react-生命周期圖譜

從這張圖我們知道:

  • 既然沒有勾選”展示不常用的生命周期“,這里顯示的 5 個方法就是常用的生命周期方法
  • 組件的生命周期可以分三個階段:掛載、更新、卸載
  • 掛載時的順序是:constructor()render()componentDidMount()

Tip

  • componentDidMount() 會在組件掛載后(插入 DOM 樹中)立即調用。常做定時器、網絡請求
  • componentDidUpdate() 會在更新后會被立即調用。首次渲染不會執行此方法
  • componentWillUnmount() 會在組件卸載及銷毀之前直接調用。在此方法中執行必要的清理操作,例如,清除 timer,取消網絡請求或清除在 componentDidMount() 中創建的訂閱等

掛載和卸載

以 Clock 組件為例:

當 Clock 組件第一次被渲染到 DOM 中的時候,就為其設置一個計時器。這在 React 中被稱為“掛載(mount)”。

同時,當 DOM 中 Clock 組件被刪除的時候,應該清除計時器。這在 React 中被稱為“卸載(unmount)”。

請看實現:

class Clock extends React.Component {
    state = { date: new Date() }
    componentDidMount() {
        this.timerID = setInterval(
            () => this.tick(),
            1000
        )
    }
    // 組件卸載前會被調用
    componentWillUnmount() {
        clearInterval(this.timerID) // {1}
    }
    tick() {
        this.setState({
            date: new Date()
        });
    }
    handleUnmount = () => {
        // 從 DOM 中卸載組件
        ReactDOM.unmountComponentAtNode(document.getElementById('root'))
    }
    render() {
        return (
            <div>
                <h1>Hello, world!</h1>
                <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
                <button onClick={this.handleUnmount}>卸載</button>
            </div>
        );
    }
}

頁面顯示:

Hello, world!
It is 11:34:16.

卸載

時間每秒都會更新,點擊按鈕”卸載“,頁面將不再有任何信息,對應的 html 為 <div id="root"></div>

TipunmountComponentAtNode() 從 DOM 中卸載組件,會將其事件處理器(event handlers)和 state 一並清除。

:倘若將 clearInterval(this.timerID)(行{1})注釋,點擊”卸載“將報錯如下:

Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in the componentWillUnmount method.

警告:無法對卸載的組件執行 React 狀態更新。 這是一個空操作,但它表明您的應用程序中存在內存泄漏。 要修復,請取消 componentWillUnmount 方法中的所有訂閱和異步任務。
不要將定時器放入 render()

倘若將上面例子中的定時器放在 render() 中。就像這樣:

render() {
    console.log(1)
    // 定時器
    this.timerID = setInterval(
        () => this.tick(),
        1000
    )
    return (
        // ...不變
    );
}

之前 render() 每秒執行一次,現在很快就會執行過萬,因為每次執行都會生成一個定時器。

過時的生命周期方法

以下生命周期方法標記為“過時”。這些方法仍然有效,但不建議在新代碼中使用它們 —— 官網-過時的生命周期方法

  • componentWillMount,現在改名為 UNSAFE_componentWillMount(),在掛載之前被調用

  • componentWillReceiveProps,現在改名為 UNSAFE_componentWillReceiveProps(),在已掛載的組件接收新的 props 之前被調用。第一次傳的不算,以后傳的才算,有人說應該叫 componentWillReceiveNewProps

  • componentWillUpdate,現在改名為 UNSAFE_componentWillUpdate(),當組件收到新的 props 或 state 時,會在渲染之前調用。

倘若用了重命名之前的方法,控制台會有詳細的警告信息。請看示例:

class Clock extends React.Component {
    componentWillMount() {

    }
    UNSAFE_componentWillReceiveProps() {

    }
}

控制台輸出:

Warning: componentWillMount has been renamed, and is not recommended for use. See https://reactjs.org/link/unsafe-component-lifecycles for details.

* Move code with side effects to componentDidMount, and set initial state in the constructor.
* Rename componentWillMount to UNSAFE_componentWillMount to suppress this warning in non-strict mode. In React 18.x, only the UNSAFE_ name will work. To rename all deprecated lifecycles to their new names, you can run `npx react-codemod rename-unsafe-lifecycles` in your project source folder.

Please update the following components: Clock

Tip:既然這幾個方法不建議使用,所以不打算深入研究

UNSAFE_ 不是指安全性

這里的 “unsafe” 不是指安全性,而是表示使用這些生命周期的代碼在 React 的未來版本中更有可能出現 bug,尤其是在啟用異步渲染之后 —— 官網-異步渲染之更新

shouldComponentUpdate

shouldComponentUpdate() 默認返回 true。用法如下:

class Clock extends React.Component {
    state = { date: new Date() }
    componentDidMount() {
        this.timerID = setInterval(
            () => this.tick(),
            1000
        )
    }
    tick() {
        this.setState({
            date: new Date()
        });
    }
    render() {
        return (
            <div>
                <h1>Hello, world!</h1>
                <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
            </div>
        );
    }
    // 返回 false
    shouldComponentUpdate() {
        return false
    }
}

Clock 的時間不會再變化。render() 方法也不會再執行。請看下圖:

react-生命周期圖2

調用 setState(),如果 shouldComponentUpdate() 返回 false 則中斷,不再執行 render()

Tip:此方法僅作為性能優化的方式而存在。不要企圖依靠此方法來“阻止”渲染,因為這可能會產生 bug —— 官網-shouldComponentUpdate()

forceUpdate

根據上圖說明,調用 forceUpdate() 將致使組件調用 render() 方法,此操作會跳過該組件的 shouldComponentUpdate()

通常應該避免使用 forceUpdate()

新增生命周期方法

相對舊的生命周期,新增如下兩個方法,但都屬於不常見的情形,所以不做詳細研究。

getDerivedStateFromProps

getDerivedStateFromProps() 會在調用 render 方法之前調用,並且在初始掛載及后續更新時都會被調用。它應返回一個對象來更新 state,如果返回 null 則不更新任何內容。

此方法適用於罕見的用例,即 state 的值在任何時候都取決於 props。

getDerivedStateFromProps 的存在只有一個目的:讓組件在 props 變化時更新 state —— 官網-什么時候使用派生 state

getSnapshotBeforeUpdate

getSnapshotBeforeUpdate() 在最近一次渲染輸出(提交到 DOM 節點)之前調用。它使得組件能在發生更改之前從 DOM 中捕獲一些信息(例如,滾動位置)。

此用法並不常見,但它可能出現在 UI 處理中,如需要以特殊方式處理滾動位置的聊天線程等。

在函數組件中使用生命周期

我們可以在函數組件中使用 useEffect 來模擬常見的生命周期鈎子:componentDidMount()componentDidUpdate()componentWillUnmount()

體驗 useEffect

首先我們運行一個例子:

function MyButton() {
    const [count, setCount] = React.useState(0)

    const add = () => {
        setCount(count + 1)
    }

    const unMount = () => {
        ReactDOM.unmountComponentAtNode(document.getElementById('root'))
    }

    // React.useEffect() 將寫在此處 {1}
    
    return (
        <div>
            <button onClick={add}>{count}</button> <button onClick={unMount}>卸載</button>
        </div>
    );
}
ReactDOM.render(
    <MyButton />,
    document.getElementById('root')
)

頁面顯示兩個按鈕:

0 卸載

第一個按鈕顯示一個數字,每點擊一次就會自增 1,點擊第二個按鈕,此組件就會被卸載。

我們接下來在行{1}處添加 React.useEffect() 相關代碼。請看示例:

// 相當於 componentDidMount()、componentDidUpdate()
React.useEffect(() => {
    console.log('a')
})

頁面渲染后就會輸出 a,之后每點擊第一個按鈕都會輸出 a,點擊卸載沒有輸出。

可以給 useEffect 傳遞第二個參數,它是 effect 所依賴的值數組 —— 官網-effect 的條件執行

倘若給 useEffect 第二個參數傳遞一個空數組,表明沒有依賴值:

// 相當於 componentDidMount()
React.useEffect(() => {
    console.log('a')
}, [])

頁面渲染后就會輸出 a,但點擊第一個按鈕就不會再有輸出。

通常,組件卸載時需要清除 effect 創建的諸如訂閱或計時器 ID 等資源。要實現這一點,useEffect 函數需返回一個清除函數 —— 官網-清除 effect

倘若給 useEffect 函數返回一個函數。請看示例:

 React.useEffect(() => {
    console.log('a')
    return () => {
        console.log('b')
    }
}, [])

頁面渲染后就會輸出 a,但點擊第一個按鈕就不會再有輸出,點擊卸載輸出 b

優化函數組件 Clock 中的定時器

在函數組件中使用 state中我們寫過這么一個例子:

function Clock() {
    const [name] = React.useState('pjl')
    const [date, setDate] = React.useState(new Date())

    setInterval(() => {
        console.log('setInterval')
        setDate(new Date())
    }, 1000)

    return (
        <div>
            <h1>Hello, world! {name}</h1>
            <h2>It is {date.toLocaleTimeString()}.</h2>
        </div>
    );
}

十秒就會輸出一千多次 setInterval。定時器應該只執行一次,放在 componentDidMount 生命鈎子中比較合適。以下是優化后的增強版:

function Clock() {
    // console.log('Clock')
    const [name] = React.useState('pjl')
    const [date, setDate] = React.useState(new Date())

    React.useEffect(() => {
        console.log('useEffect')
        const timerId = setInterval(() => {
            // console.log('setInterval')
            setDate(new Date())
        }, 1000)

        return () => {
            clearInterval(timerId)
        }
    }, [name])

    const unMount = () => {
        ReactDOM.unmountComponentAtNode(document.getElementById('root'))
    }

    return (
        <div>
            <h1>Hello, world! {name}</h1>
            <h2>It is {date.toLocaleTimeString()}.</h2>
            <button onClick={unMount}>卸載</button>
        </div>
    );
}

受控組件和非受控組件

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

這里我們能接收兩個信息:

  1. 推薦使用受控組件
  2. 受控組件和非受控組件的區別在於:表單數據由誰來處理 —— 是 react 組件管理,還是 dom 來處理。

受控組件

將表單寫為受控組件:

class NameForm extends React.Component {
    state = { value: '' }
    // 值若改變,則將其更新到 state 中
    handleChange = event => {
        this.setState({ value: event.target.value });
    }

    // 提交表單
    handleSubmit = event => {
        console.log('提交的名字: ' + this.state.value);
        event.preventDefault();
    }

    render() {
        return (
            <form onSubmit={this.handleSubmit}>
                <label>
                    名字:
                    <input type="text" value={this.state.value} onChange={this.handleChange} />
                </label>
                <input type="submit" value="提交" />
            </form>
        );
    }
}
ReactDOM.render(
    <NameForm />,
    document.getElementById('root')
);

頁面顯示

名字:[     輸入框       ] 提交

在輸入框中輸入”123“,點擊”提交“按鈕,控制台將輸出 ”提交的名字: 123“。

非受控組件

重寫 NameForm 組件,改為功能相同的非受控組件:

class NameForm extends React.Component {
    input = React.createRef()
    handleSubmit = event => {
        console.log('提交的名字: ' + this.input.current.value);
        event.preventDefault();
    }

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

勿過度使用 Refs —— 官網

Tip:倘若發生事件的元素,是你要操作的元素時,可以通過 event.target 取得 dom。

高階函數和函數柯里化優化受控組件

按照受控組件中的寫法,如果我們定義多個 input,我們就得寫多個 handleXxxx 處理方法。就像這樣:

class NameForm extends React.Component {
    state = { name: '', age: '' }

    // 2 個 input 對應 2 個處理方法
    handleName = event => {
        this.setState({ name: event.target.value });
    }
    handleAge = event => {
        this.setState({ age: event.target.value });
    }

    handleSubmit = event => {
        console.log({ name: this.state.name, age: this.state.age });
        event.preventDefault();
    }

    render() {
        return (
            <form onSubmit={this.handleSubmit}>
                <label>
                    名字:
                    <input type="text" value={this.state.name} onChange={this.handleName} />
                </label>
                <label>
                    年齡:
                    <input type="text" value={this.state.age} onChange={this.handleAge} />
                </label>
                <input type="submit" value="提交" />
            </form>
        );
    }
}

如果我們有10個,豈不是要寫10個處理方法!我們可以用高階函數函數柯里化來對其優化。請看實現:

class NameForm extends React.Component {
    state = { name: '', age: '' }

    // saveFormField 既是`高階函數`,也使用了`函數柯里化`
    saveFormField = (stateName) => {
        return (event) => {
            this.setState({ [stateName]: event.target.value }) // {1}
        }
    }

    handleSubmit = event => {
        console.log({ name: this.state.name, age: this.state.age });
        event.preventDefault();
    }

    render() {
        return (
            <form onSubmit={this.handleSubmit}>
                <label>
                    名字:
                    <input type="text" value={this.state.name} onChange={this.saveFormField('name')} />
                </label>
                <label>
                    年齡:
                    <input type="text" value={this.state.age} onChange={this.saveFormField('age')} />
                </label>
                <input type="submit" value="提交" />
            </form>
        );
    }
}

Tipthis.setState({ [stateName]: event.target.value }) 使用的語法是 可計算屬性名

高階函數

高階函數是處理函數的函數,只要滿足其中一個條件即可:

  • 參數是函數
  • 返回函數

js 內置的高階函數有:Array.forEach、setInterval、Promise等。

函數柯里化

通過函數調用繼續返回函數,實現多次接收參數最后統一處理的函數編碼形式。

最二的一個示例是將:

function sum(a,b,c){
    return a + b + c
}

改成 sum(1)(2)(3) 的形式。就像這樣:

const sum = (a) => {
    return (b) => {
        return (c) => {
            return a + b + c
        }
    }
}

// 6
console.log(sum(1)(2)(3))

DOM 元素

React 實現了一套獨立於瀏覽器的 DOM 系統,兼顧了性能和跨瀏覽器的兼容性。我們借此機會完善了瀏覽器 DOM 實現的一些特殊情況 ——官網-DOM 元素

在 React 中,所有的 DOM 特性和屬性(包括事件處理)都應該是小駝峰命名的方式。例如,與 HTML 中的 tabindex 屬性對應的 React 的屬性是 tabIndex。

:例外的情況是 aria-* 以及 data-* 屬性,一律使用小寫字母命名。比如, 你依然可以用 aria-label 作為 aria-label。

React 與 HTML 之間有很多屬性存在差異,下面以 onChange 為例。

Tip:比如 react 中用 htmlFor 代替 for,其他更多介紹請看 DOM 元素

onChange

onChange 事件與預期行為一致:每當表單字段變化時,該事件都會被觸發。我們故意沒有使用瀏覽器已有的默認行為,是因為 onChange 在瀏覽器中的行為和名稱不對應,並且 React 依靠了該事件實時處理用戶輸入 —— 官網-onChange

change 事件並不是每次元素的 value 改變時都會觸發 —— mdn-change 事件

原生 html 中 change 事件是這樣的:

<body>
    名字:<input name="name" />

    <script>
        document.querySelector('input').
            addEventListener('change', e => console.log(e.target.value))
    </script>
</body>

在輸入框中輸入 123,點擊他處讓 input 失去焦點,控制台輸出 123

在上面受控組件 NameForm 中增加一行:

class NameForm extends React.Component {
    state = { value: '' }
    handleChange = event => {
      + console.log(event.target.value)
        this.setState({ value: event.target.value });
    }
}

在輸入框中輸入 123,控制台依次輸出:

1
12
123

每當表單字段變化時,該事件都會被觸發。事件名和行為相對應。

Diffing 算法

根節點

當對比兩棵樹時,React 首先比較兩棵樹的根節點 —— 官網-Diffing 算法

對比不同類型的元素

當根節點為不同類型的元素時,React 會拆卸原有的樹並且建立起新的樹

舉個例子,當一個元素從 <a> 變成 <img>,從 <Article> 變成 <Comment>,或從 <Button> 變成 <div> 都會觸發一個完整的重建流程

當卸載一棵樹時,對應的 DOM 節點也會被銷毀。組件實例將執行 componentWillUnmount() 方法。

在根節點以下的組件也會被卸載,它們的狀態會被銷毀。比如,當比對以下更變時:

<div>
  <Counter />
</div>

<span>
  <Counter />
</span>

React 會銷毀 Counter 組件並且重新裝載一個新的組件。

對比同類型的元素

當對比兩個相同類型的 React 元素時,React 會保留 DOM 節點,僅比對及更新有改變的屬性

比如:

<div className="before" title="stuff" />

<div className="after" title="stuff" />

通過對比這兩個元素,React 知道只需要修改 DOM 元素上的 className 屬性。

在處理完當前節點之后,React 繼續對子節點進行遞歸。

對比同類型的組件元素

當一個組件更新時,組件實例會保持不變,因此可以在不同的渲染時保持 state 一致。React 將更新該組件實例的 props 以保證與最新的元素保持一致,並且調用該實例的 componentDidUpdate() 方法。

下一步,調用 render() 方法,diff 算法將在之前的結果以及新的結果中進行遞歸

對子節點進行遞歸

默認情況下,當遞歸 DOM 節點的子元素時,React 會同時遍歷兩個子元素的列表

在子元素列表末尾新增元素時,更新開銷比較。比如:

<ul>
  <li>first</li>
  <li>second</li>
</ul>

<ul>
  <li>first</li>
  <li>second</li>
  <li>third</li>
</ul>

React 會先匹配兩個 <li>first</li> 對應的樹,然后匹配第二個元素 <li>second</li> 對應的樹,最后插入第三個元素的 <li>third</li> 樹。

如果只是簡單的將新增元素插入到表頭,那么更新開銷會比較。比如:

<ul>
  <li>Duke</li>
  <li>Villanova</li>
</ul>

<ul>
  <li>Connecticut</li>
  <li>Duke</li>
  <li>Villanova</li>
</ul>

React 並不會意識到應該保留 <li>Duke</li><li>Villanova</li>,而是會重建每一個子元素。這種情況會帶來性能問題。

Keys

為了解決上述問題(新增元素插入表頭開銷大),React 引入了 key 屬性。以下示例在新增 key 之后,使得樹的轉換效率得以提高:

<ul>
  <li key="2015">Duke</li>
  <li key="2016">Villanova</li>
</ul>

<ul>
  <li key="2014">Connecticut</li>
  <li key="2015">Duke</li>
  <li key="2016">Villanova</li>
</ul>

現在 React 知道只有帶着 '2014' key 的元素是新元素,帶着 '2015' 以及 '2016' key 的元素僅僅移動了。

正確使用 key

倘若用元素在數組中的下標作為 key,有時不僅會造成上面所說的性能問題,有時還會造成程序的錯誤。請看示例:

function Demo() {
    const [todos, setTodos] = React.useState(['a', 'b'])

    const unshift = () => {
        setTodos([++seed, ...todos])
    }
    return (
        <div>
            <ul>
                {
                    todos.map((item, index) => {
                        return <li key={index} data-index={index}> {item} <input type="text" /></li>
                    })
                }
            </ul>
            <button onClick={unshift}>頭部插入</button>
        </div>
    )
}

頁面顯示:

a [   /* input 輸入框 */   ]
b [   /* input 輸入框 */   ]
頭部插入

在第一個輸入框中輸入 a,在第二個輸入框中輸入 b,然后點擊按鈕“頭部插入”,界面錯亂如下:

1 [a                       ]
a [b                       ]
b [                        ]
頭部插入

倘若將 key 改成唯一值,使用相同的操作,界面就正常:

{
    todos.map((item, index) => {
        return <li key={item} data-index={index}> {item} <input type="text" /></li>
    })
}
1 [                        ]
a [a                       ]
b [b                       ]
頭部插入

在 Codepen 有兩個例子,分別為 展示使用下標作為 key 時導致的問題,以及不使用下標作為 key 的例子的版本,修復了重新排列,排序,以及在列表頭插入的問題 —— 官網-Keys

Tip:如果僅做簡單展示,用元素在數組中的下標作為 key 也是可以的。

其他章節請看:

七天接手react項目 系列


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM