【教程】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 中渲染和处理表单元素。

相关文章
相关标签/搜索