深刻剖析Vue源碼 - 基礎的數據代理檢測

簡單回顧一下這個系列的前兩節,前兩節花了大量的篇幅介紹了Vue的選項合併,選項合併是Vue實例初始化的開始,Vue爲開發者提供了豐富的選項配置,而每一個選項都嚴格規定了合併的策略。然而這只是初始化中的第一步,這一節咱們將對另外一個重點的概念深刻的分析,他就是數據代理,咱們知道Vue大量利用了代理的思想,而除了響應式系統外,還有哪些場景也須要進行數據代理呢?這是咱們這節分析的重點。javascript

2.1 數據代理的含義

數據代理的另外一個說法是數據劫持,當咱們在訪問或者修改對象的某個屬性時,數據劫持能夠攔截這個行爲並進行額外的操做或者修改返回的結果。而咱們知道Vue響應式系統的核心就是數據代理,代理使得數據在訪問時進行依賴收集,在修改更新時對依賴進行更新,這是響應式系統的核心思路。而這一切離不開Vue對數據作了攔截代理。然而響應式並非本節討論的重點,這一節咱們將看看數據代理在其餘場景下的應用。在分析以前,咱們須要掌握兩種實現數據代理的方法: Object.definePropertyProxyhtml

2.1.1 Object.defineProperty

官方定義:Object.defineProperty()方法會直接在一個對象上定義一個新屬性,或者修改一個對象的現有屬性, 並返回這個對象。vue

基本用法:java

Object.defineProperty(obj, prop, descriptor)
複製代碼

Object.defineProperty()能夠用來精確添加或修改對象的屬性,只須要在descriptor對象中將屬性特性描述清楚,descriptor的屬性描述符有兩種形式,一種是數據描述符,另外一種是存取描述符,咱們分別看看各自的特色。node

  1. 數據描述符,它擁有四個屬性配置
  • configurable:數據是否可刪除,可配置
  • enumerable:屬性是否可枚舉
  • value:屬性值,默認爲undefined
  • writable:屬性是否可讀寫
  1. 存取描述符,它一樣擁有四個屬性選項
  • configurable:數據是否可刪除,可配置
  • enumerable:屬性是否可枚舉
  • get:一個給屬性提供 getter 的方法,若是沒有 getter 則爲 undefined
  • set:一個給屬性提供 setter 的方法,若是沒有 setter 則爲 undefined

須要注意的是: 數據描述符的value,writable 和 存取描述符中的get, set屬性不能同時存在,不然會拋出異常。 有了Object.defineProperty方法,咱們能夠方便的利用存取描述符中的getter/setter來進行數據的監聽,這也是響應式構建的雛形。getter方法可讓咱們在訪問數據時作額外的操做處理,setter方法使得咱們能夠在數據更新時修改返回的結果。看看下面的例子,因爲設置了數據代理,當咱們訪問對象oa屬性時,會觸發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.definePropertygetset方法是對對象進行監測並響應變化,那麼數組類型是否也能夠監測呢,參照監聽屬性的思路,咱們用數組的下標做爲屬性,數組的元素做爲攔截對象,看看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

2.1.2 Proxy

爲了解決像數組這類沒法進行數據攔截,以及深層次的嵌套問題,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,setProxy支持的攔截方法,而Proxy 支持的攔截操做有13種之多,具體能夠參照ES6-Proxy文檔,前面提到,Object.definePropertygettersetter方法並不適合監聽攔截數組的變化,那麼新引入的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也很好的解決了深層次嵌套對象的問題,具體讀者能夠自行舉例分析。

2.2 initProxy

數據攔截的思想除了爲構建響應式系統準備,它也能夠爲數據進行篩選過濾,咱們接着往下看初始化的代碼,在合併選項後,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表明着什麼,getHandlerhasHandler又有什麼不一樣。 3. 如何理解爲模板數據的訪問進行數據篩選過濾。到底有什麼數據須要過濾。 4. 只有在支持原生proxy環境下才會創建這層代理,那麼在舊的瀏覽器,非法的數據又將如何展現。

帶着這些疑惑,咱們接着往下分析。

2.2.1 觸發代理

源碼中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,在其餘場景下,代理的選項是getHandlergetHandler,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))])}
}
複製代碼

2.2.2 數據過濾

咱們已經大體知道了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'
    }
})
複製代碼
  1. 支持proxy瀏覽器的結果

  1. 不支持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);
}
複製代碼

2.3 小結

這一節內容,詳細的介紹了數據代理在Vue的實現思路和另外一個應用場景,數據代理是一種設計模式,也是一種編程思想,Object.definePropertyProxy均可以實現數據代理,可是他們各有優劣,前者兼容性較好,可是卻沒法對數組或者嵌套的對象進行代理監測,而Proxy基本能夠解決全部的問題,可是對兼容性要求很高。Vue中的響應式系統是以Object.defineProperty實現的,可是這並不表明沒有Proxy的應用。initProxy就是其中的例子,這層代理會在模板渲染時對一些非法或者沒有定義的變量進行篩選判斷,和沒有數據代理相比,非法的數據定義錯誤會提早到應用層捕獲,這也有利於開發者對錯誤的排查。


相關文章
相關標籤/搜索