vue - 響應式原理梳理(一)

描述

 咱們經過一個簡單的 Vue應用 來演示 Vue的響應式屬性html

html:
    <div id="app">
        {{message}}
    </div>
    
js:
    let vm = new Vue({
        el: '#app',
        data: {
            message: '123'
        }
    })

 在應用中,message 屬性即爲 響應式屬性vue

 咱們經過 vm.message, vm.$data.message, 可訪問 響應式屬性 messagenode

 當咱們經過修改 vm.message(vm.message = '456'), 修改後的數據會 更新到UI界面中react

問題

  • 爲何修改 vm.message, 便可觸發 UI更新
  • vm.messagedata.message 的關聯關係;

官方介紹

  vue的官網文檔,對響應式屬性的原理有一個介紹。設計模式

把一個普通的 JavaScript 對象傳給 Vue 實例的 data 選項,Vue 將遍歷此對象全部的屬性,並使用 Object.defineProperty 把這些屬性所有轉爲 getter/setter。

每一個組件實例都有相應的 watcher 實例對象,它會在組件渲染的過程當中把屬性記錄爲依賴,以後當依賴項的 setter 被調用時,會通知 watcher 從新計算,從而導致它關聯的組件得以更新。數組

官方文檔app

  以上介紹,只是對響應式原理進行了簡單描述,並無深刻細節。所以本文在源碼層面,對響應式原理進行梳理,對關鍵步驟進行解析。dom

  響應式原理涉及到的關鍵步驟以下:ide

  • 構建vue實例
  • vue實例data屬性初始化,構建響應式屬性
  • 將vue實例對應的template編譯爲render函數
  • 構建vue實例的watcher對象
  • 執行render函數,構建VNode節點樹,同時創建響應式屬性和watcher對象的依賴關係
  • 將VNode節點渲染爲dom節點樹
  • 修改響應式屬性,觸發watcher的更新,從新執行render函數,生成新的VNode節點樹
  • 對比新舊Vnode,從新渲染dom節點樹

構造函數 - Vue

Vue.js 給咱們提供了一個 全局構造函數 Vue函數

 經過 new Vue(options) 生成一個 vue實例,從而能夠構建一個 Vue應用

 其中,options 爲構造vue實例的配置項,即爲 { data, methods, computed, filter ... }

/*
      options:
      {
        data: {...},
        methods: {...},
        computed: {...},
        watch: {...}
        ...
      }
    */
    function Vue (options) {
      if (process.env.NODE_ENV !== 'production' &&
        !(this instanceof Vue)
      ) {
        warn('Vue is a constructor and should be called with the `new` keyword')
      }
      
      // 根據options, 初始化vue實例
      this._init(options)
    }
    
    export default Vue;

vue實例 構造完畢以後,執行實例私有方法 _init(), 開始初始化。

  在一個 vue應用 中,存在兩種類型的 vue實例根vue實例組件vue實例

根vue實例,由構造函數 Vue 生成。

組件vue實例,由組件構造函數 VueComponent 生成,組件構造函數 繼承 自構造函數 Vue

// 全局方法extend, 會返回一個組件構造函數。
    Vue.extend = function(options) {
        ...
        
        // 組件構造函數,用於建立組件
        var Sub = function VueComponent(options) {
            this._init(options);
        };
        // 子類的prototype繼承自Vue的prototype
        // 至關於Sub實例可使用Vue實例的方法
        Sub.prototype = Object.create(Vue.prototype);
        
        ...
        
        return Sub;
    }

  經過一個 根vue實例 和多個 組件vue實例,構成了整個 Vue應用

Vue.prototype._init

  在_init方法中,vue實例會執行一系列初始化操做。

  在初始化過程當中, 咱們經過全局方法 initState 來初始化vue實例的 datapropsmethodscomputedwatch 屬性。

Vue.prototype._init = function(options) {
        var vm = this;
        
        ...  // 其餘初始化過程, 包括創建子vue實例和父vue實例的對應關係、給vue實例添加自定義事件、執行beforeCreated回調函數等
        
        // 初始化props屬性、data屬性、methods屬性、computed屬性、watch屬性
        initState(vm);
        
        ... // 其餘初始化過程,好比執行created回調函數
        
        // vue實例初始化完成之後,掛載vue實例,將模板渲染成html
        if(vm.$options.el) {
                vm.$mount(vm.$options.el);
        }
    };

    function initState (vm: Component) {
      vm._watchers = [];
      // new Vue(options) 中的 options
      const opts = vm.$options; 
      
      // 將props配置項中屬性轉化爲vue實例的響應式屬性
      if (opts.props) initProps(vm, opts.props); 
      
      // 將 methods配置項中的方法添加到 vue實例對象中
      if (opts.methods) initMethods(vm, opts.methods);
      
      // 將data配置項中的屬性轉化爲vue實例的響應式屬性
      if (opts.data) {
        initData(vm)
      } else {
        observe(vm._data = {}, true /* asRootData */)
      }
      ...
    }

  其中,initData 方法會將 data配置項 中的屬性所有轉化爲 vue實例響應式屬性

