下面這段代碼很是簡單,編寫過Vue的同窗都能看懂它在幹什麼,可是你能準確的說出這段代碼在第一秒,第二秒,第三秒頁面上分別有什麼變化嗎?javascript
<!DOCTYPE html> <html> <script src="https://unpkg.com/vue/dist/vue.js"></script> <body> <div id="app"> <div>{{ list }}</div> </div> <script> new Vue({ el: '#app', data: { list: [], }, mounted() { setTimeout(()=>{ this.list[0] = 3 }, 1000) setTimeout(()=>{ this.list.length = 5 }, 2000) setTimeout(()=>{ this.$set(this.list, this.list) }, 3000) } }) </script> </body> </html>複製代碼
你們最好能動手拷貝上面的代碼,本地新建HTML文件保存後打開調試查看,我這裏直接說一下結果。當執行這段代碼後,頁面在第一秒和第二秒無變化,直到第三秒時候纔會發生變化,思考一下第一秒和第二秒改變了list的值,爲何Vue的雙向綁定在這裏失效了呢?圍繞這個問題下面開始一步一步看看Vue的數據變化監聽實現機制。
html
這裏由淺入深的去看,先從要監聽普通數據類型看起。前端
監聽普通數據類型,即要監聽的對象屬性的值爲非對象的五種基本類型變化,這裏不直接看源碼,每一步都本身手動的去實現,更加便於理解。vue
<!DOCTYPE html> <html> <div> name: <input id="name" /> </div> </html> <script> // 監聽Model下的name屬性,當name屬性有變化時要引發頁面id=name的響應變化 const model = { name: 'vue', }; // 利用Object.defineProperty建立一個監聽器 function observe(obj) { let val = obj.name; Object.defineProperty(obj, 'name', { get() { return val; }, set(newVal) { // 當有新值設置時,執行setter console.log(`name變化:從${val}到${newVal}`); // 解析到頁面 compile(newVal); val = newVal; } }) } // 解析器,將變化的數據響應到頁面上 function compile(val) { document.querySelector('#name').value = val; } // 調用監聽器,對model開始監聽 observe(model); </script>複製代碼
在控制檯調試過程。複製代碼
上面的代碼在調試的時候,我先查看了model.name初始值後,進行了從新設置,能夠引發setter函數的觸發執行,從而頁面達到響應式效果。java
可是當給name屬性賦值爲對象類型後,再給新對象裏插入key1一個屬性後,接着改變這個key1的值,這時候頁面並不能獲得響應式觸發。node
因此上面的observe的實現中,當name是普通數據類型的時候監聽沒有問題,而要監聽的內容是對象的變化裏的時候,上面的寫法就有問題了。git
下面看看監聽對象類型屬性observe函數要怎麼實現。github
從上面的例子裏,檢測屬性值爲對象時,不能知足監聽需求,接下來進一步改造observe監聽函數,解決思路很簡單,若是是對象,只需再一次將當前對象下的全部普通類型的監聽變化便可,若是該對象下還有對象屬性,繼續監聽就能夠了,若是你對遞歸很熟,立刻就知道該如何解決這個問題。面試
<!DOCTYPE html> <html> <div> name: <input id="name" /> val: <input id="val" /> list: <input id="list" /> </div> </html> <script> // 監聽Model下的name屬性,當name屬性有變化時要引發頁面id=name的響應變化 const model = { name: 'vue', data: { val: 1 }, list: [1] }; // 監聽函數 function observe(obj) { // 遍歷全部屬性,各自監聽 Object.keys(obj).map(key => { // 將object屬性特殊處理 if (typeof obj[key] === 'object') { // 是對象屬性的再次監聽 observe(obj[key]); } else { // 非對象屬性的作監聽 defineReactive(obj, key, obj[key]); } }) } // 利用Object.defineProperty作對象屬性的作監聽 function defineReactive(obj, key, val) { Object.defineProperty(obj, key, { get() { return val; }, set(newVal) { // 當有新值設置時,執行setter console.log(`${key}變化:從${val}到${newVal}`); if (Array.isArray(obj)) { document.querySelector(`#list`).value = newVal; } else { document.querySelector(`#${key}`).value = newVal; } val = newVal; // 新增的屬性再次進行監聽 observe(newVal); } }) } // 監聽model下的全部屬性 observe(model); </script>複製代碼
在控制檯調試過程。複製代碼
在上面的實際操做中,我先改變了屬性name的值,觸發了setter,頁面收到響應,再次改變了model.data這個對象下的val屬性,頁面也獲得響應式變化,這說明咱們在以前是想observe監聽不到對象屬性變化的問題在上面的改造下獲得瞭解決。數組
接下來要注意,在最後我改變了數組屬性list下的第一個下標裏的值爲5,頁面也獲得了監聽結果,可是我改變了第二個下標後,沒有觸發setter,接着特地去改變list的length,或者push都沒有觸發數組的setter,頁面沒有變化響應。
這裏拋出兩個問題:
a、我修改了數組list的第二個下標的值,而且調用length、push改變數組list後頁面也沒有響應到變化,是怎麼回事?
b、回到文章開始示例的那一段Vue代碼裏的實現,我改變了Vue的data下list的下標屬性值,頁面是沒有響應變化的,可是這裏我改了list的內的值從1到5,頁面響應了,這又是怎麼回事?
請帶着a、b兩個問題繼續往下看。
這裏分析一下a問題修改數組下標的值和調用length、push方法改變數組時不觸發監聽器的setter函數的緣由。我以前看到不少文章寫Object.defineProperty不能監聽到數組內的值變化,真的是這樣麼?
請看下面的例子,這裏不綁定頁面,只觀察Object.defineProperty監聽的數組元素,是否能監聽到變化。
從上面代碼裏,首先監聽了model數組裏全部的屬性,而後經過各類數組的方法來修改當前數組,得出如下幾個結論。
一、直接修改數組中已有的元素是能夠被監聽的。
二、數組的操做方法若是是操做已經存在的被監聽的元素也是能夠觸發setter被監聽的。
三、只有push、length、pop一些特殊的方法確實不能觸發setter,這跟方法的內部實現與Object.defineProperty的setter鉤子的觸發實現有關係,是語言層面的緣由。
四、改變超過數組長度的下標的值時,值變化是不能監聽到的。這個其實很好理解,不存在的屬性固然是不能監聽到,由於綁定監聽操做在以前已經執行過了,後添加的元素屬性在綁定當時都尚未存在,固然沒有辦法提早去監聽它了。
因此綜上,Object.defineProperty不能監聽到數組內的值變化的說法是錯誤的,同時也得出了a問題的答案,語言層面不支持用Object.defineProperty監聽不存在的數組元素,而且經過一些能形成數組的方法形成數組改變也不能監聽到。
對於b問題,則須要去看看Vue的源碼裏,爲什麼Object.defineProperty明明能監聽到數組值的變化,而它卻沒有實現呢?
這裏分享一下我看源碼的技巧,若是直接打開github一行一行看看源碼是很懵逼的,我這裏是直接用Vue-cli在本地生成一個Vue項目,而後在安裝的node_modules下的Vue包裏進行斷點查看的,你們能夠嘗試下。
測試代碼很簡單,以下;
import Vue from './node_modules/_vue@2.6.11@vue/dist/vue.runtime.common.dev'
// 實例化Vue,啓動起來後直接
new Vue({
data () {
return {
list: [1, 3]
}
},
})複製代碼
解釋一下這一起的源碼,下面的hasProto的源碼是看是否有原型存在,arrayMethods是被重寫的數組方法,代碼流程是若是有原型,直接修改原型上的push,pop,shift,unshift,splice, sort,reverse七個方法,若是沒有原型的狀況下,走copyAugment去新增這七個屬性後賦值這七個方法,並無監聽。
/**
* Observe a list of Array items.
*/
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
// 監聽數組元素
observe(items[i])
}
}複製代碼
最後就是this.observeArray函數了,它的內部實現很是簡單,它對數組元素進行了監聽,什麼意思呢,就是改變數組裏的元素不能監聽到,可是數組內的值是對象類型的,修改它依舊能獲得監聽響應,如改變list[0].val能夠獲得監聽,可是改變list[0]不能,可是依舊沒有對數組自己的變化進行監聽。
再看看arrayMethods是如何重寫數組的操做方法的。
// 記錄原始Array未重寫以前的API原型方法
const arrayProto = Array.prototype
// 拷貝一份上面的原型出來
const arrayMethods = Object.create(arrayProto)
// 將要重寫的方法
const methodsToPatch = [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ]
/** * Intercept mutating methods and emit events */
methodsToPatch.forEach(function (method) {
def(arrayMethods, method, function mutator (...args) {
// 原有的數組方法調用執行
const result = arrayProto[method].apply(this, args)
const ob = this.__ob__
let 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
})
})複製代碼
從上面的源碼裏能夠完整的看到了Vue2.x中重寫數組方法的思路,重寫以後的數組會在每次在執行數組的原始方法以後手動觸發響應頁面的效果。
看完源碼後,問題a也水落石出了,Vue2.x中並無實現將已存在的數組元素作監聽,而是去監聽形成數組變化的方法,觸發這個方法的同時去調用掛載好的響應頁面方法,達到頁面響應式的效果。
可是也請注意並不是全部的數組方法都從新寫了一遍,只有push,pop,shift,unshift,splice, sort,reverse這七個。至於爲何不用Object.defineProperty去監聽數組中已存在的元素變化。
做者尤雨溪的考慮是由於性能緣由,給每個數組元素綁定上監聽,實際消耗很大,而受益並不大。
issue地址:https://github.com/vuejs/vue/issues/8562。
前一篇說了Vue3.0的監聽採用的是ES6新的構造方法Proxy來代理原對象作變化檢測,(對於Proxy不熟的同窗能夠翻看上一篇內容)而Proxy做爲代理的存在,當異步觸發Model裏的數據變化時,必須通過Proxy這一層,在這一層則能夠監聽數組以及各類數據類型的變化,看看下面的例子。
簡直完美,不管是數組下標賦值引發變化仍是數組方法引發變化,均可以被監聽到,並且既能夠避開監聽數組每一個屬性下形成的性能問題,還能夠解決像pop、push方法,length方法改變數組時監聽不到數組變化的問題。
接下來使用Proxy和Reflect實現Vue3.0下的雙向綁定。
<!DOCTYPE html>
<html>
<div>
name: <input id="name" />
val: <input id="val" />
list: <input id="list" />
</div>
</html>
<script>
let model = {
name: 'vue',
data: {
val: 1,
},
list: [1]
}
function isObj (obj) {
return typeof obj === 'object';
}
// 監控器
function observe(data) {
// 將屬性都作監控
Object.keys(data).map(key => {
if (isObj(data[key])) {
// 對象類型的繼續監聽它的屬性
data[key] = observe(data[key]);
}
})
return defineProxy(data);
}
// 生成Proxy代理
function defineProxy(obj) {
return new Proxy(obj, {
set(obj, key, val) {
console.log(`屬性${key}變化爲${val}`);
compile(obj, key, val);
return Reflect.set(...arguments);
}
})
}
// 解析器,響應頁面變化
function compile(obj, id, val) {
if (Array.isArray(obj)) { // 數組變化
document.querySelector('#list').value = model.list;
} else {
document.querySelector(`#${id}`).value = val;
}
}
model= observe(model);
</script>複製代碼
利用Proxy和Reflect實現以後,不用在考慮數組的操做是否觸發setter,只要操做通過proxy代理層,各類操做都會被被捕獲到,達到頁面響應式的要求。
在Vue2.x中數組變化監聽的問題,其實不是Object.definePropertype方法監聽不到,而是爲了性能和收益比例綜合考慮之下,改變了監聽方式,從本來的直接監聽結果變化這種思路變換到監聽會致使結果變化的方法上,也就上面所提到的對數組的重寫。
而Vue3.0中利用Proxy的方式則完美解決了2.0中出現的問題,因此之後面試中若是遇到Vue中對於數組監聽的處理的時候,必定要分清楚是哪個版本,本文完。