前言
在vue组件中内data内函数返回的对象默认是响应式的,vue的observe函数会遍历此对象所有的属性和子孙属性并转化为getter/setter, 使Vue能够追踪依赖,在属性被访问和修改时通知变更。这种响应式在被用在模板更新、watch变更、computed依赖中非常有用。但如果我们的数据并不会改变,或者只会整体改变,或者本身就不需要响应式,那上述为深度响应式做的转化、依赖以及产生的闭包、watcher空间其实是多余的,白白浪费了时间和性能。平时我们自己写的对象不会太复杂这种性能消耗并不明显,但当在引用第三方工具库,比如图表、地图、模型等,如果把多个不需要深度响应式的第三方实例或数据直接挂载到data属性上,又或者遇到大数据量列表,性能的影响就会比较明显。本文会介绍几种目前本人尽可能想到的添加非响应式数据的方式,分析其中的利弊并给出推荐。
1、避免把数据挂载到data内函数返回的对象上
显然,如果不把数据挂载到data内函数返回的对象上,就不会被observe函数处理
1.1 将数据定义在export default之外
实现如下
const bigData = {
...
}
export default {
...
}
定义在export default之外的数据,依然能被export default内的代码正常访问。定义清晰,写法简单,但存在的2点问题,首先是不能在模板内使用,其次由于实质是定义在组件这个类上面的,是类的内部变量,被所有实例对象共享,其中一个实例对象的内改变数据,另一个对象内的数据也会被改变。因此这种方式更适合不需要在模板内使用的常量、不变配置项等。然而这种情况数据一般不会很大,因此实际应用场景有限。
1.2 将数据定义在组件的自定义属性上
实现如下
export default {
···
bigData: { // 自定义属性
....
},
methods: {
doSomething() {
return this.$options.bigData
}
}
}
由于挂载到实例上,通过例子中this.$options.bigData
的这种方式就可以正常访问数据。弊端在于数据的定义被分在了2个地方,添加的自定义属性对不了解的人会产生误解,使用时也会增加调用链。
最后,这种写法bigData
属性是非响应式的,如果数据更改,需要手动调用this.$forceUpdate()
才能使模板更新。
如无特殊说明,本文中提到的bigData
属性都是非响应式的。
2、利用Vue无法检测到对象属性的添加来实现
官方文档中有介绍
受现代 JavaScript 的限制 (而且 Object.observe 也已经被废弃),Vue 无法检测到对象属性的添加或删除。由于 Vue 会在初始化实例时对属性执行 getter/setter 转化,所以属性必须在 data 对象上存在才能让 Vue 将它转换为响应式的。例如:
var vm = new Vue({
data:{
a:1
}
})
// `vm.a` 是响应式的
vm.b = 2
// `vm.b` 是非响应式的
因此,我们可以待实例完成初始化observe后,即created中加入
export default {
data() {
return {}
},
created() {
this.bigData = {
···
}
}
···
}
这种写法简单,并且后添加是属性也可以在模板中访问。只是同样的,数据的定义被分在了2个地方。
3、剖析observe函数来寻找办法
终于来到observe内部,找到observer关键的几处代码,可以只留意几个切入口处:
function initData (vm: Component) {
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
if (!isPlainObject(data)) {
data = {}
process.env.NODE_ENV !== 'production' && warn(
'data functions should return an object:\n' +
'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
vm
)
}
// proxy data on instance
const keys = Object.keys(data)
const props = vm.$options.props
const methods = vm.$options.methods
let i = keys.length
while (i--) {
const key = keys[i]
if (process.env.NODE_ENV !== 'production') {
if (methods && hasOwn(methods, key)) {
warn(
`Method "${key}" has already been defined as a data property.`,
vm
)
}
}
if (props && hasOwn(props, key)) {
process.env.NODE_ENV !== 'production' && warn(
`The data property "${key}" is already declared as a prop. ` +
`Use prop default value instead.`,
vm
)
} else if (!isReserved(key)) {
proxy(vm, `_data`, key)
}
}
// observe data
observe(data, true /* asRootData */)
}
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)
}
}
/**
* Walk through all properties and convert them into
* getter/setters. This method should only be called when
* value type is Object.
*/
walk (obj: Object) {
const keys = Object.keys(obj) // 切入口2
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
// 省略
}
/**
* Define a reactive property on an Object.
*/
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep()
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) { // 切入口3
return
}
// 省略响应式处理代码
}
/**
* 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 = value.__ob__
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) && // 切入口1
!value._isVue
) {
ob = new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
vue生命周期内会调用initData,然后从函数observe开始,顺序找到其中可利用的判断点Object.isExtensible(value)、Object.keys(obj)、property.configurable === false
3.1使Object.isExtensible(value)返回为false
Object.isExtensible() 方法判断一个对象是否是可扩展的(是否可以在它上面添加新的属性)。默认情况下,对象是可扩展的:即可以为他们添加新的属性。Object.preventExtensions,Object.seal 或 Object.freeze 方法都可以标记一个对象为不可扩展。 下面以Object.preventExtensions为例
export default {
data() {
return {
bigData: Object.preventExtensions({
···
})
}
}
}
虽然上述的三种方法都可以实现,但也要注意区别,Object.seal会额外使其所有属性都不可配置且因此不可删除,Object.freeze更会使所有属性都不可更改。Object.seal和Object.freeze都会遍历一次对象的所有属性,性能上比Object.preventExtensions略差。不过由于是浅层遍历,实践上影响不大,而且对于数组,浏览器实现时做了优化,几乎没有性能损耗。综合看推荐使用Object.preventExtensions和Object.seal。 另外三者都有一个共同的注意点,当bigData值改变时,需要重新调用一次,即
updateBigData (newBigData) {
this.bigData = Object.preventExtensions(newBigData)
}
最后,这种写法bigData
属性是响应式的,值改变后模板会自动更新;当然如果是bigData某个属性改变,仍然需要手动调用this.$forceUpdate()
。
ps: 1.思考,为什么检测到不可添加新的数据,vue就不做响应式处理了? 2.第三方工具可能会需要在原数据上添加属性,这会限制此方法的适用范围。
3.2使挂载的数据key不在Object.keys(obj)返回的数组中
Object.keys() 方法会返回一个由一个给定对象的自身可枚举属性组成的数组。
3.2.1 把属性改成不可枚举
显然如果我们把属性改成不可枚举,就实现了 代码如下
export default {
data() {
const data = {
bigData: {
···
}
··· // 其他属性
}
Object.defineProperty(data, 'bigData', {
enumerable: false
})
return data
}
}
要注意的是,如果像例子这样bigData
是在根属性上,会跳过initData方法里把bigData
代理到vue实例的根属性上的过程,访问时需要使用this.$data.bigData
,无法避免作用域链加一级的情况。
3.2.2 使用Symble类型作为属性名称
利用Object.keys()无法获取Symbol类型属性名称的方式,实现如下
export default {
data() {
let bigData = Symbol.for('bigData')
return {
[bigData]: {
···
},
bigData,
··· // 其他属性
}
},
}
这种方法获取数据就更麻烦了,如果bigData
是在根属性上,需要使用this.$data[this.bigData]
或者this.$data[Symbol.for('bigData')]
来获取。
3.3 使property.configurable === false
这个和3.2.1写法类似
export default {
data() {
const data = {
bigData: {
···
}
··· // 其他属性
}
Object.defineProperty(data, 'bigData', {
configurable: false
})
return data
}
}
}
和3.2.1比好处是不会存在bigData
代理被跳过的情况,使用this.bigData
可以直接访问。
整体对比
方式 | 和响应式数据一起定义 | 模板自动更新 | 便捷程度 | 其他说明 | 推荐指数 |
---|---|---|---|---|---|
在export default之外定义 | ✘ | ✘ | 简单 | 不可在模板内使用;不可被改变 | ★ |
定义在自定义属性上 | ✘ | ✘ | 复杂 | ★★ | |
在created钩子中加入 | ✘ | ✘ | 简单 | 不需要在模板内使用时 | ★★★★★ |
使用Object.preventExtensions处理 | ✔ | ✔ | 较简单 | 适用广泛 | ★★★★ |
使用Object.seal处理 | ✔ | ✔ | 较简单 | 方法名称短 | ★★★★ |
使用Object.freeze处理 | ✔ | ✔ | 较简单 | 处理后的数据属性值不可更改 | ★★★ |
设置属性不可枚举 | ✔ | ✘ | 复杂 | ★ | |
使用Symble类型作为属性名称 | ✔ | ✘ | 复杂 | 难得可以使用Symble类型的场景,加一星 | ★★ |
设置属性描述符不可更改 | ✔ | ✘ | 一般 | ★★★ |
由上表可见,本人比较推荐的是在created钩子中加入和使用Object.preventExtensions处理,在数据对象属性不是超级多时也推荐使用Object.seal处理,毕竟字数较少。
简单性能对比
这里选取Object.preventExtensions、Object.seal来和正常数据做个简单的性能对比。数据采用随机生成包含一万属性的对象,并加入不加对象数据做对照组。
const getTestData = function (length = 1e4) {
return Array.from({ length }).reduce((pre, curr, index) => {
pre['attr' + index] = Math.random()
return pre
}, {})
}
结果如下
方式 | 结果 |
---|---|
不加数据 | 0.04ms |
正常响应式 | 13ms |
使用Object.preventExtensions处理 | 0.05ms |
使用Object.seal处理 | 0.13ms |
可见性能提升明显,另外这里只测试了初始化响应式数据花费的时间,实际项目中对运行速度、内存占用都有明显改善。