[回到首页](../) | [上一章：2.多组件应用](./2.多组件应用.md)

# 3.数组渲染与操作  
这一章我们来看看在 pastate 中如何渲染和处理 state 中的数组。

* [渲染数组](#渲染数组)
* [修改数组](#修改数组)
* [空初始数组与编辑器 intelliSence](#空初始数组与编辑器-intellisence)
* [多实例组件的内部动作处理](#多实例组件的内部动作处理)
    * [react 传统方案](#react-传统方案)
        * [传统方案1:父组件处理](#传统方案1父组件处理)
        * [传统方案2:子组件结合 index 实现](#传统方案2子组件结合-index-实现)
    * [pastate 数组元素操作方案](#pastate-数组元素操作方案)
        * [pastate 方案1:获取对于的响应式节点](#pastate-方案1获取对于的响应式节点)
        * [pastate 方案2:使用 imState 操作函数](#pastate-方案2使用-imstate-操作函数)

## 渲染数组
首先我们更新一下 state 的结构：
```javascript
const initState = {
    basicInfo: ...,
    address: ...,
    pets: [{
        id:'id01',
        name: 'Kitty',
        age: 2
    }]
}
```
我们定义了一个有对象元素构成的数组 initState.pets, 且该数组有一个初始元素。  

接着，我们定义相关组件来显示 pets 的值：
```javascript
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>
        )
    }
}
```
```javascript
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 渲染数组的模式一样，页面结果如下：  

![成功地把数组渲染出来](http://upload-images.jianshu.io/upload_images/1234637-126215065bb6d5f5.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)  

## 修改数组
首先，我们想添加或减少数组元素，这用 pasate 实现起来非常简单。受 vue.js 启发，pastate 对 store.state 的数组节点的以下7个 **数组变异方法** 都进行了加强，你可以直接调用这些数组函数，pastate 会自动触发视图的更新。这 7 个数组变异方法如下
- `push()`
- `pop()`
- `shift()`
- `unshift()`
- `splice()`
- `sort()`
- `reverse()`

我们来尝试使用 push 和 pop 来更新数组：
```javascript
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 按钮](http://upload-images.jianshu.io/upload_images/1234637-4c28e020d0a143f2.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

打开 react dev tools 的 Highlight Updates 选项，并点击 push 或 pop 按钮，可以观察到视图更新情况如我们所愿：  

![视图更新情况](http://upload-images.jianshu.io/upload_images/1234637-af5d56278c01b32b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)  

## 空初始数组与编辑器 intelliSence
通常情况下，数组节点的初始值是空的。为了实现编辑器 intelliSence， 我们可以在外面定义一个**元素类型**，并注释这个数组节点的元素为该类型：
``` javascript 
const initState = {
    ...
    /** @type {[pet]} */
    pets: []
}
const pet = {
    id: 'id01',
    name: 'Kitty',
    age: 2
}
```
你也可以使用泛型的格式来定义数组类型：  `/** @type {Array<pet>} */` 。

## 多实例组件的内部动作处理
上一章我们提到了**单实例组件**，是指组件只被使用一次；而我们可以到  PetView 被用于显示数组元素，会被多次使用。我们把这类**在多处被使用**的组件称为**多实例组件**。多实例组件内部动作的处理逻辑由组件实例的具体位置而定，与单实例组件的处理模式有差别，我们来看看。

我们试着制作一个每个宠物视图中添加两个按钮来调整宠物的年龄，我们用两种传统方案和pastate方案分别实现：

### react 传统方案

#### 传统方案1:父组件处理
**父组件向子组件传递绑定index的处理函数**：这种模式是把子组件的动作处理逻辑实现在父组件中，然后父组件把动作绑定对应的 index 后传递给子组件
```javascript
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>
        )
    }
}
```
```javascript
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` 增加一些元素后，再点击 `+` 或 `-` 按钮看看组件重新渲染的情况：

![组件重新渲染情况](http://upload-images.jianshu.io/upload_images/1234637-42efdd35c371d06e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

可以发现当我们只修改某一个数组元素内部的值(pet[x].age)时，其他数组元素也会被重新渲染。这是因为 Pet.props.addAge 和 Pet.props.reduceAge 是每次父组件 PetsView 渲染时都会重新生成的匿名对象，PureComponent 以此认为组件依赖的数据更新了，所以触发重新渲染。虽然使用 React.Component 配合 自定义的 shouldComponentUpdate 生命周期函数可以手动解决这个问题，但是每次渲染父组件 PetsView 时都重新生成一次匿名子组件属性值，也在消耗运算资源。

#### 传统方案2:子组件结合 index 实现
**父组件向子组件传递 index 值**：这种模式是父组件向子组件传递 index 值，并在子组件内部实现自身的事件处理逻辑，如下：
```javascript
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>
        )
    }
}
```
```javascript
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 改变时，组件的重新渲染情况：  

![组件重新渲染情况](http://upload-images.jianshu.io/upload_images/1234637-48672548a52cd9a4.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)  

 我们发现，数组元素组件可以很好地按需渲染，在渲染数组元素的情况下这种方法具有较高的运行效率。

但是，由于元素组件内部操作函数绑定了**唯一位置**的 state 操作逻辑，如`addAge(index){ state.pets[index].age += 1}`。假设我们还有 `state.children` 数组，数组元素的格式与 `state.pets` 一样, 我们要用相同的元素组件来同时显示和操作这两个数组时，这种数组渲染模式就不适用了。我们可以用第1种方案实现这种情况的需求，但第1种方案在渲染效率上不是很完美。

### pastate 数组元素操作方案
Pastate 的 imState 的每个节点本身带有节点位置的信息和 store 归宿信息，我们可以利用这一点来操作数组元素！
#### pastate 方案1:获取对于的响应式节点
我们使用 getResponsiveState 函数获取 imState 对于的响应式 state，如下：
```javascript
class PetsView extends PureComponent {
    ...
    render() {
        ...
        return (
            <div style={{ padding: 10, margin: 10 }}>
                ...
                {
                    state.map((pet, index) => 
                        <PetView 
                            state={pet} 
                            key={pet.id}   {/* 注意，这里无需传递 index 值，除非要在子组件中有其他用途*/}
                        />)
                }
                ...
            </div>
        )
    }
}
```
```javascript
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 的什么节点上! 这种模式使得复用组件可以在多个不同挂载位置的数组中使用，而且可以保证很好的渲染性能：  

![重新渲染情况](http://upload-images.jianshu.io/upload_images/1234637-5543f4352d57ba0d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)  

#### pastate 方案2:使用 imState 操作函数
Pastate 提供个三个直接操作 imState 的函数，分别为 `set`, `merge`, `update`。我们来演示用这些操作函数来代替  `getResponsiveState` 实现上面操作宠物年龄的功能：

```javascript
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 到父节点中，不可以使用  `getResponsiveState` ，`set` 或 `update`。我们在设计 state 结构时，应尽量避免使用绝对空值，我们完全可以用 `''`, `[]` 等代替绝对空值。

[下一章](./4.表单渲染与操作.md)，我们来看看如何在 pastate 中渲染和处理表单元素。