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,你得到了一個沒有被監聽的列表
為什么computed沒有監聽我的data
因為我們的computedList中,沒有依賴,即沒有任何訪問響應式數據(如data/props上的屬性/其他依賴過的computed等)的操作,根據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來實現監聽,性能將節約不少,參見http://www.lxweimin.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();
}
});
}