一、vue.js響應式原理html
參考:https://cn.vuejs.org/v2/guide/reactivity.htmlvue
https://github.com/answershuto/learnVuereact
注:learnVue講解的vue版本是2.3.0,我粘貼的源碼是2.6.10git
Vue 最獨特的特性之一,是其非侵入性的響應式系統。數據模型僅僅是普通的 JavaScript 對象。而當你修改它們時,視圖會進行更新。github
當你把一個普通的 JavaScript 對象傳入 Vue 實例做爲 data
選項,Vue 將遍歷此對象全部的屬性,並使用 Object.defineProperty
把這些屬性所有轉爲 getter/setter。Object.defineProperty
是 ES5 中一個沒法 shim 的特性,這也就是 Vue 不支持 IE8 以及更低版本瀏覽器的緣由。這些 getter/setter 對用戶來講是不可見的,可是在內部它們讓 Vue 可以追蹤依賴,在屬性被訪問和修改時通知變動。每一個組件實例都對應一個 watcher 實例,它會在組件渲染的過程當中把「接觸」過的數據屬性記錄爲依賴。以後當依賴項的 setter 觸發時,會通知 watcher,從而使它關聯的組件從新渲染。算法
受現代 JavaScript 的限制 (並且 Object.observe
也已經被廢棄),Vue 沒法檢測到對象屬性的添加或刪除。因爲 Vue 會在初始化實例時對屬性執行 getter/setter 轉化,因此屬性必須在 data
對象上存在才能讓 Vue 將它轉換爲響應式的。express
var vm = new Vue({ data:{ a:1 // `vm.a` 是響應式的 } }) vm.b = 2 // `vm.b` 是非響應式的
對於已經建立的實例,Vue 不容許動態添加根級別的響應式屬性。可是,可使用 Vue.set(object, propertyName, value)
方法向嵌套對象添加響應式屬性。因爲 Vue 不容許動態添加根級響應式屬性,因此你必須在初始化實例前聲明全部根級響應式屬性,哪怕只是一個空值。數組
var vm = new Vue({ data: { message: '' // 聲明 message 爲一個空值字符串 }, template: '<div>{{ message }}</div>' }) vm.message = 'Hello!' // 以後設置 `message`
若是你未在 data
選項中聲明 message
,Vue 將警告你渲染函數正在試圖訪問不存在的屬性。瀏覽器
Vue 在更新 DOM 時是異步執行的。只要偵聽到數據變化,Vue 將開啓一個隊列,並緩衝在同一事件循環中發生的全部數據變動。若是同一個 watcher 被屢次觸發,只會被推入到隊列中一次。這種在緩衝時去除重複數據對於避免沒必要要的計算和 DOM 操做是很是重要的。而後,在下一個的事件循環「tick」中,Vue 刷新隊列並執行實際 (已去重的) 工做。Vue 在內部對異步隊列嘗試使用原生的 Promise.then
、MutationObserver
和 setImmediate
,若是執行環境不支持,則會採用 setTimeout(fn, 0)
代替。例如,當你設置 vm.someData = 'new value'
,該組件不會當即從新渲染。當刷新隊列時,組件會在下一個事件循環「tick」中更新。多數狀況咱們不須要關心這個過程,可是若是你想基於更新後的 DOM 狀態來作點什麼,這就可能會有些棘手。雖然 Vue.js 一般鼓勵開發人員使用「數據驅動」的方式思考,避免直接接觸 DOM,可是有時咱們必需要這麼作。爲了在數據變化以後等待 Vue 完成更新 DOM,能夠在數據變化以後當即使用 Vue.nextTick(callback)
。這樣回調函數將在 DOM 更新完成後被調用。閉包
在initData中會調用observe這個函數將Vue的數據設置成observable的。當_data數據發生改變的時候就會觸發set,對訂閱者進行回調。那麼問題來了,須要對app._data.text操做纔會觸發set。爲了偷懶,咱們須要一種方便的方法經過app.text直接設置就能觸發set對視圖進行重繪。那麼就須要用到代理。
function initData (vm) { var data = vm.$options.data; data = vm._data = typeof data === 'function' ? getData(data, vm) : data || {}; // proxy data on instance var keys = Object.keys(data); var i = keys.length; while (i--) { var key = keys[i]; proxy(vm, "_data", key); } // observe data observe(data, true /* asRootData */); }
let app = new Vue({ el: '#app', data: { text: 'text' } })
把data上面的屬性代理到了vm實例上
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); }
二、vue.js依賴收集
new Vue({ template: `<div> <span>text1:</span> {{text1}} <span>text2:</span> {{text2}} <div>`, data: { text1: 'text1', text2: 'text2', text3: 'text3' } });
按照以前《響應式原理》中的方法進行綁定則會出現一個問題——text3在實際模板中並無被用到,然而當text3的數據被修改(this.text3 = 'test')的時候,一樣會觸發text3的setter致使從新執行渲染,這顯然不正確。
當對data上的對象進行修改值的時候會觸發它的setter,那麼取值的時候天然就會觸發getter事件,因此咱們只要在最開始進行一次render,那麼全部被渲染所依賴的data中的數據就會被getter收集到Dep的subs中去。在對data中的數據進行修改的時候setter只會觸發Dep的subs的函數。
Dep(依賴)
var Dep = function Dep () { this.id = uid++; this.subs = []; }; Dep.prototype.addSub = function addSub (sub) { this.subs.push(sub); }; /*依賴收集*/ Dep.prototype.depend = function depend () { if (Dep.target) { Dep.target.addDep(this); } };
Watcher(觀察者)
/*添加一個依賴關係到Deps集合中*/ Watcher.prototype.addDep = function addDep (dep) { var id = dep.id; if (!this.newDepIds.has(id)) { this.newDepIds.add(id); this.newDeps.push(dep); if (!this.depIds.has(id)) { dep.addSub(this); } } };
將觀察者Watcher實例賦值給全局的Dep.target,而後觸發render操做只有被Dep.target標記過的纔會進行依賴收集。有Dep.target的對象會將Watcher的實例push到subs中,在對象被修改觸發setter操做的時候dep會調用subs中的Watcher實例的update方法進行渲染。
function defineReactive$$1 (obj, key, val, customSetter, shallow) { var dep = new Dep(); // cater for pre-defined getter/setters var getter = property && property.get; var setter = property && property.set; Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { var value = getter ? getter.call(obj) : val; if (Dep.target) { dep.depend(); } return value } }); }
三、vue.js數據綁定原理
這張圖比較清晰地展現了整個流程,首先經過一次渲染操做觸發Data的getter(這裏保證只有視圖中須要被用到的data纔會觸發getter)進行依賴收集,這時候其實Watcher與data能夠當作一種被綁定的狀態(其實是data的閉包中有一個Deps訂閱者,在修改的時候會通知全部的Watcher觀察者),在data發生變化的時候會觸發它的setter,setter通知Watcher,Watcher進行回調通知組件從新渲染的函數,以後根據diff算法來決定是否發生視圖的更新。
initData主要是初始化data中的數據,將數據進行Observer,監聽數據的變化。下面這段代碼主要作了兩件事,一是將_data上面的數據代理到vm上,另外一件事經過observe將全部數據變成observable。
function initData (vm) { var data = vm.$options.data; data = vm._data = typeof data === 'function' ? getData(data, vm) : data || {}; // proxy data on instance var keys = Object.keys(data); var i = keys.length; while (i--) { // 遍歷data中的數據 var key = keys[i]; proxy(vm, "_data", key); // 將data上面的屬性代理到了vm實例上 } // observe data /*從這裏開始咱們要observe了,開始對數據進行綁定,這裏有尤大大的註釋asRootData,這步做爲根數據,下面會進行遞歸observe進行對深層對象的綁定。*/ observe(data, true /* asRootData */); }
observe
/** * Attempt to create an observer instance for a value, * returns the new observer if successfully observed, * or the existing observer if the value already has one. */ // 嘗試建立一個Observer實例(__ob__),若是成功建立Observer實例則返回新的Observer實例,若是已有Observer實例則返回現有的Observer實例。 function observe (value, asRootData) { if (!isObject(value) || value instanceof VNode) { return } var ob; /*這裏用__ob__這個屬性來判斷是否已經有Observer實例,若是沒有Observer實例則會新建一個Observer實例並賦值給__ob__這個屬性,若是已有Observer實例則直接返回該Observer實例*/ if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { ob = value.__ob__; } else if ( shouldObserve && !isServerRendering() && (Array.isArray(value) || isPlainObject(value)) && Object.isExtensible(value) && !value._isVue ) { ob = new Observer(value); } if (asRootData && ob) { /*若是是根數據則計數,後面Observer中的observe的asRootData非true*/ ob.vmCount++; } return ob }
Vue的響應式數據都會有一個__ob__的屬性做爲標記,裏面存放了該屬性的觀察器,也就是Observer的實例,防止重複綁定。
Observer的做用就是遍歷對象的全部屬性將其進行雙向綁定。
/** * Observer class that is attached to each observed * object. Once attached, the observer converts the target * object's property keys into getter/setters that * collect dependencies and dispatch updates. */ var Observer = function Observer (value) { this.value = value; this.dep = new Dep(); this.vmCount = 0; def(value, '__ob__', this); // 將Observer實例綁定到value的__ob__屬性上面去 if (Array.isArray(value)) { /* 若是是數組,將修改後能夠截獲響應的數組方法替換掉該數組的原型中的原生方法,達到監聽數組數據變化響應的效果。 這裏若是當前瀏覽器支持__proto__屬性,則直接覆蓋當前數組對象原型上的原生數組方法,若是不支持該屬性,則直接覆蓋數組對象的原型。 */ if (hasProto) { protoAugment(value, arrayMethods); } else { copyAugment(value, arrayMethods, arrayKeys); } this.observeArray(value); // 若是是數組則須要遍歷數組的每個成員進行observe } else { this.walk(value); // 若是是對象則直接walk進行綁定 } };
/** * Walk through all properties and convert them into * getter/setters. This method should only be called when * value type is Object. */ Observer.prototype.walk = function walk (obj) { var keys = Object.keys(obj); // 遍歷對象的每個屬性進行defineReactive綁定 for (var i = 0; i < keys.length; i++) { defineReactive$$1(obj, keys[i]); } }; /** * Observe a list of Array items. */ Observer.prototype.observeArray = function observeArray (items) { // 數組須要遍歷每個成員進行observe for (var i = 0, l = items.length; i < l; i++) { observe(items[i]); } };
Observer爲數據加上響應式屬性進行雙向綁定。若是是對象則進行深度遍歷,爲每個子對象都綁定上方法,若是是數組則爲每個成員都綁定上方法。若是是修改一個數組的成員,該成員是一個對象,那隻須要遞歸對數組的成員進行雙向綁定便可。但這時候出現了一個問題:若是咱們進行pop、push等操做的時候,push進去的對象根本沒有進行過雙向綁定,更別說pop了,那麼咱們如何監聽數組的這些變化呢? Vue.js提供的方法是重寫push、pop、shift、unshift、splice、sort、reverse這七個數組方法。
參考:https://cn.vuejs.org/v2/guide/list.html#數組更新檢測
變異方法,顧名思義,會改變調用了這些方法的原始數組。相比之下,也有非變異方法,例如 filter()
、concat()
和 slice()
。它們不會改變原始數組,而老是返回一個新數組。當使用非變異方法時,能夠用新數組替換舊數組。你可能認爲這將致使 Vue 丟棄現有 DOM 並從新渲染整個列表。幸運的是,事實並不是如此。Vue 爲了使得 DOM 元素獲得最大範圍的重用而實現了一些智能的啓發式方法,因此用一個含有相同元素的數組去替換原來的數組是很是高效的操做。
var arrayProto = Array.prototype; var arrayMethods = Object.create(arrayProto); var methodsToPatch = [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ]; /** * Intercept mutating methods and emit events */ methodsToPatch.forEach(function (method) { // cache original method var original = arrayProto[method]; def(arrayMethods, method, function mutator () { var args = [], len = arguments.length; while ( len-- ) args[ len ] = arguments[ len ]; var result = original.apply(this, args); // 調用原生的數組方法 var ob = this.__ob__; var inserted; switch (method) { case 'push': case 'unshift': inserted = args; break case 'splice': inserted = args.slice(2); break } if (inserted) { ob.observeArray(inserted); } // 數組新插入的元素須要從新進行observe才能響應式 // notify change // dep通知全部註冊的觀察者進行響應式處理 ob.dep.notify(); return result }); });
從數組的原型新建一個Object.create(arrayProto)對象,經過修改此原型能夠保證原生數組方法不被污染。在保證不污染不覆蓋數組原生方法添加監聽,主要作了兩個操做,第一是通知全部註冊的觀察者進行響應式處理,第二是若是是添加成員的操做,須要對新成員進行observe。
Dep就是一個發佈者,能夠訂閱多個觀察者,依賴收集以後Deps中會存在一個或多個Watcher對象,在數據變動的時候通知全部的Watcher。
// 通知全部訂閱者 Dep.prototype.notify = function notify () { // stabilize the subscriber list first var subs = this.subs.slice();for (var i = 0, l = subs.length; i < l; i++) { subs[i].update(); } };
/** * Subscriber interface. * Will be called when a dependency changes. */ // 調度者接口,當依賴發生改變的時候進行回調 Watcher.prototype.update = function update () { /* istanbul ignore else */ if (this.lazy) { this.dirty = true; } else if (this.sync) { // 同步則執行run直接渲染視圖 this.run(); } else { // 異步推送到觀察者隊列中,由調度者調用 queueWatcher(this); } };
/** * Scheduler job interface. * Will be called by the scheduler. */ // 調度者工做接口,將被調度者回調 Watcher.prototype.run = function run () { if (this.active) { var value = this.get(); if ( value !== this.value || // Deep watchers and watchers on Object/Arrays should fire even // when the value is the same, because the value may // have mutated. // 即使值相同,擁有Deep屬性的觀察者以及在對象/數組上的觀察者應該被觸發更新,由於它們的值可能發生改變。 isObject(value) || this.deep ) { // set new value var oldValue = this.value; this.value = value; if (this.user) { try { this.cb.call(this.vm, value, oldValue); } catch (e) { handleError(e, this.vm, ("callback for watcher \"" + (this.expression) + "\"")); } } else { this.cb.call(this.vm, value, oldValue); } } } };
/** * Define a reactive property on an Object. */ function defineReactive$$1 (obj, key, val, customSetter, shallow) { var dep = new Dep();// cater for pre-defined getter/setters // 若是以前該對象已經預設了getter以及setter函數則將其取出來,新定義的getter/setter中會將其執行,保證不會覆蓋以前已經定義的getter/setter。 var getter = property && property.get; var setter = property && property.set;// 對象的子對象遞歸進行observe並返回子節點的Observer對象 var childOb = !shallow && observe(val); Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { var value = getter ? getter.call(obj) : val; if (Dep.target) { dep.depend(); // 進行依賴收集 if (childOb) { childOb.dep.depend(); if (Array.isArray(value)) { // 是數組則須要對每個成員都進行依賴收集,若是數組的成員仍是數組,則遞歸 dependArray(value); } } } return value }, set: function reactiveSetter (newVal) { } }); }
四、vue.js事件機制
爲何在HTML中監聽事件(參考:https://cn.vuejs.org/v2/guide/events.html#爲何在-HTML-中監聽事件)
你可能注意到這種事件監聽的方式違背了關注點分離這個長期以來的優良傳統。但沒必要擔憂,由於全部的 Vue.js 事件處理方法和表達式都嚴格綁定在當前視圖的 ViewModel 上,它不會致使任何維護上的困難。實際上,使用 v-on
有幾個好處:掃一眼 HTML 模板便能輕鬆定位在 JavaScript 代碼裏對應的方法;由於你無須在 JavaScript 裏手動綁定事件,你的 ViewModel 代碼能夠是很是純粹的邏輯,和 DOM 徹底解耦,更易於測試;當一個 ViewModel 被銷燬時,全部的事件處理器都會自動被刪除。你無須擔憂如何清理它們。