【教程】Pastate.js 響應式框架(三)數組渲染與操做

這是 Pastate.js 響應式 react state 管理框架系列教程的第三章,歡迎關注,持續更新。javascript

這一章咱們來看看在 pastate 中如何渲染和處理 state 中的數組。vue

渲染數組

首先咱們更新一下 state 的結構:java

const initState = {
    basicInfo: ...,
    address: ...,
    pets: [{
        id:'id01',
        name: 'Kitty',
        age: 2
    }]
}

咱們定義了一個有對象元素構成的數組 initState.pets, 且該數組有一個初始元素。 react

接着,咱們定義相關組件來顯示 pets 的值:segmentfault

class PetsView extends PureComponent {
    render() {
        /** @type {initState['pets']} */
        let state = this.props.state;
        return (
            <div style={{ padding: 10, margin: 10 }}>
                <div><strong>My pets:</strong></div>
                {state.map(pet => <PetView state={pet} key={pet.id}/>)}
            </div>
        )
    }
}
class PetView extends PureComponent {
    render() {
        /** @type {initState['pets'][0]} */
        let state = this.props.state;
        return (
            <div>
                <li> {state.name}: {state.age} years old.</li>
            </div>
        )
    }
}

這裏定義了兩個組件,第一個是 PetsView,用來顯示 pets 數組; 第二個是 PetView,用來顯示 pet 元素。
接下來把 PetsView 組件放入 AppView 組件中顯示:數組

...
class AppView extends PureComponent {
    render() {
        /** @type {initState} */
        let state = this.props.state;
        return (
            <div style={{ padding: 10, margin: 10, display: "inline-block" }}>
                <BasicInfoView state={state.basicInfo} />
                <AddressView state={state.address} />
                <PetsView state={state.pets} />
            </div>
        )
    }
}
...

完成!咱們成功渲染了一個數組對象,這與用原生 react 渲染數組的模式同樣,頁面結果以下: 閉包

成功地把數組渲染出來

修改數組

首先,咱們想添加或減小數組元素,這用 pasate 實現起來很是簡單。受 vue.js 啓發,pastate 對 store.state 的數組節點的如下7個 數組變異方法 都進行了增強,你能夠直接調用這些數組函數,pastate 會自動觸發視圖的更新。這 7 個數組變異方法以下框架

  • push()
  • pop()
  • shift()
  • unshift()
  • splice()
  • sort()
  • reverse()

咱們來嘗試使用 push 和 pop 來更新數組:編輯器

class PetsView extends PureComponent {

    pushPet(){
        state.pets.push({
            id: Date.now() + '',
            name: 'Puppy',
            age: 1
        })
    }

    popPet(){
        state.pets.pop()
    }

    render() {
        /** @type {initState['pets']} */
        let state = this.props.state;
        return (
            <div style={{ padding: 10, margin: 10 }}>
                <div><strong>My pets:</strong></div>
                {state.map(pet => <PetView state={pet} key={pet.id}/>)}
                <div>
                    <button onClick={this.pushPet}>push pet</button>
                    <button onClick={this.popPet}>pop pet</button>
                </div>
            </div>
        )
    }
}

很是容易!咱們還添加了兩個按鈕並指定了點擊處理函數,運行體驗一下: 函數

新增 push 和 pop 按鈕

打開 react dev tools 的 Highlight Updates 選項,並點擊 push 或 pop 按鈕,能夠觀察到視圖更新狀況如咱們所願:

視圖更新狀況

空初始數組與編輯器 intelliSence

一般狀況下,數組節點的初始值是空的。爲了實現編輯器 intelliSence, 咱們能夠在外面定義一個元素類型,並註釋這個數組節點的元素爲該類型:

const initState = {
    ...
    /** @type {[pet]} */
    pets: []
}
const pet = {
    id: 'id01',
    name: 'Kitty',
    age: 2
}

你也可使用泛型的格式來定義數組類型: /** @type {Array<pet>} */

多實例組件的內部動做處理

上一章咱們提到了單實例組件,是指組件只被使用一次;而咱們能夠到 PetView 被用於顯示數組元素,會被屢次使用。咱們把這類在多處被使用的組件稱爲多實例組件。多實例組件內部動做的處理邏輯由組件實例的具體位置而定,與單實例組件的處理模式有差異,咱們來看看。

