工做中咱們經過搜索引擎或者官方文檔很容易就會知道一個語法怎麼使用,可是你知道其中的原理嗎?我想有一部分同窗應該作不到清楚的說明其實現原理。衆所周知,現在技術更新迭代速度很快,據 Vue 做者尤雨溪表示 Vue3.x 會在今年的下半年發佈正式版本,視頻地址在這裏 VUE CONF 杭州之 3.0 進展 。若是你在使用 Vue 或者正在學習 Vue,那麼我建議你觀看一遍完整的視頻,這可能對你之後接觸 3.0 版本有很大幫助。這篇文章是基於 Vue2.x 的版本講述的。主要是圍繞如下三個問題展開討論。javascript
- 爲何
data
要寫成函數,而不容許寫成對象?- Vue 中常說的數據劫持究竟是什麼?
- Vue 實例中數組改變
length
或下標賦值爲何不能更新視圖?
若是你已經掌握了這三個問題的緣由和原理;html
若是你以爲你不須要掌握原理會用便可;vue
抖個機靈——請看本文最後一行。java
接下來,咱們就對這三個問題一一解答。react
爲何
data
要寫成函數,而不容許寫成對象?
想要理解這個問題,咱們首先要知道如下三點。面試
data
是 Vue 實例上的一個屬性。2. 對象是對於內存地址的引用。3. 函數有本身的做用域空間。
第一點無可厚非,data
屬性附着於 Vue 實例上。算法
第二點,JS 的數據類型分爲基本類型和引用類型,基本類型存儲在棧內存中,引用類型存儲在堆內存中,而且引用類型指向的是棧內存中的堆區地址。下面兩個例子能夠幫助你清晰地理解這句話。windows
var a = 10;
var b = 10;
var c = a;
console.log(a === b); // true
a ++ ;
console.log(a); // 11
console.log(c); // 10
複製代碼
這段代碼分別給 a、b 賦值 10,a 和 b 是全等的。而後用 a 來初始化 c,那麼 c 的值也是 10。但 c 中的 10 與 a 中的是徹底獨立的,該值只是 a 中的值的一個副本,此後, 這兩個變量能夠參加任何操做而相互不受影響。具體位置以下示意圖。數組
var a = {};
var b = {};
var c = a;
console.log(a === b); // false
a.name = 'Marry';
a.say = () => console.log('Hi Marry!');
console.log(c.name); // 'Marry'
console.log(c.say()); // 'Hi Marry!'
複製代碼
上面這段代碼。首先聲明瞭a、b兩個空對象,而後把 a 賦值給 c。由於對象是對棧內存的地址的引用,因此不一樣的對象的地址是不一樣的,因此他們不是全等的。接着給 a 新增長屬性和方法,c 一樣能夠擁有此屬性和方法,主要是由於 c 和 a 指向堆內存中的同一個地址。其關係圖以下示意圖所示。app
至於第三點,大多數有 JS 基礎的同窗應該都能理解,每一個函數都有本身的做用域。
以上是對三個注意點的說明,那麼接下來咱們就以兩個例子解釋問題一:爲何 data
要寫成函數,而不容許寫成對象?
function MyCompnent() {}
MyCompnent.prototype.data = {
age: 12
};
var JackMa = new MyCompnent();
var PonyMa = new MyCompnent();
console.log(JackMa.data.age === PonyMa.data.age); // true
JackMa.data.age = 13;
console.log('JackMa ' + JackMa.data.age + '歲;' + 'PonyMa ' + PonyMa.data.age + '歲');
// JackMa 13歲;PonyMa 13歲
複製代碼
上面的示例中,咱們建立一個構造函數 MyCompnent,它充當的角色至關於 Vue,在他的原型屬性上聲明一個data
屬性,其實也至關於 Vue.$data
。接着聲明兩個引用,改變一個引用的值,另一個引用也跟着改變,這個道理其實和引用類型賦值大同小異。
function MyCompnent() {
this.data = this.data();
}
MyCompnent.prototype.data = function() {
return {
age: 12
}
};
var JackMa = new MyCompnent();
var PonyMa = new MyCompnent();
console.log(JackMa.data.age === PonyMa.data.age); // true
JackMa.data = {age: 13};
console.log('JackMa ' + JackMa.data.age + '歲;' + 'PonyMa ' + PonyMa.data.age + '歲');
// JackMa 13歲;PonyMa 12歲
複製代碼
上述代碼模擬了 Vue 實例上的data
爲函數的時候,若是改變一個實例的data
屬性的值,那麼不會影響到另一個實例上的data
的值。
面試過程當中常常會被問到 JS 數據類型問題,若是你只回答基本類型和引用類型可能你只能獲得一半分數,可是若是你能把存儲位置等要點回答出來而且舉例說明,想必是很加分的。
Vue 裏面data
屬性之因此不能寫成對象的格式,是由於對象是對地址的引用,而不是獨立存在的。若是一個.vue 文件有多個子組件共同接收一個變量的話,改變其中一個子組件內此變量的值,會影響其餘組件的這個變量的值。若是寫成函數的話,那麼他們有一個做用域的概念在裏面,相互隔閡,不受影響。
Vue 中常說的數據劫持究竟是什麼?
相信大多數用過或者瞭解 Vue 的同窗都聽過數據劫持,進一步問爲何可能你也能答出一二,例如 getter、setter 之類。今天我就係統地和你說一下數據劫持之美。首先咱們先看一看下圖。
上圖完整的描述了 Vue 運行的機制,首先數據發生改變,就會通過 Data
處理,而後Dep
會發出通知(notify
),告訴 Watcher
有數據發生了變化,接着 Watcher
會傳達給渲染函數跟他說有數據變化了,能夠渲染視圖了(數據驅動視圖),進而渲染函數執行render
方法去更新 VNODE
,也就是咱們說的虛擬DOM,最後虛擬DOM根據最優算法,去局部更新須要渲染的視圖。這裏的 Data
就作了咱們今天要說的事——數據劫持。
想要更深刻地理解如何劫持,咱們就須要看源碼實現。
/** * Vue中的每個變量都是由 Observer 構造函數生成的。 * 細心的你可能會發現,你打印出來任何一個Vue上的引用類型屬性,後面都有 __ob__: Observer 的字樣。 */
var Observer = function Observer (value) {
this.value = value;
// 這裏把發佈者 Dep 註冊了
this.dep = new Dep();
// ···
// 此處調用 walk
this.walk(value);
};
複製代碼
/** * 此處會將 obj 裏面的每個值用 defineReactive$$1 處理,而它就是今晚的主角。 */
Observer.prototype.walk = function walk (obj) {
var keys = Object.keys(obj);
for (var i = 0; i < keys.length; i++) {
defineReactive$$1(obj, keys[i]);
}
};
複製代碼
/** * 這個函數就是數據劫持的根據地,裏面爲對象重寫了 get 和 set 方法以及固有屬性 enumerable 等。 * */
function defineReactive$$1 ( obj, key, val, customSetter, shallow ) {
// ···
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
// ···
return value
},
set: function reactiveSetter (newVal) {
// ···
// 此處最爲關鍵,這個函數的主要做用就是經過 notify 告訴 Watcher 有數據變化了。
dep.notify();
}
});
}
複製代碼
/** * subs 是全部 Watcher 的收集器,類型爲數組;notify 實則是調用了每一個Watcher的 update方法 。 */
Dep.prototype.notify = function notify () {
var subs = this.subs.slice();
// ···
for (var i = 0, l = subs.length; i < l; i++) {
subs[i].update();
}
};
複製代碼
/** * 更新視圖的最直觀的方法就是 Watcher 上的 update 方法 , Dep subs 反覆調用 * 這裏最終都是調用 run 方法。 */
Watcher.prototype.update = function update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true;
} else if (this.sync) {
this.run();
} else {
queueWatcher(this);
}
};
複製代碼
/** * run 方法內的 cb 方法創建 Watcher 和 VNode 之間的關聯關係,從而引起視圖的更新。 */
Watcher.prototype.run = function run () {
// ···
if (this.user) {
// ···
this.cb.call(this.vm, value, oldValue);
// ···
} else {
this.cb.call(this.vm, value, oldValue);
}
// ···
};
複製代碼
至此,咱們不但瞭解了數據劫持的的原理,還知道了誰去劫持,劫持事後作了什麼,是誰引起的視圖更新等等。是否是對 Vue 的運行機制更明白了一些呢?
Vue 實例中數組改變
length
或下標賦值爲何不能更新視圖?
上圖示例中,爲方便調試在 mounted 週期內執行windows.vm = this;
。week
包含週一到週五五個元素,咱們嘗試改變 week
的 length
爲 3 以及給它下標爲 4 的元素賦值一個周八
,結果都沒有生效。那怎麼能夠生效呢?請看下圖。
push
,若是你願意嘗試,你會發現調用數組的
slice
方法是不行的。只要是由於 Vue 提取了數組的能夠改變原數組的原生方法,進行了再加工。只有通過 Vue 處理過的方法纔有更新視圖的能力。下面我將從內置方法和源碼的角度給你們說明這個結論。
__proto__
裏面內置了
pop
、
push
等多個數組的方法;在第二個
__proto__
不但有上面幾種還有更多其餘的方法。因而可知,Vue 是對數組的 Api 進行了劫持。
var methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
];
/** * Intercept mutating methods and emit events * 此方法主要做用就是遍歷數組局部方法,調用的同時去調用 dep 的 notify 通知 Watcher 進而更新視圖 */
methodsToPatch.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); }
// 這一步相當重要
ob.dep.notify();
return result
});
});
複製代碼
到這裏咱們知道了,Vue 劫持了數組能夠改變原數組的 Api,使得每次調用都會執行 dep.notify()
方法進而去更新視圖。
分享的時光永遠這麼短暫,但我還要對你說:工做中常常遇到此類問題,但願咱們多問本身一個爲何?去研究它究竟是怎麼實現的,掌握設計理念,學習設計思想,而不是僅限於知道如何使用。這樣本身才會有更多的成長!
最後,若是你願意點個贊,這將給我很大的動力!