簡單回顧一下這個系列的前兩節,前兩節花了大量的篇幅介紹了
Vue
的選項合併,選項合併是Vue
實例初始化的開始,Vue
爲開發者提供了豐富的選項配置,而每一個選項都嚴格規定了合併的策略。然而這只是初始化中的第一步,這一節咱們將對另外一個重點的概念深刻的分析,他就是數據代理,咱們知道Vue
大量利用了代理的思想,而除了響應式系統外,還有哪些場景也須要進行數據代理呢?這是咱們這節分析的重點。javascript
數據代理的另外一個說法是數據劫持,當咱們在訪問或者修改對象的某個屬性時,數據劫持能夠攔截這個行爲並進行額外的操做或者修改返回的結果。而咱們知道Vue
響應式系統的核心就是數據代理,代理使得數據在訪問時進行依賴收集,在修改更新時對依賴進行更新,這是響應式系統的核心思路。而這一切離不開Vue
對數據作了攔截代理。然而響應式並非本節討論的重點,這一節咱們將看看數據代理在其餘場景下的應用。在分析以前,咱們須要掌握兩種實現數據代理的方法: Object.defineProperty
和 Proxy
。html
官方定義:
Object.defineProperty()
方法會直接在一個對象上定義一個新屬性,或者修改一個對象的現有屬性, 並返回這個對象。vue
基本用法:java
Object.defineProperty(obj, prop, descriptor)
複製代碼
Object.defineProperty()
能夠用來精確添加或修改對象的屬性,只須要在descriptor
對象中將屬性特性描述清楚,descriptor
的屬性描述符有兩種形式,一種是數據描述符,另外一種是存取描述符,咱們分別看看各自的特色。node
configurable
:數據是否可刪除,可配置enumerable
:屬性是否可枚舉value
:屬性值,默認爲undefined
writable
:屬性是否可讀寫configurable
:數據是否可刪除,可配置enumerable
:屬性是否可枚舉get
:一個給屬性提供 getter
的方法,若是沒有 getter
則爲 undefined
。set
:一個給屬性提供 setter
的方法,若是沒有 setter
則爲 undefined
。須要注意的是: 數據描述符的value,writable
和 存取描述符中的get, set
屬性不能同時存在,不然會拋出異常。 有了Object.defineProperty
方法,咱們能夠方便的利用存取描述符中的getter/setter
來進行數據的監聽,這也是響應式構建的雛形。getter
方法可讓咱們在訪問數據時作額外的操做處理,setter
方法使得咱們能夠在數據更新時修改返回的結果。看看下面的例子,因爲設置了數據代理,當咱們訪問對象o
的a
屬性時,會觸發getter
執行鉤子函數,當修改a
屬性的值時,會觸發setter
鉤子函數去修改返回的結果。react
var o = {}
var value;
Object.defineProperty(o, 'a', {
get() {
console.log('獲取值')
return value
},
set(v) {
console.log('設置值')
value = qqq
}
})
o.a = 'sss'
// 設置值
console.log(o.a)
// 獲取值
// 'qqq'
複製代碼
前面說到Object.defineProperty
的get
和set
方法是對對象進行監測並響應變化,那麼數組類型是否也能夠監測呢,參照監聽屬性的思路,咱們用數組的下標做爲屬性,數組的元素做爲攔截對象,看看Object.defineProperty
是否能夠對數組的數據進行監控攔截。webpack
var arr = [1,2,3];
arr.forEach((item, index) => {
Object.defineProperty(arr, index, {
get() {
console.log('數組被getter攔截')
return item
},
set(value) {
console.log('數組被setter攔截')
return item = value
}
})
})
arr[1] = 4;
console.log(arr)
// 結果
數組被setter攔截
數組被getter攔截
4
複製代碼
顯然,**已知長度的數組是能夠經過索引屬性來設置屬性的訪問器屬性的。**可是數組的添加確沒法進行攔截,這個也很好理解,不論是經過arr.push()
仍是arr[10] = 10
添加的數據,數組所添加的索引值並無預先加入數據攔截中,因此天然沒法進行攔截處理。這個也是使用Object.defineProperty
進行數據代理的弊端。爲了解決這個問題,Vue
在響應式系統中對數組的方法進行了重寫,間接的解決了這個問題,詳細細節能夠參考後續的響應式系統分析。es6
另外若是須要攔截的對象屬性嵌套多層,若是沒有遞歸去調用Object.defineProperty
進行攔截,深層次的數據也依然沒法監測。web
爲了解決像數組這類沒法進行數據攔截,以及深層次的嵌套問題,es6
引入了Proxy
的概念,它是真正在語言層面對數據攔截的定義。和Object.defineProperty
同樣,Proxy
能夠修改某些操做的默認行爲,可是不一樣的是,Proxy
針對目標對象會建立一個新的實例對象,並將目標對象代理到新的實例對象上,。 本質的區別是後者會建立一個新的對象對原對象作代理,外界對原對象的訪問,都必須先經過這層代理進行攔截處理。而攔截的結果是咱們只要經過操做新的實例對象就能間接的操做真正的目標對象了。針對Proxy
,下面是基礎的寫法:算法
var obj = {}
var nobj = new Proxy(obj, {
get(target, key, receiver) {
console.log('獲取值')
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
console.log('設置值')
return Reflect.set(target, key, value, receiver)
}
})
nobj.a = '代理'
console.log(obj)
// 結果
設置值
{a: "代理"}
複製代碼
上面的get,set
是Proxy
支持的攔截方法,而Proxy
支持的攔截操做有13種之多,具體能夠參照ES6-Proxy文檔,前面提到,Object.defineProperty
的getter
和setter
方法並不適合監聽攔截數組的變化,那麼新引入的Proxy
又可否作到呢?咱們看下面的例子。
var arr = [1, 2, 3]
let obj = new Proxy(arr, {
get: function (target, key, receiver) {
// console.log("獲取數組元素" + key);
return Reflect.get(target, key, receiver);
},
set: function (target, key, receiver) {
console.log('設置數組');
return Reflect.set(target, key, receiver);
}
})
// 1. 改變已存在索引的數據
obj[2] = 3
// result: 設置數組
// 2. push,unshift添加數據
obj.push(4)
// result: 設置數組 * 2 (索引和length屬性都會觸發setter)
// // 3. 直接經過索引添加數組
obj[5] = 5
// result: 設置數組 * 2
// // 4. 刪除數組元素
obj.splice(1, 1)
複製代碼
顯然Proxy
完美的解決了數組的監聽檢測問題,針對數組添加數據,刪除數據的不一樣方法,代理都能很好的攔截處理。另外Proxy
也很好的解決了深層次嵌套對象的問題,具體讀者能夠自行舉例分析。
數據攔截的思想除了爲構建響應式系統準備,它也能夠爲數據進行篩選過濾,咱們接着往下看初始化的代碼,在合併選項後,vue
接下來會爲vm
實例設置一層代理,這層代理能夠爲vue在模板渲染時進行一層數據篩選,這個過程究竟怎麼發生的,咱們看代碼的實現。
Vue.prototype._init = function(options) {
// 選項合併
...
{
// 對vm實例進行一層代理
initProxy(vm);
}
...
}
複製代碼
initProxy
的實現以下:
// 代理函數
var initProxy = function initProxy (vm) {
if (hasProxy) {
var options = vm.$options;
var handlers = options.render && options.render._withStripped
? getHandler
: hasHandler;
// 代理vm實例到vm屬性_renderProxy
vm._renderProxy = new Proxy(vm, handlers);
} else {
vm._renderProxy = vm;
}
};
複製代碼
首先是判斷瀏覽器是否支持原生的proxy
。
var hasProxy =
typeof Proxy !== 'undefined' && isNative(Proxy);
複製代碼
當瀏覽器支持Proxy
時,vm._renderProxy
會代理vm
實例,而且代理過程也會隨着參數的不一樣呈現不一樣的效果;當瀏覽器不支持Proxy
時,直接將vm
賦值給vm._renderProxy
。
讀到這裏,我相信你們會有不少的疑惑。 1. 這層代理的訪問時機是什麼,也就是說什麼場景會觸發這層代理 2. 參數options.render._withStripped
表明着什麼,getHandler
和hasHandler
又有什麼不一樣。 3. 如何理解爲模板數據的訪問進行數據篩選過濾。到底有什麼數據須要過濾。 4. 只有在支持原生proxy
環境下才會創建這層代理,那麼在舊的瀏覽器,非法的數據又將如何展現。
帶着這些疑惑,咱們接着往下分析。
源碼中vm._renderProxy
的使用出如今Vue
實例的_render
方法中,Vue.prototype._render
是將渲染函數轉換成Virtual DOM
的方法,這部分是關於實例的掛載和模板引擎的解析,筆者並不會在這一章節中深刻分析,咱們只須要先有一個認知,**Vue
內部在js
和真實DOM
節點中設立了一箇中間層,這個中間層就是Virtual DOM
,遵循js -> virtual -> 真實dom
的轉換過程,而Vue.prototype._render
是前半段的轉換,**當咱們調用render
函數時,代理的vm._renderProxy
對象便會訪問到。
Vue.prototype._render = function () {
···
// 調用vm._renderProxy
vnode = render.call(vm._renderProxy, vm.$createElement);
}
複製代碼
那麼代理的處理函數又是什麼?咱們回過頭看看代理選項handlers
的實現。 handers
函數會根據 options.render._withStripped
的不一樣執行不一樣的代理函數,當使用相似webpack
這樣的打包工具時,一般會使用vue-loader
插件進行模板的編譯,這個時候options.render
是存在的,而且_withStripped
的屬性也會設置爲true
(關於編譯版本和運行時版本的區別能夠參考後面章節),因此此時代理的選項是hasHandler
,在其餘場景下,代理的選項是getHandler
。getHandler,hasHandler
的邏輯類似,咱們只分析使用vue-loader
場景下hasHandler
的邏輯。另外的邏輯,讀者能夠自行分析。
var hasHandler = {
// key in obj或者with做用域時,會觸發has的鉤子
has: function has (target, key) {
···
}
};
複製代碼
hasHandler
函數定義了has
的鉤子,前面介紹過,proxy
的鉤子有13個之多,而has
是其中一個,它用來攔截propKey in proxy
的操做,返回一個布爾值。而除了攔截 in
操做符外,has
鉤子一樣能夠用來攔截with
語句下的做用對象。例如:
var obj = {
a: 1
}
var nObj = new Proxy(obj, {
has(target, key) {
console.log(target) // { a: 1 }
console.log(key) // a
return true
}
})
with(nObj) {
a = 2
}
複製代碼
那麼這兩個觸發條件是否跟_render
過程有直接的關係呢?答案是確定的。vnode = render.call(vm._renderProxy, vm.$createElement);
的主體是render
函數,而這個render
函數就是包裝成with
的執行語句,**在執行with
語句的過程當中,該做用域下變量的訪問都會觸發has
鉤子,這也是模板渲染時之全部會觸發代理攔截的緣由。**咱們經過代碼來觀察render
函數的原形。
var vm = new Vue({
el: '#app'
})
console.log(vm.$options.render)
//輸出, 模板渲染使用with語句
ƒ anonymous() {
with(this){return _c('div',{attrs:{"id":"app"}},[_v(_s(message)+_s(_test))])}
}
複製代碼
咱們已經大體知道了Proxy
代理的訪問時機,那麼設置這層代理的做用又在哪裏呢?首先思考一個問題,咱們經過data
選項去設置實例數據,那麼這些數據能夠隨着我的的習慣任意命名嗎?顯然不是的,若是你使用js
的關鍵字(像Object,Array,NaN
)去命名,這是不被容許的。另外一方面,Vue
源碼內部使用了以$,_
做爲開頭的內部變量,因此以$,_
開頭的變量名也是不被容許的,這就構成了數據過濾監測的前提。接下來咱們具體看hasHandler
的細節實現。
var hasHandler = {
has: function has (target, key) {
var has = key in target;
// isAllowed用來判斷模板上出現的變量是否合法。
var isAllowed = allowedGlobals(key) ||
(typeof key === 'string' && key.charAt(0) === '_' && !(key in target.$data));
// _和$開頭的變量不容許出如今定義的數據中,由於他是vue內部保留屬性的開頭。
// 1. warnReservedPrefix: 警告不能以$ _開頭的變量
// 2. warnNonPresent: 警告模板出現的變量在vue實例中未定義
if (!has && !isAllowed) {
if (key in target.$data) { warnReservedPrefix(target, key); }
else { warnNonPresent(target, key); }
}
return has || !isAllowed
}
};
// 模板中容許出現的非vue實例定義的變量
var allowedGlobals = makeMap(
'Infinity,undefined,NaN,isFinite,isNaN,' +
'parseFloat,parseInt,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,' +
'Math,Number,Date,Array,Object,Boolean,String,RegExp,Map,Set,JSON,Intl,' +
'require' // for Webpack/Browserify
);
複製代碼
首先allowedGlobals
定義了javascript
保留的關鍵字,這些關鍵字是不容許做爲用戶變量存在的。(typeof key === 'string' && key.charAt(0) === '_' && !(key in target.$data)
的邏輯對以$,_
開頭,或者是不是data
中未定義的變量作判斷過濾。這裏對未定義變量的場景多解釋幾句,前面說到,代理的對象vm.renderProxy
是在執行_render
函數中訪問的,而在使用了template
模板的狀況下,render
函數是對模板的解析結果,換言之,之因此會觸發數據代理攔截是由於模板中使用了變量,例如<div>{{message}}}</div>
。而若是咱們在模板中使用了未定義的變量,這個過程就被proxy
攔截,並定義爲不合法的變量使用。
咱們能夠看看兩個報錯信息的源代碼(是否是很熟悉):
// 模板使用未定義的變量
var warnNonPresent = function (target, key) {
warn(
"Property or method \"" + key + "\" is not defined on the instance but " +
'referenced during render. Make sure that this property is reactive, ' +
'either in the data option, or for class-based components, by ' +
'initializing the property. ' +
'See: https://vuejs.org/v2/guide/reactivity.html#Declaring-Reactive-Properties.',
target
);
};
// 使用$,_開頭的變量
var warnReservedPrefix = function (target, key) {
warn(
"Property \"" + key + "\" must be accessed with \"$data." + key + "\" because " +
'properties starting with "$" or "_" are not proxied in the Vue instance to ' +
'prevent conflicts with Vue internals' +
'See: https://vuejs.org/v2/api/#data',
target
);
};
複製代碼
分析到這裏,前面的疑惑只剩下最後一個問題。只有在瀏覽器支持proxy
的狀況下,纔會執行initProxy
設置代理,那麼在不支持的狀況下,數據過濾就失效了,此時非法的數據定義還能正常運行嗎?咱們先對比下面兩個結論。
// 模板中使用_開頭的變量,且在data選項中有定義
<div id="app">{{_test}}</div>
new Vue({
el: '#app',
data: {
_test: 'proxy'
}
})
複製代碼
proxy
瀏覽器的結果proxy
瀏覽器的結果顯然,在沒有通過代理的狀況下,使用_
開頭的變量依舊會 報錯,可是它變成了js
語言層面的錯誤,表示該變量沒有被聲明。可是這個報錯沒法在Vue
這一層知道錯誤的詳細信息,而這就是能使用Proxy
的好處。接着咱們會思考,既然已經在data
選項中定義了_test
變量,爲何訪問時仍是找不到變量的定義呢? 原來在初始化數據階段,Vue
已經爲數據進行了一層篩選的代理。具體看initData
對數據的代理,其餘實現細節不在本節討論範圍內。
function initData(vm) {
vm._data = typeof data === 'function' ? getData(data, vm) : data || {}
if (!isReserved(key)) {
// 數據代理,用戶可直接經過vm實例返回data數據
proxy(vm, "_data", key);
}
}
function isReserved (str) {
var c = (str + '').charCodeAt(0);
// 首字符是$, _的字符串
return c === 0x24 || c === 0x5F
}
複製代碼
vm._data
能夠拿到最終data
選項合併的結果,isReserved
會過濾以$,_
開頭的變量,proxy
會爲實例數據的訪問作代理,當咱們訪問this.message
時,實際上訪問的是this._data.message
,而有了isReserved
的篩選,即便this._data._test
存在,咱們依舊沒法在訪問this._test
時拿到_test
變量。這就解釋了爲何會有變量沒有被聲明的語法錯誤,而proxy
的實現,又是基於上述提到的Object.defineProperty
來實現的。
function proxy (target, sourceKey, key) {
sharedPropertyDefinition.get = function proxyGetter () {
// 當訪問this[key]時,會代理訪問this._data[key]的值
return this[sourceKey][key]
};
sharedPropertyDefinition.set = function proxySetter (val) {
this[sourceKey][key] = val;
};
Object.defineProperty(target, key, sharedPropertyDefinition);
}
複製代碼
這一節內容,詳細的介紹了數據代理在Vue
的實現思路和另外一個應用場景,數據代理是一種設計模式,也是一種編程思想,Object.defineProperty
和Proxy
均可以實現數據代理,可是他們各有優劣,前者兼容性較好,可是卻沒法對數組或者嵌套的對象進行代理監測,而Proxy
基本能夠解決全部的問題,可是對兼容性要求很高。Vue
中的響應式系統是以Object.defineProperty
實現的,可是這並不表明沒有Proxy
的應用。initProxy
就是其中的例子,這層代理會在模板渲染時對一些非法或者沒有定義的變量進行篩選判斷,和沒有數據代理相比,非法的數據定義錯誤會提早到應用層捕獲,這也有利於開發者對錯誤的排查。