initData

initData 方法的主要過程:

  • 根據data配置項,建立vue實例的私有屬性: _data
  • 經過 observe 方法,將 _data 對象中的屬性轉化爲 響應式屬性
  • 經過全局方法proxy, 創建 vue實例_data 的關聯關係。
function initData(vm) {
        // 獲取data配置項對象
        var data = vm.$options.data;
        // 組件實例的data配置項是一個函數
        data = vm._data = typeof data === 'function'? getData(data, vm): data || {};
        
        // 獲取data配置項的屬性值
        var keys = Object.keys(data);
        // 獲取props配置項的屬性值
        var props = vm.$options.props;
        // 獲取methods配置項的屬性值;
        var methods = vm.$options.methods;
        var i = keys.length;
        
        while(i--) {
            var key = keys[i];
            {
                // methods配置項和data配置項中的屬性不能同名
                if(methods && hasOwn(methods, key)) {
                    warn(
                        ("method \"" + key + "\" has already been defined as a data property."),
                        vm
                    );
                }
            }
            // props配置項和data配置項中的屬性不能同名
            if(props && hasOwn(props, key)) {
                "development" !== 'production' && warn(
                    "The data property \"" + key + "\" is already declared as a prop. " +
                    "Use prop default value instead.",
                    vm
                );
            } else if(!isReserved(key)) { // 若是屬性不是$,_ 開頭(vue的保留屬性)
                // 創建 vue實例 和 _data 的關聯關係性
                proxy(vm, "_data", key);
            }
        }
        // 觀察data對象, 將對象屬性所有轉化爲響應式屬性
        observe(data, true /* asRootData */);
    }

observe

  全局方法 observe 的做用是用來觀察一個對象,將_data對象的屬性所有轉化爲 響應式屬性

// observe(_data, true)
    function observe(value, asRootData) {
        if(!isObject(value)) {
            return
        }
        var ob;
       ...
       // 
       ob = new Observer(value);
       
       ...
       
       return ob;
    }


    var Observer = function Observer(value) {
        ...
        
        if(Array.isArray(value)) {
            // 若是value是數組,對數組每個元素執行observe方法
            this.observeArray(value);
        } else {
            // 若是value是對象, 遍歷對象的每個屬性, 將屬性轉化爲響應式屬性
            this.walk(value);
        }
    };
    
    // 若是要觀察的對象時數組, 遍歷數組,而後調用observe方法將對象的屬性轉化爲響應式屬性
    Observer.prototype.observeArray = function observeArray(items) {
        for(var i = 0, l = items.length; i < l; i++) {
            observe(items[i]);
        }
    };
    
    
     // 遍歷obj的屬性,將obj對象的屬性轉化爲響應式屬性
    Observer.prototype.walk = function walk(obj) {
        var keys = Object.keys(obj);
        for(var i = 0; i < keys.length; i++) {
           // 給obj的每個屬性都賦予getter/setter方法。
           // 這樣一旦屬性被訪問或者更新,這樣咱們就能夠追蹤到這些變化
            defineReactive(obj, keys[i], obj[keys[i]]);
        }
    };

defineReactive

  經過 defineProperty 方法, 提供屬性的 getter/setter 方法。

讀取 屬性時,觸發 getter,將與響應式屬性相關的vue實例保存起來。

修改 屬性時,觸發 setter,更新與響應式屬性相關的vue實例。

