vue框架 中最核心的就是 vue的響應(yīng)式 ,通過對
vue中data數(shù)據(jù)的變更實現(xiàn)頁面效果的重新渲染。但在實際開發(fā)中經(jīng)常會有人發(fā)現(xiàn)明明更改了對應(yīng)數(shù)據(jù)的值,但是vue卻沒有重新渲染。明明說好了是響應(yīng)式的,但為什么有的數(shù)據(jù)可以通過響應(yīng)式實現(xiàn),而有的只能通過vue.set方法實現(xiàn)。這里就會有一個疑問:vue的響應(yīng)式到底是怎么回事,他的原理又是什么
這個文章主要就是針對這個疑問所對Vue 響應(yīng)式的一個解析。
前言
直白的描述下響應(yīng)式,就是對你數(shù)據(jù)的變化,vue會有一個響應(yīng),去完成某件事。
由于Vue 的核心庫只關(guān)注視圖層,所以這件事就是去重新渲染頁面。具體是局部渲染還是全部渲染,這個是基于vue的一個性能優(yōu)化,本篇文章暫時就不談了。
看到這里應(yīng)該明白了些,vue的響應(yīng)式原理是基于vue知道數(shù)據(jù)發(fā)生了改變。
所以針對上文提到的可以重新描述下問題:什么時候的賦值是才不是vue的盲區(qū)?
Vue數(shù)據(jù)劫持
數(shù)據(jù)劫持
數(shù)據(jù)劫持: vue.js 則是采用數(shù)據(jù)劫持結(jié)合發(fā)布者-訂閱者模式的方式,通過Object.defineProperty()來劫持各個屬性的setter,getter,在數(shù)據(jù)變動時發(fā)布消息給訂閱者,觸發(fā)相應(yīng)的監(jiān)聽回調(diào)。
Object.defineProperty() 方法會直接在一個對象上定義一個新屬性,或者修改一個對象的現(xiàn)有屬性, 并返回這個對象。
如下面的代碼塊就是一個很簡單的Getter/Setter構(gòu)造器。
當(dāng)obj的param被賦值的時候,執(zhí)行set對應(yīng)的方法;當(dāng)obj的param被取值的時候,執(zhí)行g(shù)et對應(yīng)的方法。
var obj = {};
Object.defineProperty(obj,"param",{
set :function SelfSetter(newVal) {
console.log("被賦值")
this._param = newVal
},
get :function SelfGetter () {
console.log("取值")
return this._param
}
})
obj.param = 'newparam'; // 被賦值
console.log(obj.param); // 取值 newparam
通過下方源碼可以看到,當(dāng)set的回調(diào)觸發(fā)之后將數(shù)據(jù)的變動推送給訂閱者。
// Vue.js v2.5.13 #964-1018
function defineReactive (
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;
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 ("development" !== 'production' && customSetter) {
customSetter();
}
if (setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
childOb = !shallow && observe(newVal);
dep.notify();
}
});
}
發(fā)布消息給訂閱者
在上述vue源碼中可以看到,當(dāng)set的回調(diào)觸發(fā)時,回去執(zhí)行dep.notify();發(fā)布消息給訂閱者。
迭代數(shù)據(jù)劫持
上述描述中僅僅只是對數(shù)據(jù)的一個屬性進行了劫持,vue中通過observe方法實現(xiàn)所有的數(shù)據(jù)屬性的劫持。
// Vue.js v2.5.13 #934-959
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 (
observerState.shouldConvert &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value);
}
if (asRootData && ob) {
ob.vmCount++;
}
return ob
}
數(shù)據(jù)劫持盲區(qū)
在日常開發(fā)時候發(fā)現(xiàn)的賦值之后沒有渲染頁面,也就是說該屬性沒有被劫持,當(dāng)變化時候沒有去發(fā)布消息給訂閱者,屬于盲區(qū)。
簡述
- vue在實例化的時候會將data數(shù)據(jù)中的屬性都做數(shù)據(jù)劫持。
- 如果是對象,也會迭代本身屬性將全部屬性都實現(xiàn)數(shù)據(jù)劫持。
- 當(dāng)賦值的時候,如果是
newVal是對象,也會去迭代newVal的屬性實現(xiàn)全部屬性的數(shù)據(jù)劫持
那在什么情況下會有盲區(qū):如果對一個對象添加一個新的屬性 如obj本身沒有newparam屬性,現(xiàn)在 obj.newparam = 1,因為本身obj.newparam的屬性不是Getter/Setter,所以在賦值后不會去發(fā)布消息給訂閱者。這就是所謂的盲區(qū)。而且無論以后obj.newparam如何變化都不會發(fā)布消息(實際就是vue根本不知道這個屬性發(fā)生了變化)。
但是有一個有趣的現(xiàn)象:就是雖然obj.newparam不會發(fā)布消息,但是如果別的發(fā)布者觸發(fā)的時候,頁面局部渲染時如果包括obj.newparam的值,渲染效果也是會顯示obj.newparam的最新值。這是由于頁面更新時是直接讀取的obj.newparam的值。
數(shù)組Array的特殊性
可能又有同學(xué)會發(fā)現(xiàn),Array對象是沒有辦法通過上述方法實現(xiàn)數(shù)據(jù)劫持的。
那么數(shù)組是如何實現(xiàn)的:vue中實現(xiàn)的方法實際是對數(shù)組的屬性重寫,重寫過后的方法不僅能實現(xiàn)原有的功能,還能發(fā)布消息給訂閱者。
// Vue.js v2.5.13 #818-851
var arrayProto = Array.prototype;
var arrayMethods = Object.create(arrayProto);[
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
].forEach(function (method) {
// cache original method
var original = arrayProto[method];
def(arrayMethods, method, function mutator () {
var args = [], len = arguments.length;
while ( len-- ) args[ len ] = arguments[ len ];
var result = original.apply(this, args);
var ob = this.__ob__;
var 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
});
});
當(dāng)然Array也有特殊現(xiàn)象:如果要更新 Array 某個索引對應(yīng)的值得時候,要用Vue.set方式實現(xiàn)
Vue.set
一直在說Vue.set是對數(shù)據(jù)進行攔截,那么Vue.set究竟是什么
閱讀源碼就能發(fā)現(xiàn),實際就是數(shù)據(jù)劫持處理,并發(fā)布一次消息
// #1025-1050
function set (target, key, val) {
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key);
target.splice(key, 1, val);
return val
}
if (key in target && !(key in Object.prototype)) {
target[key] = val;
return val
}
var ob = (target).__ob__;
if (target._isVue || (ob && ob.vmCount)) {
"development" !== 'production' && warn(
'Avoid adding reactive properties to a Vue instance or its root $data ' +
'at runtime - declare it upfront in the data option.'
);
return val
}
if (!ob) {
target[key] = val;
return val
}
defineReactive(ob.value, key, val);
ob.dep.notify();
return val
}