指针、深浅拷贝与react渲染机制的一些坑

前言

这次我们先巩固知识,再看问题。
刚刚入门的前端程序员,或者其他语言的程序员,一般都会先学习该语言的变量类型。
在前端的javascript中,变量分为值类型(简单类型)引用类型(复杂类型),以及ES6新出的symbol类型,Set和Map数据结构。
其中值类型string、number、boolean、undefined、null五种,当它们赋值给变量时,该变量直接存储其值于内存栈中。
引用类型object,其中function、array本质上也属于object,其特点是,当它们赋值给变量时,变量存储的是它们的指针,也就是存储地址,存于内存栈中。其真实的值存放在了该地址所指向的内存空间中,也就是内存堆中.

道理我们都懂,但仍过不好这一生仍然会踩坑。

举个例子

让我们来看一个简单的:
值类型的变量,由于存储的直接是该变量对应的值,所以变量之间是互不影响的:

var a = 1
var b = a
b += 1
console.log('a', a) // 1
console.log('b', b) // 2

引用类型的变量,由于存储的是对应值得指针,也就是存储地址,所以变量之间会相互影响:

var obj1 = {
  c: 1
}
var obj2 = obj1
obj2.c = 2
console.log('obj1', obj1) // {c: 2}
console.log('obj2', obj2) // {c: 2}

神奇的事情发生了!我并没有改变obj1,但是其中的a值居然发生了变化.这是因为obj2和obj1共用一个指针,改变了其中一个,存储于堆中的值就会发生改变,另一个也会相应变化,这就是引用类型的存储方式带来的弊端,如图:

变量存储图解

发现了问题,就该解决问题.如何解决这种弊端?
涉及到一个前端面试中常见的概念:深浅拷贝

深浅拷贝

浅拷贝: 很简单,就是上述代码中实现的方式 -- 使用表达式直接赋值.
深拷贝: 将引用类型存储方式带来的弊端消除, 也就是拷贝后的值与拷贝前的值不形成相互影响.大概有这么几种方式:
1. 递归遍历
2. 使用原生API进行编码和解码
3. 使用社区现有组件

1.递归遍历:
像上面的例子,对象的成员是Number类型,只需要对新变量遍历赋值就行.但如果对象成员也是Object类型,成员的成员也是Object类型,就不能单纯遍历这么简单了.这时候就要使用到递归:

var deepClone = function(currobj){
    if(typeof currobj !== 'object'){
        return currobj;
    }
    if(currobj instanceof Array){
        var newobj = [];
    }else{
        var newobj = {}
    }
    for(var key in currobj){
        if(typeof currobj[key] !== 'object'){
            // 不是引用类型,则复制值
            newobj[key] = currobj[key];
        }else{
            // 引用类型,则递归遍历复制对象
            newobj[key] = deepClone(currobj[key])    
        }
    }
    return newobj
}

缺陷:这个方法的主要问题就是不处理循环引用,不处理对象原型,函数依然是引用类型。上面描述过的复杂问题依然存在,可以说是最简陋但是日常工作够用的深拷贝方式。

2. 使用原生API进行编码和解码
比较巧妙的方法,使用JSON API进行序列化,先编码再解码:

// 调用JSON内置方法先序列化为字符串再解析还原成对象
newObj = JSON.parse(JSON.stringify(obj))

缺陷:JSON是一种表示结构化数据的格式,只支持简单值、对象和数组三种类型,不支持变量、函数或对象实例。所以我们工作中可以使用它解决常见问题,但也要注意其短板:函数会丢失,原型链会丢失,以及上面说到的所有缺陷。

3.使用社区现有组件
以上两种方式可以解决大部分业务场景,但都有缺陷.本文推荐引入社区现有组件来解决问题,不仅简单易用,还不用担心后遗症.常用的组件有以下两种:

  • lodash库(强烈推荐)
    lodash是一个一致性、模块化、高性能的 JavaScript 实用工具库,提供了很多处理数据的方法.我们要使用的是它的cloneDeep方法:
import _ from 'lodash'
var obj1 = {
  c: 1
}
var obj2 = _.cloneDeep(obj1)
obj2.c = 2
console.log('obj1', obj1) // {c: 1}
console.log('obj2', obj2) // {c: 2}

可以看到,使用lodash后,obj2对obj1进行了深拷贝,二者不会相互干扰,并且对任何复杂结构都有效,没有后顾之忧.

  • jQuery库
    jQuery这个库,是前端新人必学的,也是很多没有专职前端的公司的后端开发人员使用的库,优点是简化了繁琐的原生JS DOM操作,社区强大.但其本质还是操作DOM,对性能没有优化,大公司已基本弃用,转为使用高性能的流行框架react/vue/angular,无需操作DOM,有高效的渲染机制.
    但jQuery的某些方法还是可以参考的,比如我们要介绍的extend方法:
