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
是数组的原生办法,用来做数组映射,相似的非变更办法(不会扭转原数组)还有 slice
、concat
、filter
这些,它们不会变更原始数组,而总是返回一个新数组
那么在 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
时会常常用到 map
、filter
这些
数组项为对象的状况
在开发中可能会遇到 this.list[1].name = 'jerry'
这种状况,如果数组项是对象,那么是能够通过下标间接更新这个对象的属性
其实是 Vue
在初始化数组的时候做了解决,对于数组项是非对象会间接返回,不做操作,如果是对象,那么会用 Observer
类初始化它,给对象属性加上 getter
、setter
监听器
局部源码:
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
给属性加上 getter
、setter
监听器,所以给数组项从新赋值就会触发响应了
详情src/core/observer/index.js
对于为什么 Vue
没有将 arr[index] = val
变成响应式,网上有很多探讨,作者也有答复,大体来说,就是 性能代价和取得的用户体验不成正比。arr[index] = val
尽管不是响应式,但也有提供的官网 API
来操作,作为一个框架,Vue
曾经做的够多了。当然 Vue3
将 Object.defineProperty
换成了 Proxy
,那么这个问题也就不复存在了。