全文字数约 1000 字
为什么要写这篇文章
锻炼写作能力、总结能力。输出更好更优质的技术文章。
数据操作
对象
问题1
<div id="demo">
<button v-on:click="update">
{{obj.name}} <!-- 'res'-->
</button>
<button v-on:click="add">
{{obj}} <!-- {name: 'res'}-->
</button>
</div>
export default {
data:{
return {
obj: {name: 'res'}
}
},
methods:{
update(){
delete this.obj.name;// {} 页面无法响应更新
},
add(){
this.obj.age = 33;// {name:'res',age:'33'} 页面无法响应更新
}
}
}
为什么?
我们都知道 vue 的核心响应式采用了 object.defineProperty.通过属性函数getter 和 setter ,vue 在组件初始化中,通过递归对数据模版中现有的数据对象进行了深层次读写劫持,实现了数据和视图之间的双向交互。但也因此导致 vue 对数据的新增和删除是无感的。
怎么解决?
- 手动刷新 -- 不建议这么操作
this.obj.__ob__.dep.notify() //notify即是dep通知更新的函数
- API 删除
this.$delete(this.obj, 'name');//$delete => 如果对象是响应式的,确保删除能触发更新视图
this.$set(this.obj, 'age', 2222);//$set => 将数据添加为响应式
所以
- 在项目中我们应该避免直接 delete obj key
- 保证 对象中常用的 key 在原始模版中被初始化,以免带来不必要的困惑
数组
问题2
<div id="demo">
<button v-on:click="clear">
{{arr}}
</button>
<button v-on:click="update">
{{arr}}
</button>
<button v-on:click="add">
{{arr}}
</button>
</div>
export default {
data:{
return {
arr: ['age', { name: '111' }]
}
},
methods:{
clear(){
this.arr.length = 0;//页面无法响应更新?
},
update(){
this.arr[1].name = 222;//页面无法响应更新?
this.arr[0] = '111111';//页面响应更新?
},
add(){
this.arr[1] = 'fdfsdf';//页面无法响应更新?
},
}
}
为什么?
上文我们知道 vue 使用了object.defineProperty 对数据对象进行了响应式处理,那么对于数组 vue 是怎么做的呢?让我们来看一段源码
// Array原型
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
// 会改变原数组的操作
const methodsToPatch = [
'push',//末尾添加
'pop',//末尾删除
'shift',//首位删除
'unshift',//首位添加
'splice',//截取删除替换
'sort',//排序
'reverse'//翻转
]
methodsToPatch.forEach(function (method) {
const original = arrayProto[method]
//def => defineProperty方法的封装
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args)
const ob = this.__ob__
//通知 dep 更新
ob.dep.notify()
return result
})
})
是的,vue 总结了会改变数组的所有方法;对这些方法进行了重写;最终通过__proto__引入到数组的原型中。这也是为什么 我们在执行 clear 和 update 第一步操作 的时候无法触发更新;那又是为什么 update 第二步操作会触发更新呢,让我们再看一段源码。
export class Observer {
value: any;
dep: Dep;
vmCount: number;
constructor (value: any) {
if (Array.isArray(value)) {
// 数组的 响应处理
this.observeArray(value)
}
}
//遍历数组中所有的元素
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
//响应处理函数
function observe (value: any, asRootData: ?boolean): Observer | void {
if (!isObject(value) || value instanceof VNode) {
return
}
// __ob__ 代表被响应式处理过
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
) {
//重点在这里 对数组中的value 进行深层的递归 响应处理
ob = new Observer(value)
}
return ob
}
通过上面的代码我们可以得知,vue 对数组中元素的进行了深层响应处理。这也是为什么update 第二步中对数组对象的操作会触发更新。但是对于相对浅层的数据对象,递归的效率会很高,但是随着嵌套层级的递增,数组的响应初始化效率也在递减。
怎么解决?
- 手动刷新(和上文对象的操作一样) -- 不建议这么操作
this.arr.__ob__.dep.notify() //notify即是dep通知更新的函数
- API
this.$delete(this.arr, 0);//$delete => 如果对象是响应式的,确保删除能触发更新
所以
- 避免直接操作key改变数组的行为。尽量使用push pop等操作。
- 减少嵌套过深数组对象的使用。
小结
对象和数组是项目中最常见的两种数据类型,优化对两者的操作方法会在很大程度上影响程序的健壮性。vue 框架为我们提供了很多开箱即用的方法和api,合理的使用则会事半功倍。
另外您在阅览文章的时候发现问题,请您在评论区不吝赐教~🤝