指针、深浅拷贝与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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容