咱們試着製做一個每一個寵物視圖中添加兩個按鈕來調整寵物的年齡,咱們用兩種傳統方案和pastate方案分別實現:

react 傳統方案

傳統方案1:父組件處理

父組件向子組件傳遞綁定index的處理函數:這種模式是把子組件的動做處理邏輯實如今父組件中,而後父組件把動做綁定對應的 index 後傳遞給子組件

class PetsView extends PureComponent {
    ...
    addAge(index){
        state.pets[index].age += 1
    }
    reduceAge(index){
        state.pets[index].age -= 1
    }
    render() {
        /** @type {initState['pets']} */
        let state = this.props.state;
        return (
            <div style={{ padding: 10, margin: 10 }}>
                ...
                {
                    state.map((pet, index) => 
                        <PetView 
                            state={pet} 
                            key={pet.id} 
                            addAge={() => this.addAge(index)} // 綁定 index 值,傳遞給子組件
                            reduceAge={() => this.reduceAge(index)} //  綁定 index 值,傳遞給子組件
                        />)
                }
                ...
            </div>
        )
    }
}
class PetView extends PureComponent {
    render() {
        /** @type {initState['pets'][0]} */
        let state = this.props.state;
        return (
            <div >
                <li> {state.name}: 
                    <button onClick={this.props.reduceAge}> - </button> {/* 使用已綁定 index 值得動做處理函數 */}
                    {state.age} 
                    <button onClick={this.props.addAge}> + </button> {/* 使用已綁定 index 值得動做處理函數 */}
                    years old.
                </li>
            </div>
        )
    }
}

這種模式能夠把動做的處理統一在一個組件層級,若是多實例組件的視圖含義不明確、具備通用性,如本身封裝的 Button 組件等,使用這種動做處理模式是最好的。可是若是多實例組件的含義明顯、不具備通用性,特別是用於顯示數組元素的狀況下,使用這種模式會引起多餘的渲染過程。

打開 react dev tools 的 Highlight Updates 選項,點擊幾回 push pet 增長一些元素後,再點擊 +- 按鈕看看組件從新渲染的狀況:

組件從新渲染狀況

能夠發現當咱們只修改某一個數組元素內部的值(pet[x].age)時,其餘數組元素也會被從新渲染。這是由於 Pet.props.addAge 和 Pet.props.reduceAge 是每次父組件 PetsView 渲染時都會從新生成的匿名對象,PureComponent 以此認爲組件依賴的數據更新了,因此觸發從新渲染。雖然使用 React.Component 配合 自定義的 shouldComponentUpdate 生命週期函數能夠手動解決這個問題,可是每次渲染父組件 PetsView 時都從新生成一次匿名子組件屬性值,也在消耗運算資源。

傳統方案2:子組件結合 index 實現

父組件向子組件傳遞 index 值:這種模式是父組件向子組件傳遞 index 值,並在子組件內部實現自身的事件處理邏輯,以下:

class PetsView extends PureComponent {
    ...
    render() {
        ...
        return (
            <div style={{ padding: 10, margin: 10 }}>
                ...
                {
                    state.map((pet, index) => 
                        <PetView 
                            state={pet} 
                            key={pet.id} 
                            index={index} // 直接把 index 值傳遞給子組件
                        />)
                }
                ...
            </div>
        )
    }
}
class PetView extends PureComponent {

    // 在子組件實現動做邏輯

    // 調用時傳遞 index
    addAge(index){
        state.pets[index].age += 1
    }

    // 或函數自行從 props 獲取 index
    reduceAge = () => { // 函數內部使用到 this 對象,使用 xxx = () => {...} 來定義組件屬性更方便
        state.pets[this.props.index].age -= 1
    }

    render() {
        /** @type {initState['pets'][0]} */
        let state = this.props.state;
        let index = this.props.index;
        return (
            <div >
                <li> {state.name}: 
                    <button onClick={() => this.reduceAge(index)}> - </button> {/* 使用閉包傳遞 index 值 */}
                    {state.age} 
                    <button onClick={this.addAge}> + </button> {/* 或讓函數實現本身去獲取index值 */}
                    years old.
                </li>
            </div>
        )
    }
}

