React引用数据类型与immutable.js的使用实例

代码最新内容请在github阅读,github上托管了完整的使用immutable.js管理state的代码实例,也欢迎您star和issue

一,React中浅层次拷贝的问题

例子1

我们给出下面的事实:

const detail = {name:'qinlaing',school:{loc:'dalian'}}
const copy = Object.assign({},detail);
copy.school.loc ="北京";
//此时你会发现我们的detail.school.loc也变成了"北京"了
copy.school === detail.school
//此时copy.school和detail.school指向同一个对象,引用相同,一个值被修改那么另一个同样被修改

这也就是告诉我们,如果你要对整个state对象进行处理,那么你一定要深度拷贝,而不是浅层次的拷贝,因为Object.assign这种类型会导致copy后的结果发送变化,而源对象也是会变化的。下面我再给出一个例子:

onChange = (relation)=>{
   this.setState({ownRelationship:relation});
   this.props.onSave&&this.props.onSave(Transform2RawRelationship(deepCopy(this.state.ownRelationship)));
 }

我先给出场景:假如我们每次删除一行数据的时候都会调用onChange函数,而onChange回调函数会接收到最新的删除了特定行的数据的对象。我们此处就是直接将state.ownRelationship设置为最新的对象。但是,除了设置state.ownRelationship以外,我们还会将最新的对象通知给上层的组件,于是我们直接调用了onSave方法,这里的逻辑本来很清楚,但是你仔细看看这一句代码:

this.props.onSave&&this.props.onSave(Transform2RawRelationship(deepCopy(this.state.ownRelationship)));

你的关注点应该是在Transform2RawRelationship方法上,它首先对this.state.ownRelationship做了一次深度拷贝,然后将调用Transform2RawRelationship方法的结果传递给上层组件。那么你肯定会问,为什么要做深度拷贝?

解答:如果我们不做深度拷贝,那么在方法Transform2RawRelationship中我们无法确保其是否对this.state.ownRelationship做了改变,以及做了改变后这个数据是否会导致render方法得到的数据不满足组件指定的格式从而报错,从而报错,从而报错!

那么做一次浅层次的复制是否可以?

解答:不可以。就像文章第一个例子展示的情况,我们虽然使用Object.assign做了一次浅层次的复制,但是当我们在方法Transform2RawRelationship中对shallowCopy(this.state.ownRelationship)的引用数据做了修改,那么原来的this.state.ownRelationship也会发生改变,从而有可能导致数据不是组件渲染需要的格式(因为setState后组件重新渲染,但是this.state.ownRelationship可能已经被污染了,即被Transform2RawRelationship默默的修改掉了,从而不是组件需要的渲染数据)

例子2