function defineReactive(obj, key, val, customSetter, shallow) {
        // 每個響應式屬性都會有一個 Dep對象實例, 該對象實例會存儲訂閱它的Watcher對象實例
        var dep = new Dep();
        
        // 獲取對象屬性key的描述對象
        var property = Object.getOwnPropertyDescriptor(obj, key);
        
        // 若是屬性是不可配置的,則直接返回
        if(property && property.configurable === false) {
            return
        }

        // 屬性原來的getter/setter
        var getter = property && property.get;
        var setter = property && property.set;
        
        // 若是屬性值是一個對象,遞歸觀察屬性值,
        var childOb = !shallow && observe(val);
        
        // 從新定義對象obj的屬性key
        Object.defineProperty(obj, key, {
            enumerable : true,
            configurable : true,
            get : function reactiveGetter() {
                // 當obj的某個屬性被訪問的時候,就會調用getter方法。
                var value = getter ? getter.call(obj) : val;
                
                
                // 當Dep.target不爲空時,調用dep.depend 和 childOb.dep.depend方法作依賴收集
                if(Dep.target) {
                
                    // 經過dep對象, 收集依賴關係
                    dep.depend();
                    if(childOb) {
                        childOb.dep.depend();
                    }
                    // 若是訪問的是一個數組, 則會遍歷這個數組, 收集數組元素的依賴
                    if(Array.isArray(value)) {
                        dependArray(value);
                    }
                }
                return value
            },
            set : function reactiveSetter(newVal) {
                // 當改變obj的屬性是,就會調用setter方法。這是就會調用dep.notify方法進行通知
                var value = getter ? getter.call(obj) : val;
                /* eslint-disable no-self-compare */
                if(newVal === value || (newVal !== newVal && value !== value)) {
                    return
                }
                /* eslint-enable no-self-compare */
                if("development" !== 'production' && customSetter) {
                    customSetter();
                }
                if(setter) {
                    setter.call(obj, newVal);
                } else {
                    val = newVal;
                }
                childOb = !shallow && observe(newVal);
                // 當響應式屬性發生修改時,經過dep對象通知依賴的vue實例進行更新
                dep.notify();
            }
        });
    }

  響應式屬性, 經過一個 dep 對象, 收集依賴響應式屬性的vue實例,在屬性改變時 通知vue實例更新

  一個 響應式屬性, 對應一個 dep 對象。

Dep

  在觀察者設計模式中,有兩種角色:SubjectObserver

Subject 會維護一個 Observer的依賴列表。當 Subject 發生變化時,會通知 Observer 更新。

  在vue中,響應式屬性做爲Subject, vue實例做爲Observer, 響應式屬性的更新會通知vue實例更新。

  響應式屬性經過 dep 對象來收集 依賴關係 。一個響應式屬性,對應一個dep對象。

var Dep = function Dep() {
        // dep對象的id
        this.id = uid++;
        // 數組,用來存儲依賴響應式屬性的Observer
        this.subs = [];
    };
    
    // 將Observer添加到dep對象的依賴列表中
    Dep.prototype.addSub = function addSub(sub) {
        // Dep對象實例添加訂閱它的Watcher
        this.subs.push(sub);
    };
    
    // 將Observer從dep對象的依賴列表中刪除
    Dep.prototype.removeSub = function removeSub(sub) {
        // Dep對象實例移除訂閱它的Watcher
        remove(this.subs, sub);
    };
    
    // 收集依賴關係
    Dep.prototype.depend = function depend() {
        // 把當前Dep對象實例添加到當前正在計算的Watcher的依賴中
        if(Dep.target) {
            Dep.target.addDep(this);
        }
    };
    
    // 通知Observer更新
    Dep.prototype.notify = function notify() {
        // stabilize the subscriber list first
        var subs = this.subs.slice();
        // 遍歷全部的訂閱Watcher,而後調用他們的update方法
        for(var i = 0, l = subs.length; i < l; i++) {
            subs[i].update();
        }
    };

proxy

  經過 defineProperty 方法, 給vue實例對象添加屬性,提供屬性的 getter/setter 方法。

  讀取vue實例的屬性( data配置項中的同名屬性 ), 觸發 getter,讀取 _data 的同名屬性。

  修改vue實例的屬性( data配置項中的同名屬性 ), 觸發 setter,修改 _data 的同名屬性。

// proxy(vm, _data, 'message')
    function proxy(target, sourceKey, key) {
        sharedPropertyDefinition.get = function proxyGetter() {
            return this[sourceKey][key]
        };
        sharedPropertyDefinition.set = function proxySetter(val) {
            this[sourceKey][key] = val;
        };
        Object.defineProperty(target, key, sharedPropertyDefinition);
    }

  經過 proxy 方法,vue實例 可代理私有屬性 _data, 即經過 vue實例 能夠訪問/修改 響應式屬性

總結

  結合源碼理解, 響應式屬性 的原理爲:

clipboard.png

相關文章
相關標籤/搜索