关于vue.js:关于vue更新数组项你知道多少

10次阅读

共计 4013 个字符,预计需要花费 11 分钟才能阅读完成。

vue 中如何更新数组某一项,这是开发中常常遇到的问题

例如:data 外面有个 list 数组,数组外面有三项

export default {data() {
    return {
      list: [
        'foo',
        'bar',
        'baz'
      ]
    }
  }
}

如何将第二项的值更新为 'jerry'

不少小伙伴可能都试过 this.list[1] = 'jerry',但遗憾的是页面并不会更新
这种办法的确扭转了 list[1] 的值,但没法触发页面更新

那么在 Vue 中,如何更新数组某一项呢?上面是总结的一些办法:

数组原生办法

Array.prototype.splice 被称为数组最弱小的办法,具备删除、减少、替换性能,能够用 splice 来更新

this.list.splice(1, 1, 'jerry')

为什么 splice 能够触发更新?
Vue 将被侦听的数组(这里就是list)的变更办法进行了包裹,所以它们也将会触发视图更新。这些被包裹过的办法包含:

  • push()
  • pop()
  • shift()
  • unshift()
  • splice()
  • sort()
  • reverse()

splice 不再是数组原生办法了,而是 Vue 重写过的办法
局部源码:

const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

/**
 * Intercept mutating methods and emit events
 */
methodsToPatch.forEach(function (method) {
  // cache original method
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator (...args) {const result = original.apply(this, args)
    const ob = this.__ob__
    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    if (inserted) ob.observeArray(inserted)
    // notify change
    ob.dep.notify()
    return result
  })
})

能够看到,splice 除了执行自身的逻辑 (original.apply(this, args)) 之外,会把插入的值变成响应式对象 (observeArray(inserted)),而后调用 ob.dep.notify() 手动触发依赖告诉。
详情 src/core/observer/array.js

官网 API Vue.set()

Vue.set 是官网提供的全局 API,别名 vm.$set,用来被动触发响应

Vue.set(this.list, 1, 'jerry')
// 或者
this.$set(this.list, 1, 'jerry')

其实 set 办法实质上还是调用的 splice 办法来触发响应,局部源码

function set (target: Array<any> | Object, key: any, val: any): any {
  // ...
  if (Array.isArray(target) && isValidArrayIndex(key)) {target.length = Math.max(target.length, key)
    target.splice(key, 1, val)
    return val
  }
  // ...
}

set 的第一个参数为数组时,间接调用 target.splice()
详情 src/core/observer/index.js

vm.$forceUpdate()

强制使 Vue 实例从新渲染,其实 this.list[1] = 'jerry' 操作,list 的确曾经更改了,咱们调用 vm.$forceUpdate() 能够强制渲染

this.list[1] = 'jerry'
this.$forceUpdate()

通常你应该防止应用这个办法,而是通过数据驱动的失常办法来操作

当你无路可走的时候,能够试试这个办法,但此办法不可滥用,想想你只是想更改某个数组项,然而却可能更新了整个组件

正如官网所说的:

如果你发现你本人须要在 Vue 中做一次强制更新,99.9% 的状况,是你在某个中央做错了事。

深拷贝

个别粗犷的都是通过 序列化而后反序列化 回来 来实现

this.list[1] = 'jerry'
this.list = JSON.parse(JSON.stringify(this.list))

可能你还会封装本人的 cloneDeep 办法,尽管也能触发响应,然而仅仅是更新某一项就要用到深拷贝,的确有点顺当

map()

map 是数组的原生办法,用来做数组映射,相似的非变更办法(不会扭转原数组)还有 sliceconcatfilter 这些,它们不会变更原始数组,而总是返回一个新数组
那么在 Vue 中咱们间接替换数组也是能够实现更新的

this.list = this.list.map((item, index) => {if (index === 1) {return 'jerry'}
  return item
})

你可能认为这将导致 Vue 抛弃现有 DOM 并从新渲染整个列表。侥幸的是,Vue 做的够多。Vue 为了使得 DOM 元素失去最大范畴的重用而实现了一些智能的启发式办法,所以用一个含有雷同元素的数组去替换原来的数组是十分高效的操作。还记得在模板中应用 v-for 必须要提供 key 吗,Vue 依据这个 key 值可能更高效的找出差别,精准定位并更新。

实际上,这种基于源数据生成新数据(同时不影响内部)的形式合乎函数式编程的思维,如果你用过 redux,那么在写 reducer 时会常常用到 mapfilter 这些

数组项为对象的状况

在开发中可能会遇到 this.list[1].name = 'jerry' 这种状况,如果数组项是对象,那么是能够通过下标间接更新这个对象的属性
其实是 Vue 在初始化数组的时候做了解决,对于数组项是非对象会间接返回,不做操作,如果是对象,那么会用 Observer 类初始化它,给对象属性加上 gettersetter监听器
局部源码:

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)
    }
  }
  walk (obj: Object) {const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {defineReactive(obj, keys[i])
    }
  }
  observeArray (items: Array<any>) {for (let i = 0, l = items.length; i < l; i++) {observe(items[i])
    }
  }
}

Observer 类初始化时,如果是数组,个别状况下会调用 protoAugment()observeArray()protoAugment 会将 value(数组)的 __proto__ 指向 arrayMethods,这里着重看 observeArray,它会在每个数组项调用 observe()observe 如下:

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 = 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
}

能够看到,如果数组项不是对象,会间接返回;数组项为对象,会对该对象持续进行 Observer 初始化,进而调用 walk(),对每个属性调用 defineReactive()
defineReactive 会通过 Object.defineProperty 给属性加上 gettersetter监听器,所以给数组项从新赋值就会触发响应了
详情 src/core/observer/index.js

对于为什么 Vue 没有将 arr[index] = val 变成响应式,网上有很多探讨,作者也有答复,大体来说,就是 性能代价和取得的用户体验不成正比
arr[index] = val 尽管不是响应式,但也有提供的官网 API 来操作,作为一个框架,Vue 曾经做的够多了。当然 Vue3Object.defineProperty 换成了 Proxy,那么这个问题也就不复存在了。

正文完
 0