import '_' from 'lodash';
const Component = React.createClass({
  getInitialState() {
    return {
      data: { times: 0 }
    }
  },handleAdd() {
    let data = this.state.data;
    //data和this.state.data指向了同一个数据,即data:{times:0}这个引用类型
    data.times = data.times + 1;
    //此时你会发现我们的this.state.data和data.times都会发生变化了,因为他们指向同一个引用
    this.setState({ data: data });
    //我们此时的data还是原来的data,只是其times属性值发生变化了,所以SCU判断的时候要小心
    console.log(this.state.data.times);
    //此时this.state.data.times是已经修改后的数据了
  }
}

盗用别人的一个例子只是为了说明,如果是引用类型的时候不管是浅层次的拷贝,还是压根不拷贝都是很可能产生副作用的。所以上面的代码我们经常会做一次深拷贝,一方面是为了保持state的不可变特性,还有另一方面就是为了使得SCU(shouldComponentUpdate)能够正常判断,而不是两次nextProps或者nextState指向同一个引用。这也是为什么react官方建议将state设置为不可变的原因。

import '_' from 'lodash';

const Component = React.createClass({
  getInitialState() {
    return {
      data: { times: 0 }
    }
  },handleAdd() {
    let data = _.cloneDeep(this.state.data);
    data.times = data.times + 1;
    this.setState({ data: data });
    // 如果上面不做 cloneDeep,下面打印的结果会是已经加 1 后的值。
    console.log(this.state.data.times);
  }
}

二,React中引用类型导致组件不更新

首先看下面的例子:

class Parent extends React.Component{
   state = {
     school:{
       location:"Dalian",name :"DLUT"
     }
   }
   onClick =()=>{
    this.setState({school:{location:"HUNan",name:"湖南大学"}});
    // const newSchool = this.state.school;
    // newSchool.location = "HUNan";
    // this.setState({school:newSchool});
   }
   render(){
       return (
         <div> <Child school={this.state.school}/> <button onClick={this.onClick}>点击我改变state</button> </div> ) } } class Child extends React.Component{ shouldComponentUpdate(nextProps,nextState,nextContext){ return true; } render(){ return ( <div> 学校名称:{this.props.school.name} 学校位置:{this.props.school.location} <\/div> ) } } ReactDOM.render( <Parent/>,document.getElementById('example') );

我们通过this.state.school将上层组件的一个引用传入到子组件中,于是子组件就可以拿到这个上层组件的引用了。当我们点击按钮修改state的时候,即调用下面的逻辑:

this.setState({school:{location:"HUNan",name:"湖南大学"}});

如果你在Child组件的SCU方法中做如下判断,那么判断的结果就是false:

console.log('this.props.school===nextProps.school',this.props.school===nextProps.school);

原因在于,我们其实是采用了一个新的对象来替换原来的this.state.school中的对象,所以导致this.state.school的引用已经发生变化了。所以下面组件接受到的this.props.school和nextProps.school并不是指向同一个引用,因此上面返回了false。

但是如果你将上面setState用下面几句代码替代,问题就会出现了

const newSchool = this.state.school;
  newSchool.location = "HUNan";
  this.setState({school:newSchool});
 //其实newSchool和this.state.school指向的是同一个引用(即指针),因此导致下层组件接受到的this.props.school依然是同一个引用。即,此时我们的变量newSchool保存的是引用而不是一个对象,不过其引用的值指向的是该对象

此时我们的下面的判断就会得到true:

console.log('this.props.school===nextProps.school',this.props.school===nextProps.school);

原因很简单:在父组件中通过newSchool保存了一个指针,这个指针和this.state.school这个指针指向的对象是完全相同的。当你通过this.setState({school:newSchool})重置this.state.school的值的时候,其实你将school设置的内存地址newSchool和当前的this.state.schoo的内存地址是完全一样的,所以导致this.props.school和nextProps.school的内存地址是完全一样的。通过这个例子你要明白:this.props.school和nextProps.school的内存地址完全一样(newSchool只是this.state.school指针的一个copy)是导致组件不会重新渲染的罪魁祸首。你可以通过深度拷贝或者immutable(如果对象改变,那么会得到一个不一样的引用,而没有变化的部分结构依然可以共享)来完成组件重新渲染。同时,你也要注意到,通过下面这种方式来改变state组件是会重新渲染的,因为地址已经发生改变:

this.setState({school:{location:"HUNan",name:"湖南大学"}});

三,immutable.js的使用

immutable.js例子1

var map1 = Immutable.Map({a:1,b:2,c:3,school:{
        location:"DaLian",name :"DLUT"
     }});
   var map2 = map1.set('b',50);
    //(1)我这里仅仅设置了b的值,那么其他的值都会共享
    console.log('引用相等',map1===map2);
    //(2)immutable.js中每次返回的引用都是不一样的,此处返回false
    console.log('school的引用没有变化',map2.school===map1.school);
    //(3)immutable.js中没有变化的对象将会共享,所以此处返回true
    var map3 = {a:1,c:3}
    var map4 = map3;
    //(4)map4拿到的是map3的指针,所以一个变化后另外一个也会变化,但是变化的是值,引用本身是不变化的,所以map3===map4返回true
    map4.c =4;
    console.log('map3===map4',map3===map4);

immutable.js例子2

var map1 = Immutable.fromJS(
      {a:1,home:{
         location:{
           name:'Hunan huaihua',street:405
         }
       },school:{
       location:"DaLian",name :"DLUT",ratio:{
         Hunan:698,ZheJiang : 900
       }
      }
    });
 var map2 = map1.updateIn(['school','ratio',"ZheJiang"],value => value + 1);
 console.log('引用相等',map1===map2);
 //打印false
 const updatedRatio = map1.getIn(["school","ratio"])===map2.getIn(["school","ratio"]);
 console.log('map2在map1的基础上更新了ratio此时ratio引用不再相等',updatedRatio);
 //打印false
 const updatedSchool = map1.getIn(["school"]) === map2.getIn(['school']);
 console.log('map2在map1的基础上更新了ratio此时school引用不再相等',updatedSchool);
 //打印false
 const updatedObject = map1 === map2;
 console.log('map2在map1的基础上更新了ratio此时对象引用不再相等',updatedObject);
//打印false
 const updatedHome = map1.getIn(["home"]) === map2.getIn(['home']);
 console.log('map2在map1的基础上更新了ratio此时home引用依然相等',updatedHome);
 //打印true
 const updatedHomeLocation = map1.getIn(["home","location"]) === map2.getIn(['home','location']);
 console.log('map2在map1的基础上更新了ratio此时home的location引用依然相等',updatedHomeLocation);
 //打印true

我想要通过这两个例子给自己一个直观的认知,如果你通过immutable.js的方法修改了对象的某一个属性的时候,该属性的所有的父级属性的引用都会发生改变,而其他属性的引用都是共享的。这部分网上都有的说,但是通过代码展现出来也能够加深理解。因此在immutable.js中你常常会看到这样的SCU:

import { is } from 'immutable';
shouldComponentUpdate: (nextProps = {},nextState = {}) => {
  const thisProps = this.props || {},thisState = this.state || {};
  if (Object.keys(thisProps).length !== Object.keys(nextProps).length ||
      Object.keys(thisState).length !== Object.keys(nextState).length) {
    return true;
  }
  for (const key in nextProps) {
    if (!is(thisProps[key],nextProps[key])) {
      return true;
    }
  }
  for (const key in nextState) {
    if (thisState[key] !== nextState[key] || !is(thisState[key],nextState[key])) {
      return true;
    }
  }
  return false;
}

其主要通过immutable.js的is方法来判断数据是否发生变化,Immutable.js提供了简洁高效的判断数据是否变化的方法,只需 === 和 is比较就能知道是否需要执行render(),而这个操作几乎0成本,所以可以极大提高性能。所以如果我们有如下的组件树:

而且我们的props是从最顶层组件往下传递的,那么对于整个组件树中的组件不会都要求重新渲染。而渲染的只会是绿色的部分,因为只有这部分的数据发生改变,按照immutable.js的实现,只有在变化的节点以及父节点的引用会发生变化,从而要求重新渲染。

继续阅读请前往我的github

参考文献:

如何有效地提高react渲染效率–深复制,浅复制,immutable原理

Immutable 详解及 React 中实践

react组件性能优化探索实践

正式学习 React(三)番外篇 reactjs性能优化之shouldComponentUpdate

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


react 中的高阶组件主要是对于 hooks 之前的类组件来说的,如果组件之中有复用的代码,需要重新创建一个父类,父类中存储公共代码,返回子类,同时把公用属性...
我们上一节了解了组件的更新机制,但是只是停留在表层上,例如我们的 setState 函数式同步执行的,我们的事件处理直接绑定在了 dom 元素上,这些都跟 re...
我们上一节了解了 react 的虚拟 dom 的格式,如何把虚拟 dom 转为真实 dom 进行挂载。其实函数是组件和类组件也是在这个基础上包裹了一层,一个是调...
react 本身提供了克隆组件的方法,但是平时开发中可能很少使用,可能是不了解。我公司的项目就没有使用,但是在很多三方库中都有使用。本小节我们来学习下如果使用该...
mobx 是一个简单可扩展的状态管理库,中文官网链接。小编在接触 react 就一直使用 mobx 库,上手简单不复杂。
我们在平常的开发中不可避免的会有很多列表渲染逻辑,在 pc 端可以使用分页进行渲染数限制,在移动端可以使用下拉加载更多。但是对于大量的列表渲染,特别像有实时数据...
本小节开始前,我们先答复下一个同学的问题。上一小节发布后,有小伙伴后台来信问到:‘小编你只讲了类组件中怎么使用 ref,那在函数式组件中怎么使用呢?’。确实我们...
上一小节我们了解了固定高度的滚动列表实现,因为是固定高度所以容器总高度和每个元素的 size、offset 很容易得到,这种场景也适合我们常见的大部分场景,例如...
上一小节我们处理了 setState 的批量更新机制,但是我们有两个遗漏点,一个是源码中的 setState 可以传入函数,同时 setState 可以传入第二...
我们知道 react 进行页面渲染或者刷新的时候,会从根节点到子节点全部执行一遍,即使子组件中没有状态的改变,也会执行。这就造成了性能不必要的浪费。之前我们了解...
在平时工作中的某些场景下,你可能想在整个组件树中传递数据,但却不想手动地通过 props 属性在每一层传递属性,contextAPI 应用而生。
楼主最近入职新单位了,恰好新单位使用的技术栈是 react,因为之前一直进行的是 vue2/vue3 和小程序开发,对于这些技术栈实现机制也有一些了解,最少面试...
我们上一节了了解了函数式组件和类组件的处理方式,本质就是处理基于 babel 处理后的 type 类型,最后还是要处理虚拟 dom。本小节我们学习下组件的更新机...
前面几节我们学习了解了 react 的渲染机制和生命周期,本节我们正式进入基本面试必考的核心地带 -- diff 算法,了解如何优化和复用 dom 操作的,还有...
我们在之前已经学习过 react 生命周期,但是在 16 版本中 will 类的生命周期进行了废除,虽然依然可以用,但是需要加上 UNSAFE 开头,表示是不安...
上一小节我们学习了 react 中类组件的优化方式,对于 hooks 为主流的函数式编程,react 也提供了优化方式 memo 方法,本小节我们来了解下它的用...
开源不易,感谢你的支持,❤ star me if you like concent ^_^
hel-micro,模块联邦sdk化,免构建、热更新、工具链无关的微模块方案 ,欢迎关注与了解
本文主题围绕concent的setup和react的五把钩子来展开,既然提到了setup就离不开composition api这个关键词,准确的说setup是由...
ReactsetState的执行是异步还是同步官方文档是这么说的setState()doesnotalwaysimmediatelyupdatethecomponent.Itmaybatchordefertheupdateuntillater.Thismakesreadingthis.staterightaftercallingsetState()apotentialpitfall.Instead,usecom