【Vue.js】不要把所有东西都放进data里了(至少2.0是如
Vue组件实例中的data
是我们再熟悉不过的东西了,用来存放需要绑定的数据
但是对于一些特定场景,data虽然能够达到预期效果,但是会存在一些问题
我们写下如下代码,建立一个名单,记录他们的名字,年龄和兴趣:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
<div id="app"> </div>
<script src="some-data.js"></script>
<script>
const template = `
<div>
<h3>data列表</h3>
<ol>
<li v-for="item in dataList">
姓名:{{item.name}},年龄:{{item.age}},兴趣:{{item.hobby.join('、')}}
</li>
</ol>
</div>
`
new Vue({
el: '#app',
data () {
return {
dataList: [
{ name: '张三', age: 33, hobby: ['唱','跳','rap','篮球'] },
{ name: '李四', age: 24, hobby: ['唱','跳','rap','篮球'] },
{ name: '王五', age: 11, hobby: ['唱','跳','rap','篮球'] },
{ name: '赵六', age: 54, hobby: ['唱','跳','rap','篮球'] },
{ name: '孙七', age: 23, hobby: ['唱','跳','rap','篮球'] },
{ name: '吴八', age: 55, hobby: ['唱','跳','rap','篮球'] }
],
}
},
mounted () {
console.table(this.dataList) // 打印列表形式的dataList
console.log(this.dataList) // 打印字面量
},
template
})
</script>
</body>
</html>
Vue通过data生成我们能用的绑定数据,大概走了以下几个步骤:
1.从 initData
[1]方法 中获取你传入的data,校验data是否合法
2.调用observe
[2]函数,新建一个Observer
[3]实例,将data
变成一个响应式对象,而且为data添加 __ob__
属性,指向当前Observer
实例
3.Observer
保存了你的value
值、数据依赖dep
[4]和vue组件实例数vmCount
4.对你的data调用defineReactive$$1
[5]递归地监所有的key/value(你在data中声明的),使你的key/value都有自己的dep
, getter
[6] 和setter
[7]
我们忽略html的内容,重点放在这个dataList
上(我用2种不同的形式打印了dataList),如上述步骤2、3、4所说,data中每个key/value值(包括嵌套的对象和数组)都添加了一个Observer:
之前我们说滥用data会产生一些问题,问题如下:
设想一下这样的场景,如果你的data属于纯展示的数据,你根本不需要对这个数据进行监听,特别是一些比这个例子还复杂的列表/对象,放进data中纯属浪费性能。
那怎么办才好?
放进computed中
还是刚才的代码,我们创建一个数据一样的list,丢进computed里:
computed: {
computedList () {
return [
{ name: '张三', age: 33, hobby: ['唱','跳','rap','篮球'] },
{ name: '李四', age: 24, hobby: ['唱','跳','rap','篮球'] },
{ name: '王五', age: 11, hobby: ['唱','跳','rap','篮球'] },
{ name: '赵六', age: 54, hobby: ['唱','跳','rap','篮球'] },
{ name: '孙七', age: 23, hobby: ['唱','跳','rap','篮球'] },
{ name: '吴八', age: 55, hobby: ['唱','跳','rap','篮球'] }
]
}
},
打印computedList,你得到了一个没有被监听的列表
computedList
为什么computed没有监听我的data
因为我们的computedList中,没有任何访问当前实例属性的操作,根据Vue的依赖收集机制,只有在computed中引用了实例属性,触发了属性的getter,getter会把依赖收集起来,等到setter调用后,更新相关的依赖项
我们来看官方文档对computed的说明:
computed: {
// 计算属性的 getter
reversedMessage: function () {
// `this` 指向 vm 实例
return this.message.split('').reverse().join('')
}
}
这里强调的是
所以,对于任何复杂逻辑,你都应当使用计算属性。
但是很少有人注意到api说明中的这一句:
计算属性的结果会被缓存,除非依赖的响应式属性变化才会重新计算。
也就是说,对于纯展示的数据,使用computed会更加节约你的内存
另外 computed 其实是Watcher
[6]的实现,有空的话会更新这部分的内容
为什么说“至少2.0是如此”
因为3.0将使用Proxy来实现监听,性能将节约不少,参见https://www.jianshu.com/p/f99822cde47c
源码附录 (v2.6.10)
[1] initData: 检查data的合法性(比如是否作为函数返回、是否冲突或者没有提供data属性),初始化data
function initData (vm) {
var data = vm.$options.data;
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {};
if (!isPlainObject(data)) {
data = {};
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
var keys = Object.keys(data);
var props = vm.$options.props;
var methods = vm.$options.methods;
var i = keys.length;
while (i--) {
var key = keys[i];
{
if (methods && hasOwn(methods, key)) {
warn(
("Method \"" + key + "\" has already been defined as a data property."),
vm
);
}
}
if (props && hasOwn(props, key)) {
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 */);
}
[2] observe: 对当前对象新建一个Observer实例
function observe (value, asRootData) {
if (!isObject(value) || value instanceof VNode) {
return
}
var ob;
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
}
[3] Observer类:为对象声明依赖,和响应式方法,同时对数组做兼容处理(Vue可以通过调用使原数组变化的方法如push、reverse、sort等触发监听)
/**
* Observer class that is attached to each observed
* object. Once attached, the observer converts the target
* object's property keys into getter/setters that
* collect dependencies and dispatch updates.
*/
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)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
/**
* Observe a list of Array items.
*/
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
[4] dep 依赖类Dep的实例实例,当notify
被setter调用时触发Watcher更新,建议先看[5][6][7]再回过头来看参考
/**
* A dep is an observable that can have multiple
* directives subscribing to it.
*/
export default class Dep {
static target: ?Watcher;
id: number;
subs: Array<Watcher>;
constructor () {
this.id = uid++
this.subs = []
}
addSub (sub: Watcher) {
this.subs.push(sub)
}
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
if (process.env.NODE_ENV !== 'production' && !config.async) {
// subs aren't sorted in scheduler if not running async
// we need to sort them now to make sure they fire in correct
// order
subs.sort((a, b) => a.id - b.id)
}
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
// The current target watcher being evaluated.
// This is globally unique because only one watcher
// can be evaluated at a time.
Dep.target = null
const targetStack = []
export function pushTarget (target: ?Watcher) {
targetStack.push(target)
Dep.target = target
}
export function popTarget () {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}
[5] `defineReactive$$1 [6] getter [7] setter,数据绑定的核心方法:通过调用Object.defineProperty对对象中的每一个key添加dep依赖和设置getter和setter,getter触发依赖收集,setter触发依赖更新
/**
* Define a reactive property on an Object.
*/
function defineReactive$$1 (
obj,
key,
val,
customSetter,
shallow
) {
var dep = new Dep();
var property = Object.getOwnPropertyDescriptor(obj, key);
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
var getter = property && property.get;
var setter = property && property.set;
if ((!getter || setter) && arguments.length === 2) {
val = obj[key];
}
var childOb = !shallow && observe(val);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
var value = getter ? getter.call(obj) : val;
if (Dep.target) {
dep.depend();
if (childOb) {
childOb.dep.depend();
if (Array.isArray(value)) {
dependArray(value);
}
}
}
return value
},
set: function reactiveSetter (newVal) {
var value = getter ? getter.call(obj) : val;
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (customSetter) {
customSetter();
}
// #7981: for accessor properties without setter
if (getter && !setter) { return }
if (setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
childOb = !shallow && observe(newVal);
dep.notify();
}
});
}