以前寫過一篇響應式原理-如何監聽Array的變化,最近準備給團隊同事分享,發現以前看的太粗糙了,所以決定再寫一篇詳細版~javascript
相信初學Vue
的同窗必定踩過這個坑,改變數組的索引,沒有觸發視圖更新。 好比下面這個案例:html
var vm = new Vue({
data: {
items: ['a', 'b', 'c']
}
})
vm.items[1] = 'x' // 不是響應性的
複製代碼
以上案例摘抄Vue官方文檔 - 數組更新檢測。vue
Vue
官方文檔也有給出,使用Vue.set
便可達到觸發視圖更新的效果。java
// Vue.set
Vue.set(vm.items, indexOfItem, newValue);
複製代碼
Vue
官方給出瞭解釋,不能檢測。git
因爲 JavaScript 的限制,Vue 不能檢測如下數組的變更: 當你利用索引直接設置一個數組項時,例如:
vm.items[indexOfItem] = newValue
。github
那緣由是什麼?我在學習的過程當中發現不少文章都在斷章取義,Vue
官方給出瞭解釋是【Vue
不能檢測】,而不少文章寫出的是【Object.defineProperty
不能檢測】。segmentfault
但實際上Object.defineProperty
是能夠檢測到數組索引的變化的。以下案例:數組
let data = [1, 2];
function defineReactive (obj, key, val) {
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: () => {
console.log('我被讀了,我要不要作點什麼好?');
return val;
},
set: newVal => {
if (val === newVal) {
return;
}
val = newVal;
console.log("數據被改變了,我要渲染到頁面上去!");
}
})
}
defineReactive(data, 0, 1);
console.log(data[0]);
data[0] = 5;
複製代碼
你們能夠本身在控制檯中嘗試一下,答案很是明顯了。瀏覽器
Vue
只是沒有使用這個方式去監聽數組索引的變化,由於尤大認爲性能消耗太大,因而在性能和用戶體驗之間作了取捨。 詳細可見這邊文章Vue爲何不能檢測數組變更。 bash
好了,終於揭開了謎底,爲何Vue
爲何不能檢測數組變更,由於不作哈哈。
可是咱們開發者確定是有這個需求的,解決方式就是以下,使用Vue.set
。
// Vue.set
Vue.set(vm.items, indexOfItem, newValue);
複製代碼
原理?很是明顯,在初始的過程當中沒有循環對全部數組索引監聽,可是開發者須要監聽哪一個索引。Vue.set
就幫你監聽哪一個,核心仍是Object.defineProperty
。只是儘量的避免了無用的數組索引監聽。
Object.defineProperty
雖然能檢測索引的變化,但的確是監聽不到數組的增長或刪除的。能夠閱讀 Vue官方文檔 - 對象變動檢測注意事項 進行了解。
這個時候Vue
是怎麼作的呢?
Vue
的解決方案,就是重寫了數組的原型,更準確的表達是攔截了數組的原型。
首先選擇了7個可以改變數組自身的幾個方法。其次看下案例吧:
// 得到原型上的方法
const arrayProto = Array.prototype;
// 建立一個新對象,使用現有的對象來提供新建立的對象的__proto__
const arrayMethods = Object.create(arrayProto);
// 作一些攔截的操做
Object.defineProperty(arrayMethods, 'push', {
value(...args) {
console.log('用戶傳進來的參數', args);
// 真正的push 保證數據如用戶指望
arrayProto.push.apply(this, args);
},
enumerable: true,
writable: true,
configurable: true,
});
let list = [1];
list.__proto__ = arrayMethods; // 重置原型
list.push(2, 3);
console.log('用戶獲得的list:', list);
複製代碼
爲何叫攔截,咱們在重寫案例中的push
方法時,還須要使用真正的push
,這樣才能保證數組如用戶所指望的push
進去。
能夠看到如下效果,咱們既能監聽到用戶傳進來的參數,也就是監聽到這個數組變化了,還能保證數組如用戶所指望的push
進去。
爲何使用arrayMethods
繼承真正的原型,由於這樣纔不會污染全局的Array.prototype
,由於咱們要監聽的數組只有vm.data
中的。
export class Observer {
constructor (value: any) {
// 若是是數組
if (Array.isArray(value)) {
// 若是原型上有__proto__屬性, 主要是瀏覽器判斷兼容
if (hasProto) {
// 直接覆蓋響應式對象的原型
protoAugment(value, arrayMethods)
} else {
// 直接拷貝到對象的屬性上,由於訪問一個對象的方法時,先找他自身是否有,而後纔去原型上找
copyAugment(value, arrayMethods, arrayKeys)
}
} else {
// 若是是對象
this.walk(value);
}
}
}
複製代碼
以上能夠看到Observer
對數組的特別處理。
咱們知道對象是在getter中收集依賴,setter中派發更新
。 那簡單回憶下:
function defineReactive (obj, key, val) {
// 生成一個Dep實例
let dep = new Dep();
Object.defineProperty(obj, key, {
get: () => {
// 依賴收集
dep.depend();
},
set: () => {
// 派發更新
dep.notify();
},
})
}
複製代碼
爲了保證data
中每一個數據有着一對一的dep
,這裏應用了閉包,保證每一個dep
實例不會被銷燬。那麼問題來了,dep
是一個局部變量呀~ 而監聽數組變化,須要在數組攔截器中進行派發更新。那就訪問不到這個dep
了,就沒法知道具體要通知哪些Watcher
了!
那Vue是怎麼作的呢?既然這個訪問不到,那就再來一個dep
吧。
export class Observer {
constructor (value: any) {
this.value = value // data屬性
this.dep = new Dep() // 掛載dep實例
// 爲數據定義了一個 __ob__ 屬性,這個屬性的值就是當前 Observer 實例對象
def(value, '__ob__', this) // 把當前Observer實例掛在到data的__ob__上
}
}
複製代碼
在Vue
初始化的過程當中,給data
中的每一個數據都掛載了當前的Observer
實例,又在這個實例上掛載了dep
。這樣就能保證咱們在數組攔截器中訪問到dep
了。以下:
Object.defineProperty(arrayMethods, 'push', {
value(...args) {
console.log('用戶傳進來的參數', args);
// 真正的push 保證數據如用戶指望
arrayProto.push.apply(this, args);
// this指向當前這個數組,在初始化的時候被賦值__ob__
console.log(this.__ob__.dep)
},
enumerable: true,
writable: true,
configurable: true,
});
複製代碼
如今咱們即可以在攔截器中執行dep.notify()
啦。
那如何收集依賴呢?
// 獲取當前data上的 observe實例,也就是__ob__
let childOb = !shallow && observe(val);
function defineReactive (obj, key, val) {
// 生成一個Dep實例
let dep = new Dep();
Object.defineProperty(obj, key, {
get: () => {
if (Dep.target) {
// 依賴收集
dep.depend();
// 二次收集
if (childOb.dep) {
// 再收集一次依賴
childOb.dep.depend();
}
}
return val;
},
})
}
複製代碼
如今要存放2個dep
,那天然是要在getter
中收集2次的,childOb
其實就是observe
中返回的__ob__
。不用在乎細節,自行查看源碼就知道啦~
總結一下,針對數組在getter中收集依賴,在攔截器中觸發更新
。
判斷某個數組是否已Observer過,避免重複執行。
Vue.set
和Vue.del
,都是須要訪問dep
的。
由於Object.defineProperty
不能檢測數組的長度變化,例如:vm.items.length = newLength
。
var vm = new Vue({
data: {
items: ['a']
}
})
// 從新賦值,改變長度
vm.items = ['a, 'b', 'c']
複製代碼
那vm.items = ['a, 'b', 'c']
這種狀況,Vue
是如何監聽的?這種狀況其實監聽的是對象vm
的items
屬性,和數組實際上是不要緊的。由於以前發現有人誤解,這裏簡單的提示一下~
本文主要仍是講原理及思路,並不會涉及到不少代碼,畢竟源碼總會變。同時還要保證本身的js基礎紮實,閱讀源碼纔不會吃力哦~ 我就是很吃力的那種😭
若是你以爲對你有幫助,就點個贊吧~
已完成:
Vue源碼解讀系列篇
Github博客 歡迎交流~