這種模式可使子組件獲取 index 並處理自身的動做邏輯,並且子組件也能夠把自身所在的序號顯示出來,具備較強的靈活性。咱們再來看看其當元素內部 state 改變時,組件的從新渲染狀況:

組件從新渲染狀況

咱們發現,數組元素組件能夠很好地按需渲染,在渲染數組元素的狀況下這種方法具備較高的運行效率。

可是,因爲元素組件內部操做函數綁定了惟一位置的 state 操做邏輯,如addAge(index){ state.pets[index].age += 1}。假設咱們還有 state.children 數組,數組元素的格式與 state.pets 同樣, 咱們要用相同的元素組件來同時顯示和操做這兩個數組時,這種數組渲染模式就不適用了。咱們能夠用第1種方案實現這種狀況的需求,但第1種方案在渲染效率上不是很完美。

pastate 數組元素操做方案

Pastate 的 imState 的每一個節點自己帶有節點位置的信息和 store 歸宿信息,咱們能夠利用這一點來操做數組元素!

pastate 方案1:獲取對於的響應式節點

咱們使用 getResponsiveState 函數獲取 imState 對於的響應式 state,以下:

class PetsView extends PureComponent {
    ...
    render() {
        ...
        return (
            <div style={{ padding: 10, margin: 10 }}>
                ...
                {
                    state.map((pet, index) => 
                        <PetView 
                            state={pet} 
                            key={pet.id}   {/* 注意,這裏無需傳遞 index 值,除非要在子組件中有其餘用途*/}
                        />)
                }
                ...
            </div>
        )
    }
}
import {..., getResponsiveState } from 'pastate'

class PetView extends PureComponent {
    addAge = () => {
        /** @type {initState['pets'][0]} */
        let pet = getResponsiveState(this.props.state); // 使用 getResponsiveState 獲取響應式 state 節點
        pet.age += 1
    }
    reduceAge = () => {
        /** @type {initState['pets'][0]} */
        let pet = getResponsiveState(this.props.state); // 使用 getResponsiveState 獲取響應式 state 節點
        pet.age -= 1
    }
    render() {
        /** @type {initState['pets'][0]} */
        let state = this.props.state;
        return (
            <div >
                <li> {state.name}: 
                    <button onClick={this.reduceAge}> - </button>
                    {state.age} 
                    <button onClick={this.addAge}> + </button>
                    years old.
                </li>
            </div>
        )
    }
}

咱們能夠看到,子組件經過 getResponsiveState 獲取到當前的 props.state 對應的響應式 state,從而能夠直接對 state 進行復制修改,你無需知道 props.state 究竟在 store.state 的什麼節點上! 這種模式使得複用組件能夠在多個不一樣掛載位置的數組中使用,並且能夠保證很好的渲染性能:

從新渲染狀況

pastate 方案2:使用 imState 操做函數

Pastate 提供個三個直接操做 imState 的函數,分別爲 set, merge, update。咱們來演示用這些操做函數來代替 getResponsiveState 實現上面操做寵物年齡的功能:

import {..., set, merge, update } from 'pastate'

class PetView extends PureComponent {
    addAge = () => {
        set(this.props.state.age, this.props.state.age + 1); 
    }
    reduceAge = () => {
        merge(this.props.state, {
            age: this.props.state.age - 1
        });
    }
    reduceAge_1 = () => {
        update(this.props.state.age, a => a - 1);
    }
    ...
}

可見,這種 imState 操做函數的模式也很是簡單!

使用 pastate 數組元素操做方案的注意事項:當操做的 state 節點的值爲 null 或 undefined 時, 只能使用 merge 函數把新值 merge 到父節點中,不可使用 getResponsiveStatesetupdate。咱們在設計 state 結構時,應儘可能避免使用絕對空值,咱們徹底能夠用 '', [] 等代替絕對空值。

下一章,咱們來看看如何在 pastate 中渲染和處理表單元素。

相關文章
相關標籤/搜索