深刻剖析Vue源碼 - 數據代理,關聯子父組件

簡單回顧一下這個系列的前兩節,前兩節花了大篇幅講了vue在初始化時進行的選項合併。選項配置是vue實例化的第一步,針對不一樣類型的選項,vue提供的豐富選項配置策略以保證用戶可使用不一樣豐富的配置選項。而在這一節中,咱們會分析選項合併後的又兩步重要的操做: 數據代理和關聯子父組件關係,分別對應的處理過程爲initProxy和initLifecycle。這章節的知識點也爲後續的響應式系統介紹和模板渲染作鋪墊。

2.1 Object.defineProperty和Proxy

在介紹這一章的源碼分析以前,咱們須要掌握一下貫穿整個vue數據代理,監控的技術核心:Object.defineProperty 和 Proxyjavascript

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

咱們能夠用來精確添加或修改對象的屬性,只須要在descriptor中將屬性特性描述清楚,descriptor的屬性描述符有兩種形式,一種是數據描述符,另外一種是存取描述符。前端

數據描述符vue

  • configurable:數據是否可刪除
  • enumerable:屬性是否可枚舉
  • value:屬性值,默認爲undefined
  • writable:屬性是否可讀寫

存取描述符java

  • configurable:數據可改變
  • enumerable:可枚舉
  • get:一個給屬性提供 getter 的方法,若是沒有 getter 則爲 undefined。
  • set:一個給屬性提供 setter 的方法,若是沒有 setter 則爲 undefined。

注意: 數據描述符的value,writable 和 存取描述符的get, set屬性不能同時存在,不然會拋出異常。
有了Object.defineProperty方法,咱們能夠方便的利用存取描述符中的getter/setter來進行數據監聽,在get,set鉤子中分別作不一樣的操做,這是vue雙向數據綁定原理的雛形,咱們會在響應式系統的源碼分析時具體闡述。node

var o = {}
var value;
Object.defineProperty(o, 'a', {
    get() {
        console.log('獲取值')
        return value
    },
    set(v) {
        console.log('設置值')
        value = v
    }
})
o.a = 'sss' 
// 設置值
console.log(o.a)
// 獲取值
// 'sss'

然而Object.defineProperty的get和set方法只能觀測到對象屬性的變化,對於數組類型的變化並不能檢測到,這是用Object.defineProperty進行數據監控的缺陷,而vue中對於數組類型的方法作了特殊的處理。
es6的proxy能夠完美的解決這一類問題。webpack

2.1.2 Proxy

Proxy 是es6的語法,和Object.defineProperty同樣,也是用於修改某些操做的默認行爲,可是和Object.defineProperty不一樣的是,Proxy針對目標對象,會建立一個新的實例對象,並將目標對象代理到新的實例對象上, 本質的區別就是多了一層代理,外界對該對象的訪問,都必須先經過這層攔截,所以提供了一種機制,能夠對外界的訪問進行過濾和改寫。外界經過操做新的實例對象從而操做真正的目標對象。針對getter和setter的基本用法以下:git

var obj = {}
var nobj = new Proxy(obj, {
    get(target, property) {
        console.log('獲取值')
        return target[property]
    },
    set(target, key, value) {
        console.log('設置值')
        return target[key]
    }
})
nobj.a = 1111 // 經過操做代理對象從而映射到目標對象上
// 設置值
// 獲取值
// 1111
console.log(nobj.a)

Proxy 支持的攔截操做有13種之多,具體能夠參照Proxy,上面提到,Object.defineProperty的get和set方法並不能監測到數組的變化,而Proxy是否能作到呢?es6

var arr = [1, 2, 3]
let obj = new Proxy(arr, {
    get: function (target, key, receiver) {
        console.log("獲取數組");
        return Reflect.get(target, key, receiver);
    },
    set: function (target, key, receiver) {
        console.log('設置數組');
        return Reflect.set(target, key, receiver);
    }
})

obj.push(222) 
// '獲取數組'
// '設置數組'

顯然proxy能夠很容易的監聽到數組的變化。github

2.2 initProxy

有了這些理論基礎,咱們往下看vue的源碼,在初始化合並選項後,vue接下來的操做是爲vm實例設置一層代理,代理的目的是爲vue在模板渲染時進行一層數據篩選。若是瀏覽器不支持Proxy,這層代理檢驗數據則會失效。(檢測數據會放到其餘地方檢測)web

