react-immutable

loading 2022年12月13日 131次浏览

1. 引子

以前的深复制方案总有各种各样的问题:

  • ...展开运算符复制:甚至不能称为深复制,因为只是比浅复制多了一层而已。
  • json.stringify/json.parse:弊端
  • 手写递归:性能极差,需要把所有节点都遍历

因此,需要一种更为完善的深拷贝方案:引入immutable搭配react使用。

每次修改一个Immutable对象时都会创建一个新的不可变的对象,在新对象上操作并不会影响到原对象的数据。

Immutable实现的原理是Persistent Data Structure(持久化数据结构),也就是使用旧数据创建新数据时,要保证旧数据同时可用且不变。同时为了避免deepCopy把所有节点都复制一遍带来的性能损耗,Immutable使用了Structural Sharing(结构共享),即如果对象树中一个节点发生变化,只修改这个节点和受它影响的父节点。

2. 基本操作

通过Map或者fromJS转换对象

import {Map} from 'immutable'

let obj = {
    name : 'react',
    age: 100
}

// 将对象转换为immutable对象
let oldImmuObj = Map(obj);

// set设置属性
let newImmuObj = oldImmuObj.set('name' , 'vue')

// get获取属性值
console.log(oldImmuObj.get('name') , newImmuObj.get('name'))

// toJS转换回普通对象
console.log(oldImmuObj.toJS() , newImmuObj.toJS())

结合react的state,有两种写法:

直接在state阶段就Map转换,那么在渲染时就要通过get去获取

export default class App extends Component {

    state = {
        info:Map({
            name:"react",
            age:100
        })
    }

    render() {
        return (
            <div>

                <button onClick={()=>{
                    this.setState({
                        info:this.state.info.set("name","xiaoming").set("age",18)
                    })
                }}>click</button>
                {this.state.info.get("name")}--
                {this.state.info.get("age")}
            </div>
        )
    }
}

或者在点击按钮时才在事件中进行转化

export default class App extends Component {

    state = {
        info: {
            name: "react",
            age: 100
        }
    }

    render() {
        return (
            <div>
                <button onClick={() => {
                    var oldImmu = Map(this.state.info)
                    var newImmu = oldImmu.set("name", "xiaoming").set("age", 18)
                    this.setState({
                        info: newImmu.toJS()
                    })
                }}>click</button>
                {this.state.info.name}--
                {this.state.info.age}
            </div>
        )
    }
}

通过List转换数组

var arr = List([1, 2, 3])
var arr2 = arr.push(4) //不会影响老的对象结构
var arr3 = arr2.concat([5, 6, 7])
console.log(arr.toJS(), arr2.toJS(), arr3.toJS())

3. 应用于shouldComponentUpdate

假设父组件只给子组件传了属性a,但是父组件修改自己的状态b所导致的重新渲染也会导致子组件的重新渲染,此时子组件可以通过shouldComponentUpdate来阻止渲染。

之前scu是通过json.stringify()的方式判断新老对象是否相同,但是这种判断方式会有各种弊端,比如对象里有undefined等。这时候就可以通过immutable实现:

export default class App extends Component {
    // 父组件状态 这里要用两次Map包住对象
    state = {
        info: Map({
            name: "react",
            filter: Map({
                text: ""
            })
        })
    }

    render() {
        return (
            <div>
                {/* 修改状态 引发re-render */}
                <button onClick={() => {
                    this.setState({
                        info: this.state.info.set("name", "xiaoming")
                    })
                }}>click</button>
                {this.state.info.get("name")}
                {/* 将filter这个immutable对象传给孩子 */}
                <Child filter={this.state.info.get("filter")} />
            </div>
        )
    }
}


class Child extends Component {
    shouldComponentUpdate(nextProps, nextState) {
        // 这里此时以直接用===来判断 而无需再通过json.stringify了
        if (this.props.filter === nextProps.filter) {
            return false
        }
        return true
    }

    render() {
        return <div>
            child
        </div>
    }
}

4. 个人信息修改demo

首先写出一个比较复杂的state,既包含对象又包含数组,**为了保证老state的可用,需要复制一个新的state用于操作。**此时就只有immutable比较合适。

    state = {
        info: Map({
            name: "react",
            location: Map({
                province: "广东",
                city: "广州"
            }),
            favor: List(["读书", "看报", "写代码"])
        })
    }

具体主要就是先通过fromJS转换为对象,再通过setIn,updateIn进行修改属性,更新数组等操作。

render() {
        return (
            <div>
                <h1>个人信息修改</h1>
                <button onClick={() => {
                    this.setState({
                        // 通过setIn修改属性
                        info: this.state.info.setIn(["name"], "vue").setIn(["location", "city"], "深圳")
                    })
                }}>修改</button>
                <div>
                    {this.state.info.get("name")}-
                    {this.state.info.get("location").get("province")}-
                    {this.state.info.get("location").get("city")}

                    {
                        this.state.info.get("favor").map((item, index) =>
                            <li key={item}>{item}
                                <button onClick={() => {
                                    this.setState({
                                        // 通过updateIn修改数组 接受回调函数返回一个新的list
                                        info: this.state.info.updateIn(["favor"], (list) => list.splice(index, 1))
                                    })
                                }}>del</button>
                            </li>
                        )
                    }
                </div>
            </div>
        )
    }