咱們經過一個簡單的 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, 可訪問 響應式屬性 message。node
當咱們經過修改 vm.message(vm.message = '456'), 修改後的數據會 更新到UI界面中。react
vue的官網文檔,對響應式屬性的原理有一個介紹。設計模式
把一個普通的 JavaScript 對象傳給 Vue 實例的 data 選項,Vue 將遍歷此對象全部的屬性,並使用 Object.defineProperty 把這些屬性所有轉爲 getter/setter。每一個組件實例都有相應的 watcher 實例對象,它會在組件渲染的過程當中把屬性記錄爲依賴,以後當依賴項的 setter 被調用時,會通知 watcher 從新計算,從而導致它關聯的組件得以更新。數組
官方文檔app
以上介紹,只是對響應式原理進行了簡單描述,並無深刻細節。所以本文在源碼層面,對響應式原理進行梳理,對關鍵步驟進行解析。dom
響應式原理涉及到的關鍵步驟以下:ide
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應用。
在_init方法中,vue實例會執行一系列初始化操做。
在初始化過程當中, 咱們經過全局方法 initState 來初始化vue實例的 data、props、methods、computed、watch 屬性。
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 方法的主要過程:
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 的做用是用來觀察一個對象,將_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]]); } };
經過 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 對象。
在觀察者設計模式中,有兩種角色:Subject 和 Observer。
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(); } };
經過 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實例 能夠訪問/修改 響應式屬性。
結合源碼理解, 響應式屬性 的原理爲: