在上一篇Vue響應式原理-理解Observer、Dep、Watcher簡單講解了Observer
、Dep
、Watcher
三者的關係。javascript
在Observer
的僞代碼中咱們模擬了以下代碼:java
class Observer {
constructor() {
// 響應式綁定數據經過方法
observe(this.data);
}
}
export function observe (data) {
const keys = Object.keys(data);
for (let i = 0; i < keys.length; i++) {
// 將data中咱們定義的每一個屬性進行響應式綁定
defineReactive(obj, keys[i]);
}
}
export function defineReactive () {
// ...省略 Object.defineProperty get-set
}
複製代碼
今天咱們就進一步瞭解Observer
裏還作了什麼事。git
data
中的數據若是是一個數組怎麼辦?咱們發現Object.defineProperty
對數組進行響應式化是有缺陷的。github
雖然咱們能夠監聽到索引的改變。數組
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("數據被改變了,我要渲染到頁面上去!");
}
})
}
let data = [1];
// 對數組key進行監聽
defineReactive(data, 0, 1);
console.log(data[0]); // 我被讀了,我要不要作點什麼好?
data[0] = 2; // 數據被改變了,我要渲染到頁面上去!
複製代碼
可是defineProperty
不能檢測到數組長度的變化,準確的說是經過改變length而增長的長度不能監測到。這種狀況沒法觸發任何改變。瀏覽器
data.length = 0; // 控制檯沒有任何輸出
複製代碼
並且監聽數組全部索引的的代價也比較高,綜合一些其餘因素,Vue用了另外一個方案來處理。app
首先咱們的observe
須要改造一下,單獨加一個數組的處理。異步
// 將data中咱們定義的每一個屬性進行響應式綁定
export function observe (data) {
const keys = Object.keys(data);
for (let i = 0; i < keys.length; i++) {
// 若是是數組
if (Array.isArray(keys[i])) {
observeArray(keys[i]);
} else {
// 若是是對象
defineReactive(obj, keys[i]);
}
}
}
// 數組的處理
export function observeArray () {
// ...省略
}
複製代碼
那接下來咱們就應該考慮下Array
變化如何監聽?函數
Vue
中對這個數組問題的解決方案很是的簡單粗暴,就是對可以改變數組的方法作了一些手腳。post
咱們知道,改變數組的方法有不少,舉個例子好比說push
方法吧。push
存在Array.prototype
上的,若是咱們能
能攔截到原型上的push
方法,是否是就能夠作一些事情呢?
對象裏目前存在的屬性描述符有兩種主要形式:數據描述符和存取描述符。存取描述符是由getter-setter函數對描述的屬性,也就是咱們用來給對象作響應式綁定的。Object.defineProperty-MDN
雖然咱們沒法使用Object.defineProperty
將數組進行響應式的處理,也就是getter-setter
,可是還有其餘的功能能夠供咱們使用。就是數據描述符,數據描述符是一個具備值的屬性,該值多是可寫的,也可能不是可寫的。
該屬性對應的值。能夠是任何有效的 JavaScript 值(數值,對象,函數等)。默認爲 undefined。
當且僅當該屬性的
writable
爲true
時,value
才能被賦值運算符改變。默認爲 false。
所以咱們只要把原型上的方法,進行value
的從新賦值。
以下代碼,在從新賦值的過程當中,咱們能夠獲取到方法名和全部參數。
function def (obj, key) {
Object.defineProperty(obj, key, {
writable: true,
enumerable: true,
configurable: true,
value: function(...args) {
console.log('key', key);
console.log('args', args);
}
});
}
// 重寫的數組方法
let obj = {
push() {}
}
// 數組方法的綁定
def(obj, 'push');
obj.push([1, 2], 7, 'hello!');
// 控制檯輸出 key push
// 控制檯輸出 args [Array(2), 7, "hello!"]
複製代碼
經過如上代碼咱們就能夠知道,用戶使用了數組上原型的方法以及參數咱們均可以攔截到,這個攔截的過程就能夠作一些變化的通知。
接下來,就看看Vue
是如何實現的吧~
第一步:先獲取原生 Array
的原型方法,由於攔截後仍是須要原生的方法幫咱們實現數組的變化。
第二步:對 Array
的原型方法使用 Object.defineProperty
作一些攔截操做。
第三步:把須要被攔截的 Array
類型的數據原型指向改造後原型。
咱們將代碼進行下改造,攔截的過程當中仍是要將開發者的參數傳給原生的方法,保證數組按照開發者的想法被改變,而後咱們再去作視圖的更新等操做。
const arrayProto = Array.prototype // 獲取Array的原型
function def (obj, key) {
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
value: function(...args) {
console.log(key); // 控制檯輸出 push
console.log(args); // 控制檯輸出 [Array(2), 7, "hello!"]
// 獲取原生的方法
let original = arrayProto[key];
// 將開發者的參數傳給原生的方法,保證數組按照開發者的想法被改變
const result = original.apply(this, args);
// do something 好比通知Vue視圖進行更新
console.log('個人數據被改變了,視圖該更新啦');
this.text = 'hello Vue';
return result;
}
});
}
// 新的原型
let obj = {
push() {}
}
// 重寫賦值
def(obj, 'push');
let arr = [0];
// 原型的指向重寫
arr.__proto__ = obj;
// 執行push
arr.push([1, 2], 7, 'hello!');
console.log(arr);
複製代碼
被改變後的arr
。
Vue
在array.js
中重寫了methodsToPatch
中七個方法,並將重寫後的原型暴露出去。
// Object.defineProperty的封裝
import { def } from '../util/index'
// 得到原型上的方法
const arrayProto = Array.prototype
// Vue攔截的方法
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
];
// 將上面的方法重寫
methodsToPatch.forEach(function (method) {
def(arrayMethods, method, function mutator (...args) {
console.log('method', method); // 獲取方法
console.log('args', args); // 獲取參數
// ...功能如上述,監聽到某個方法執行後,作一些對應的操做
// 一、將開發者的參數傳給原生的方法,保證數組按照開發者的想法被改變
// 二、視圖更新等
})
})
export const arrayMethods = Object.create(arrayProto);
複製代碼
在進行數據observer
綁定的時候,咱們先判斷是否hasProto
,若是存在__proto__
,就直接將value
的 __proto__
指向重寫事後的原型。若是不能使用 __proto__
,貌似有些瀏覽器廠商沒有實現。那就直接循環 arrayMethods
把它身上的這些方法直接裝到 value
身上好了。畢竟調用某個方法是先去自身查找,當自身找不到這關方法的時候,纔去原型上查找。
// 判斷是否有__proto__,由於部分瀏覽器是沒有__proto__
const hasProto = '__proto__' in {}
// 重寫後的原型
import { arrayMethods } from './array'
// 方法名
const arrayKeys = Object.getOwnPropertyNames(arrayMethods);
// 數組的處理
export function observeArray (value) {
// 若是有__proto__,直接覆蓋
if (hasProto) {
protoAugment(value, arrayMethods);
} else {
// 沒有__proto__就把方法加到屬性自身上
copyAugment(value, arrayMethods, )
}
}
// 原型的賦值
function protoAugment (target, src) {
target.__proto__ = src;
}
// 複製
function copyAugment (target, src, keys) {
for (let i = 0, l = keys.length; i < l; i++) {
const key = keys[i]
def(target, key, src[key]);
}
}
複製代碼
經過上面的代碼咱們發現,沒有直接修改 Array.prototype
,而是直接把 arrayMenthods
賦值給 value
的 __proto__
。由於這樣不會污染全局的Array, arrayMenthods
只對 data
中的Array
生效。
由於監聽的數組帶來的代價和一些問題,Vue
使用了重寫原型的方案代替。攔截了數組的一些方法,在這個過程當中再去作通知變化等操做。
本文的一些代碼均是Vue
源碼簡化後的,爲了方便你們理解。思想理解了,源碼就容易看懂了。
Vue源碼解讀系列篇
Github博客 歡迎交流~