定義: 數據劫持,指的是在訪問或者修改對象的某個屬性時,經過一段代碼攔截這個行爲,進行額外的操做或者修改返回結果。react
簡單地說,就是當咱們 觸發函數的時候 動一些手腳作點咱們本身想作的事情,也就是所謂的 "劫持"操做git
Object.defineProperty(obj,prop,descriptor)
數組
參數:緩存
可供定義的特性列表:bash
在Vue中其實就是經過Object.defineProperty
來劫持對象屬性的setter
和getter
操做,並「種下」一個監聽器,當數據發生變化的時候發出通知,以下:app
var data = {name:'test'}
Object.keys(data).forEach(function(key){
Object.defineProperty(data,key,{
enumerable:true,
configurable:true,
get:function(){
console.log('get');
},
set:function(){
console.log('監聽到數據發生了變化');
}
})
});
data.name //控制檯會打印出 「get」
data.name = 'hxx' //控制檯會打印出 "監聽到數據發生了變化"
複製代碼
上面的這個例子能夠看出,咱們徹底能夠控制對象屬性的設置和讀取。在Vue中,在不少地方都很是巧妙的運用了Object.defineProperty
這個方法,具體用在哪裏而且它又解決了哪些問題,下面就簡單的說一下:dom
它經過observe每一個對象的屬性,添加到訂閱器dep中,當數據發生變化的時候發出一個notice。 相關源代碼以下:(做者採用的是ES6+flow寫的,代碼在src/core/observer/index.js模塊裏面)函數
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: Function
) {
const dep = new Dep()//建立訂閱對象
const property = Object.getOwnPropertyDe述 //屬性的描述特性裏面若是configurable爲false則屬性的任何修改將無效
if (property && property.configurable === false) { return }scriptor(obj, key)//獲取obj對象的key屬性的描
// cater for pre-defined getter/setters
const getter = property && property.get
const setter = property && property.set
let childOb = observe(val)//建立一個觀察者對象
Object.defineProperty(obj, key, {
enumerable: true,//可枚舉
configurable: true,//可修改
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val//先調用默認的get方法取值 //這裏就劫持了get方法,也是做者一個巧妙設計,在建立watcher實例的時候,經過調用對象的get方法往訂閱器dep上添加這個建立的watcher實例 if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
}
if (Array.isArray(value)) {
dependArray(value)
}
}
return value//返回屬性值
},
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val//先取舊值
if (newVal === value) { return }
//這個是用來判斷生產環境的,能夠無視
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = observe(newVal)//繼續監聽新的屬性值
dep.notify()//這個是真正劫持的目的,要對訂閱者發通知了
}
})
}
複製代碼
以上是Vue監聽對象屬性的變化,那麼問題來了,咱們常常在傳遞數據的時候每每不是一個對象,頗有多是一個數組,那是否是就沒有辦法了呢,答案顯然是不然的。那麼下面就看看做者是如何監聽數組的變化:性能
看代碼:測試
const arrayProto = Array.prototype//原生Array的原型
export const arrayMethods = Object.create(arrayProto);
[
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse']
.forEach(function (method) {
const original = arrayProto[method]//緩存元素數組原型 //這裏重寫了數組的幾個原型方法
def(arrayMethods, method, function mutator () {
//這裏備份一份參數應該是從性能方面的考慮
let i = arguments.length
const args = new Array(i)
while (i--) {
args[i] = arguments[i]
}
const result = original.apply(this, args)//原始方法求值 const ob = this.__ob__//這裏this.__ob__指向的是數據的Observer
let inserted
switch (method) {
case 'push':
inserted = args
break
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted)
// notify change
ob.dep.notify()
return result
})
})
...//定義屬性
function def (obj, key, val, enumerable) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
});
}
複製代碼
上面的代碼主要是繼承了Array自己的原型方法,而後又作了劫持修改,能夠發出通知。Vue在observer數據階段會判斷若是是數組的話,則修改數組的原型,這樣的話,後面對數組的任何操做均可以在劫持的過程當中控制。結合Vue的思想,簡單的寫個小demo方便更好的理解:
let arrayMethod = Object.create(Array.prototype);
['push','shift'].forEach(function(method){
Object.defineProperty(arrayMethod,method,{
value:function(){
let i = arguments.length
let args = new Array(i)
while (i--) {
args[i] = arguments[i]
}
let original = Array.prototype[method];
let result = original.apply(this,args);
console.log("已經控制了,哈哈");
return result;
},
enumerable: true,
writable: true,
configurable: true
})
})
let bar = [1,2];
bar.__proto__ = arrayMethod;
bar.push(3);//控制檯會打印出 「已經控制了,哈哈」;而且bar裏面已經成功的添加了成員 ‘3’
複製代碼
整個過程看起來好像沒有什麼問題,彷佛Vue已經作到了完美,其實否則,Vue仍是不能檢測到數據項和數組長度改變的變化,例以下面的調用:
vm.items[index] = "xxx";
vm.items.length = 100;
複製代碼
因此咱們儘可能避免這樣的調用方式,若是確實須要,做者也幫咱們實現了一個$set
操做,下去本身瞭解
正常狀況下咱們是這樣實例化一個Vue對象:
var VM = new Vue({ data:{ name:'lhl' }, el:'#id'})
按理說咱們操做數據的時候應該是VM.data.name = ‘hxx’
纔對,可是做者以爲這樣不夠簡潔,因此又經過代理的方式實現了VM.name = ‘hxx’
的可能。 相關代碼以下:
function proxy (vm, key) {
if (!isReserved(key)) {
Object.defineProperty(vm, key, {
configurable: true,
enumerable: true,
get: function proxyGetter () {
return vm._data[key]
},
set: function proxySetter (val) {
vm._data[key] = val;
}
});
}
}
複製代碼
表面上看起來咱們是在操做VM.name
,實際上仍是經過Object.defineProperty()
中的get
和set
方法劫持實現的。
Object.defineProperty()
的缺點let arr = [1,2,3]
let obj = {}
Object.defineProperty(obj, 'arr', {
get () {
console.log('get arr')
return arr
},
set (newVal) {
console.log('set', newVal)
arr = newVal
}
})
obj.arr.push(4) // 只會打印 get arr, 不會打印 set
obj.arr = [1,2,3,4] // 這個能正常 set
複製代碼
數組的如下幾個方法不會觸發 set: push
、pop
、shift
、unshift
、splice
、sort
、reverse
Vue 把這些方法定義爲變異方法 (mutation method),指的是會修改原來數組的方法。與之對應則是非變異方法 (non-mutating method),例如 filter
, concat
, slice
等,它們都不會修改原始數組,而會返回一個新的數組。
使用 Object.defineProperty()
多數要配合 Object.keys()
和遍歷,因而多了一層嵌套。如:
Object.keys(obj).forEach(key => {
Object.defineProperty(obj, key, {
// ...
})
})
複製代碼
所謂的嵌套對象,是指相似
let obj = {
info: {
name: 'eason'
}
}
複製代碼
若是是這一類嵌套對象,那就必須逐層遍歷,直到把每一個對象的每一個屬性都調用 Object.defineProperty()
爲止。
給出完整版的數據劫持代碼:
const arrayProto = Array.prototype;// 獲得原型上的方法
const proto = Object.create(arrayProto) // 複製一份原型上的方法
;['push', 'shift', 'pop', 'splice'].forEach(method => {
// console.log(method)
// 重寫'push','shift','pop','splice',固然也能夠多加幾個方法,想加什麼就加什麼
proto[method] = function (...args) {
// console.log(this) // [ 1, 2, 3, { age: [Getter/Setter] } ]
updateView();
arrayProto[method].call(this, ...args)
}
})
function updateView() {
console.log("更新視圖成功了...")
}
function observer(obj) {
if (typeof obj !== "object" || obj == null) {
return obj
}
if (Array.isArray(obj)) {
// 若是是一個數組要重寫數組上原型上的方法
Object.setPrototypeOf(obj, proto)
for (let i = 0; i < obj.length; i++) {
let item = obj[i];
observer(item)
}
} else {
for (let key in obj) {
definedReactive(obj, key, obj[key])
}
}
}
function definedReactive(obj, key, value) {
observer(value)
Object.defineProperty(obj, key, {
get() {
console.log("獲取數據成功了...")
return value;
},
set(newValue) {
if (value !== newValue) {
observer(newValue)
value = newValue;
updateView();
}
}
})
}
let data = { name: [1, 2, 3, { age: 888 }] }
observer(data)
// 數據改變了
// data.name[3].age = 666;
// push shift unshift pop 也能改變數組中的數組
data.name.push({ address: "xxx" }) // 目的是:更新視圖
// 思路:重寫Push方法 這些方法在Array的原型上
// 不要把Array原型上的方法直接重寫了
// 先把原型上的方法copy一份,去重寫(加上視圖更新的操做)
// 再去調用最原始的push方法
複製代碼
接下來講一下Object.defineProperty()
的升級版 Proxy
在數據劫持這個問題上,Proxy
能夠被認爲是 Object.defineProperty()
的升級版。外界對某個對象的訪問,都必須通過這層攔截。所以它是針對 整個對象,而不是 對象的某個屬性。
proxy即代理的意思。我的理解,創建一個proxy
代理對象(Proxy的實例),接受你要監聽的對象和監聽它的handle
兩個參數。當你要監聽的對象發生任何改變,都會被proxy
代理攔截來知足需求。
var arr = [1,2,3]
var handle = {
//target目標對象 key屬性名 receiver實際接受的對象
get(target,key,receiver) {
console.log(`get ${key}`)
// Reflect至關於映射到目標對象上
return Reflect.get(target,key,receiver)
},
set(target,key,value,receiver) {
console.log(`set ${key}`)
return Reflect.set(target,key,value,receiver)
}
}
//arr要攔截的對象,handle定義攔截行爲
var proxy = new Proxy(arr,handle)
proxy.push(4) //能夠翻到控制檯測試一下會打印出什麼
複製代碼
優勢:
1.使用proxy
能夠解決defineProperty
不能監聽數組的問題,避免重寫數組方法;
2.不須要再遍歷key
。
3.Proxy handle
的攔截處理器除了get
、set
外還支持多種攔截方式。
4.嵌套查詢。實際上proxy get()
也是不支持嵌套查詢的。解決方法:
let handler = {
get (target, key, receiver) {
// 遞歸建立並返回
if (typeof target[key] === 'object' && target[key] !== null) {
return new Proxy(target[key], handler)
}
return Reflect.get(target, key, receiver)
}
}
複製代碼
說完了上面的,簡單說一下 依賴管理方案
Object.defineProperty
只是解決了狀態變動後,如何觸發通知的問題,那要通知誰呢?誰會關心那些屬性發生了變化呢?在 Vue 中,使用 Dep
解耦了依賴者與被依賴者之間關係的肯定過程。簡單來講:
Observer
提供的接口,遍歷狀態對象,給對象的每一個屬性、子屬性都綁定了一個專用的 Dep
對象。這裏的狀態對象主要指組件當中的data
屬性。watcher
:initComputed
將 computed
屬性轉化爲 watcher
實例initWatch
方法,將watch
配置轉化爲 watcher
實例mountComponent
方法,爲 render
函數綁定 watcher
實例dep.notify()
函數,該函數再進一步觸發 Watcher
對象 update
函數,執行watcher
的從新計算。對應下圖:
注意,Vue 組件中的 render
函數,咱們能夠單純將其視爲一種特殊的 computed
函數,在它所對應的 Watcher
對象發生變化時,觸發執行render
,生成新的 virutal-dom
結構,再交由 Vue 作diff
,更新視圖。
OK 本章就到此了!