// 进行深度复制,如果第一个参数为true则深度复制,如果目标对象不合法,则抛弃并重构为{}空对象,如果只有一个参数则功能为扩展jQuery对象
jQuery.extend = jQuery.fn.extend = function() {
    var options, name, src, copy, copyIsArray, clone,
        target = arguments[ 0 ] || {},
        i = 1,
        length = arguments.length,
        deep = false;

    // Handle a deep copy situation
    // 第一个参数可以为true来确定进行深度复制
    if ( typeof target === "boolean" ) {
        deep = target;

        // Skip the boolean and the target
        target = arguments[ i ] || {};
        i++;
    }

    // Handle case when target is a string or something (possible in deep copy)
    // 如果目标对象不合法,则强行重构为{}空对象,抛弃原有的
    if ( typeof target !== "object" && !jQuery.isFunction( target ) ) {
        target = {};
    }

    // Extend jQuery itself if only one argument is passed
    // 如果只有一个参数,扩展jQuery对象
    if ( i === length ) {
        target = this;
        i--;
    }

    for ( ; i < length; i++ ) {

        // Only deal with non-null/undefined values
        // 只处理有值的对象
        if ( ( options = arguments[ i ] ) != null ) {

            // Extend the base object
            for ( name in options ) {
                src = target[ name ];
                copy = options[ name ];

                // Prevent never-ending loop
                // 阻止最简单形式的循环引用
                // var obj={}, obj2={a:obj}; $.extend(true, obj, obj2); 就会形成复制的对象循环引用obj
                if ( target === copy ) {
                    continue;
                }
                // 如果为深度复制,则新建[]和{}空数组或空对象,递归本函数进行复制
                // Recurse if we're merging plain objects or arrays
                if ( deep && copy && ( jQuery.isPlainObject( copy ) ||
                    ( copyIsArray = Array.isArray( copy ) ) ) ) {

                    if ( copyIsArray ) {
                        copyIsArray = false;
                        clone = src && Array.isArray( src ) ? src : [];

                    } else {
                        clone = src && jQuery.isPlainObject( src ) ? src : {};
                    }

                    // Never move original objects, clone them
                    target[ name ] = jQuery.extend( deep, clone, copy );

                // Don't bring in undefined values
                } else if ( copy !== undefined ) {
                    target[ name ] = copy;
                }
            }
        }
    }

    // Return the modified object
    return target;
};

react渲染机制的一些坑

用过react的人都知道,react控制数据的机制就是:将数据挂在到组件的state上,如果要使用的话,就从state中取,要存的话,就用setState来设置.
那么,问题来了:
如果存储的数据是引用类型,且同时在两个地方对此数据进行了处理,那么,不管你有没有进行setState,数据都会发生联动变化,导致不合预期的结果.比如,我在父组件拿到数据时加了一个is_selected字段:

...
let list = res.data || res.result
        list.photos = list.photos.map((v) => {
          v.is_selected = true
          return v
        })
        this.setState({
          selectedRows: [list],
          visibleDelPhotos: true
        })
...

页面展示是这样的:


案例页面

当用户进行点击checkbox时,我对该字段进行了改变:

// 选择要删除的违规图片
  onDelPhotoChange(data, e) {
    const value = e.target.checked
    let selectedRows = _.cloneDeep(this.state.selectedRows)
    // 改变是否选中状态
    selectedRows[0].photos = selectedRows[0].photos.map((v) => {
      if (v.url === data.url) v.is_selected = value
      return v
    })
    this.setState({ selectedRows })
  },

由于业务需要,子组件对数据进行了筛选,将没有勾选的图片剔除:

componentWillReceiveProps(nextProps) {
    let selectedRows = nextProps.selectedRows
...

          // 单独删除图片,筛选未勾选的图片并处理数据结构
          if (typeof selectedRows[i].photos[0] !== 'string') {
            selectedRows[i].photos = selectedRows[i].photos.filter(
              (v) => v.is_selected !== false && v.is_dirty
            )
            selectedRows[i].photos = selectedRows[i].photos.map((v) => v.url)
          }
...
}

这时候出现了奇怪的情况: 取消勾选后,重新勾选时,该图片消失了.
当时打印出的数据和逻辑都是正常的, 查了很久都没查出问题, 后来想到, 是否是引用类型的联动变化引起的坑?遂使用lodash进行深拷贝:

componentWillReceiveProps(nextProps) {
    let selectedRows = _.cloneDeep(nextProps.selectedRows)
    ...
}

世界清静了, bug没有了.
反过来推理原因: 父子组件对同一数据源进行操作, 子组件筛选掉了未勾选的图片, 导致父组件展示图片时该条数据消失了.
所以这里建议,:

  1. 遇到state中存取引用类型数据时, 都用lodash进行取值, 以防万一;
  2. 碰到类似的问题, 如果逻辑没有漏洞, 那么优先考虑是否有在两个地方操作同一数据源的情况, 是否是引用类型带来的坑.

react用久了, 多少会遇到几次这样的坑, 其他框架也类似.望引以为戒.


更新:

发现一种更简便的,不用引入第三方插件的原生方法:
var copy = Object.assign({}, data)
亲测有效:

var obj1 = { a: 1 };
var obj2 = Object.assign({}, obj1);
obj2.a = 2;
console.log(obj1.a);  // 1

你可能感兴趣的:(指针、深浅拷贝与react渲染机制的一些坑)