{
    // 對vm實例進行一層代理
    initProxy(vm);
}
// 代理函數
var initProxy = function initProxy (vm) {
    // 瀏覽器若是支持es6原生的proxy,則會進行實例的代理,這層代理會在模板渲染時對一些非法或者不存在的字符串進行判斷,作數據的過濾篩選。
    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
// 是否支持Symbol 和 Reflect
var hasSymbol =
    typeof Symbol !== 'undefined' && isNative(Symbol) &&
    typeof Reflect !== 'undefined' && isNative(Reflect.ownKeys);
function isNative (Ctor) {
    // Proxy自己是構造函數,且Proxy.toString === 'function Proxy() { [native code] }'
    return typeof Ctor === 'function' && /native code/.test(Ctor.toString())
}

看到這裏時,心中會有幾點疑惑。

  • 何時會觸發這層代理進行數據檢測?
  • getHandler 和 hasHandler的場景分別是什麼?

要解決這個疑惑,咱們接着往下看:

  • 1.在組件的更新渲染時會調用vm實例的render方法(具體模板引擎如何工做,咱們放到相關專題在分析),咱們觀察到,vm實例的render方法在調用時會觸發這一層的代理。
Vue.prototype._render = function () {
    ···
    // 調用vm._renderProxy
    vnode = render.call(vm._renderProxy, vm.$createElement);
}

也就是說模板引擎<div>{{message}}</div>的渲染顯示,會經過Proxy這層代理對數據進行過濾,並對非法數據進行報錯提醒。

  • 2.handers函數會根據options.render 和 options.render._withStripped執行不一樣的代理函數getHandler,hasHandler。當使用相似webpack這樣的打包工具時,咱們將使用vue-loader進行模板編譯,這個時候options.render 是存在的,而且_withStripped的屬性也會設置爲true,關於編譯版本和運行版本的區別不在這一章節展開。先大體瞭解使用場景便可。
2.2.1 代理場景

接着上面的問題,vm實例代理時會根據是不是編譯的版本決定使用hasHandler或者getHandler,咱們先默認使用的是編譯版本,所以咱們單獨分析hasHandler的處理函數,getHandler的分析相似。

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
}

而在vue的render函數的內部,本質上也是調用了with語句,當調用with語句時,該做用域下變量的訪問都會觸發has鉤子,這也是模板渲染時會觸發代理攔截的緣由。

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))])}
}

再次思考:咱們知道with語句是不推薦使用的,一個最主要的緣由是性能問題,查找不是變量屬性的變量,較慢的速度會影響性能一系列性能問題。

官方給出的解釋是: 爲了減小編譯器代碼大小和複雜度,而且也提供了經過vue-loader這類構建工具,不含with的版本。

2.2.2 代理檢測過程

接着上面的分析,在模板引擎render渲染時,因爲with語句的存在,訪問變量時會觸發has鉤子函數,該函數會進行數據的檢測,好比模板上的變量是不是實例中所定義,是否包含_, $這類vue內部保留關鍵字爲開頭的變量。同時模板上的變量將容許出現javascript的保留變量對象,例如Math, Number, parseFloat等。

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內部保留屬性的開頭。
        // warnReservedPrefix警告不能以$ _開頭的變量
        // 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
);

2.3 initLifecycle

分析完initProxy方法後,接下來是initLifecycle的過程。簡單歸納,initLifecycle的目的是將當前實例添加到父實例的$children屬性中,並設置自身的$parent屬性指向父實例。這爲後續子父組件之間的通訊提供了橋樑。舉一個具體的應用場景:

<div id="app">
    <component-a></component-a>
</div>
Vue.component('component-a', {
    template: '<div>a</div>'
})
var vm = new Vue({ el: '#app'})
console.log(vm) // 將實例對象輸出

因爲vue實例向上沒有父實例,因此vm.$parent爲undefined,vm的$children屬性指向子組件componentA 的實例。

子組件componentA的 $parent屬性指向它的父級vm實例,它的$children屬性指向爲空

源碼解析以下:

function initLifecycle (vm) {
    var options = vm.$options;
    // 子組件註冊時,會把父組件的實例掛載到自身選項的parent上
    var parent = options.parent;
    // 若是是子組件,而且該組件不是抽象組件時,將該組件的實例添加到父組件的$parent屬性上,若是父組件是抽象組件,則一直往上層尋找,直到該父級組件不是抽象組件,並將,將該組件的實例添加到父組件的$parent屬性
    if (parent && !options.abstract) {
        while (parent.$options.abstract && parent.$parent) {
        parent = parent.$parent;
        }
        parent.$children.push(vm);
    }
    // 將自身的$parent屬性指向父實例。
    vm.$parent = parent;
    vm.$root = parent ? parent.$root : vm;

    vm.$children = [];
    vm.$refs = {};

    vm._watcher = null;
    vm._inactive = null;
    vm._directInactive = false;
    // 該實例是否掛載
    vm._isMounted = false;
    // 該實例是否被銷燬
    vm._isDestroyed = false;
    // 該實例是否正在被銷燬
    vm._isBeingDestroyed = false;
}

最後簡單講講抽象組件,在vue中有不少內置的抽象組件,例如<keep-alive></keep-alive>,<slot><slot>等,這些抽象組件並不會出如今子父級的路徑上,而且它們也不會參與DOM的渲染。

喜歡本系列的朋友歡迎關注公衆號 假前端,有源碼解析和算法精選哦

clipboard.png

相關文章
相關標籤/搜索