首页 > 技术文章 > vue中使用对象数组的最佳实践

cloverclover 2020-09-04 18:28 原文

前言:

    在平常的开发中,经常会在vue中用到对象数组,如渲染一个小区的列表,数据结构可能如下所示:

CommunityList: [
    {
        _id: '',
        community_code: '',
        community_name: '',
        ...
    },
    ...
 ]

    当需要实现选择某几个小区加入收藏列表的时候,页面上往往需要增加el-checkbox来勾选,在数据结构上需要增加一个类似relation属性来标志该小区是否被选择。

    所以在从接口拿到数据的时候需要在数据上增加relation属性,处理方式可能如下所示:

/** result是从接口获取的数据 **/
if (result && result.retCode === 0) {
  this.communityList = result.list.map(item => {
    item.relation = false;
    return item;
  })
}

  

    当你碰巧在处理数据之前先将result.list赋值给了另一个实例属性,如下所示:

/** result是从接口获取的数据 **/
if (result && result.retCode === 0) {
  this.communityListCache = result.list; // 新增的代码
  this.communityList = result.list.map(item => {
    item.relation = false;
    return item;
  })
}

  

    那么此时el-checkbox的表现可能会不太正常,如下所示:

    点击时状态不会立马改变,而是等鼠标移开了才改变

              

 

    下面两个图均是在el-checkbox为灰色状态下点击的结果,一个为true,另一个却是false

 

             

             

 

分析:

      经过打断点分析发现某些属性并不是像其它属性一样的表现

                

 

      如这里的relation属性(el-checkbox绑定的就是该属性),不像下面的_id属性所表现的一致,_id属性所表现的就是正常的被vue监听的状态,很明显relation并没有被vue监听,问题就出现在新增的代码那里。

      通过分析vue的源代码可知,实例属性被赋值的时候会使用Object.defineProperties方法将其转化为可被监听的属性,如果赋值了一个数组,也会将数组转化为可被监听的对象,且会深入内部监听如下:

      截取自vue源代码中的部分片段:

export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that have this object as root $data

  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      this.observeArray(value) // 当为数组时,转化数组
    } else {
      this.walk(value)
    }
  }

...

  /**
   * Observe a list of Array items.
   */
  observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i]) // 逐级转化
    }
  }
}

    又因为js中引用类型只是指向实际数据的类似指针的结构,此时this.communityListCache和result.list在内存中实际就是指向同一个地址,当vue转化this.communityListCache的时候实际就是转化了result.list

this.communityListCache = result.list; // 新增的代码

    vue会在result.list中新增__ob__这个属性来标志已经被转化过了,如下图所示:

          

 

    由于relation是在之后才新增的属性,而在vue转化的时候,如果对象有__ob__属性,则表示已经被转化过了,不会再次去处理,如下:

    截取自vue源代码中的部分片段:

/**
 * Attempt to create an observer instance for a value,
 * returns the new observer if successfully observed,
 * or the existing observer if the value already has one.
 */
export function observe (value: any, asRootData: ?boolean): Observer | void {
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob: Observer | void
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { // 判断是否本来就存在__ob__属性,如果存在就直接赋值,不做处理
    ob = value.__ob__
  } else if (
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    ob = new Observer(value)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}

  这样后面新增的relation属性实际就是没有被vue所监听的属性,自然就不会响应变化。

 

解决办法:

      1、在完全处理好原始数据后再赋值给其它的实例属性,如下:

/** result是从接口获取的数据 **/
if (result && result.retCode === 0) {
  // this.communityCache = result.list; // 原始代码
  this.communityList = result.list.map(item => {
    item.relation = false;
    return item;
  });
  this.communityCache = result.list; // 新增的代码
}

  

    2、由于接口返回的数据一般没有包涵特殊类型,所以在赋值之前进行简单的深拷贝即可,这样的话,这两个变量就不是指向同一个对象,也就不会相互影响:

/** result是从接口获取的数据 **/
if (result && result.retCode === 0) {
  // this.communityCache = result.list; // 原始代码
  this.communityCache = JSON.parse(JSON.stringify(result.list)); // 新增代码
  this.communityList = result.list.map(item => {
    item.relation = false;
    return item;
  });
  this.communityCache = result.list; // 新增的代码
}

  

  注:个人总结,如有不对,还望指正,谢谢!

 

推荐阅读