最近想深刻了解一下vue.js(後面簡稱vue)的核心原理,無心中看到了一個用於學習vue原理的項目。在深刻了解以後,發現它短小精悍,對於漸進式地瞭解vue的核心原理的實現大有幫助,因而乎就正式開始了對它探索之旅。html
概念表明着人類意識上的共識。因此,要想經過溝通交流來產生一些成果,對同一個概念達成共識是十分必要,要否則就是雞跟鴨講,不知所云。在對vue原理的瞭解過程當中,須要瞭解哪些概念呢?下面,咱們一塊兒來梳理一下。vue
準確來說,DocumentFragment是一個web API。由於它幾乎成爲了高效地操做大批量dom節點的代名詞,而vue在模板解析的實現裏面也用到了它,因此咱們有必要了解它。node
The following interfaces all inherit from Node’s methods and properties: Document, Element, Attr, CharacterData (which Text, Comment, and CDATASection inherit), ProcessingInstruction, DocumentFragment, DocumentType, Notation, Entity, EntityReferencejquery
由於咱們最經常使用的Element API 和DocumentFragment API都是繼承自Node這個接口,因此,DocumentFragment對象與普通的Element對象擁有這相同的方法和屬性。從這個角度來看,DocumentFragment對象跟普通的Element對象是「同樣」的。git
可是從全方位的角度來看,這二者是不同的。從表象看來,DocumentFragment對象與Element對象有兩個不一樣點:github
parent node
。即便你將它append到文檔中的一個element中去,這個element並不能成爲它的parent node
。const FG = document.createDocumentFragment();
const textNode = document.createTextNode('hello,documentFragment');
FG.appendChild(textNode) // 在這一步,界面並不會獲得更新
document.body.appendChild(FG); // 直到把它append到真實的文檔流,界面纔會有反應
console.log(FG.parentNode) // 雖然FG插入到真實的文檔中了,可是FG.parentNode仍然爲null
複製代碼
上面說的是表面現象。那形成這種差別表象的本質緣由是啥呢?答曰:「本質緣由是DocumentFragment對象並非真實文檔流的一部分,它只常駐在內存當中的。」因此,咱們能夠這麼理解:它只是dom節點的暫存器,當你把它(指的是DocumentFragment對象)append或者insert到真實文檔流的時候,它把本身全部的一切都掏空,還給真實的文檔流,而後本身功成告退。web
基於這個DocumentFragment對象這個特質,不少類庫用它進行大批量dom節點操做,vue也不例外。express
模板是將一個事物的結構規律予以固定化、標準化的成果,它體現的是結構形式的標準化。編程
在vue這個類庫裏面,模板有三種類型:數組
不管是哪一種類型「模版」,它本質上就是「HTML模板」-一堆由html標籤和特殊佔位字符組成的標記。這裏的「html標籤」表明着的就是一種固化的頁面結構,而「特殊佔位字符」組成就是一套「模板語法」。
「模板」都是要被解析(或者說編譯)的,而解析的對象就是那些「特殊佔位字符」。在vue裏面,江湖人稱之爲「魔符」。最後,負責實現解析功能的那些代碼咱們稱之爲「模板引擎」。從jquery時代的mustache和handlerbar到如今的angular和vue,「模板」一直伴隨咱們左右。從使用者的角度來看,它們是不同的。可是從實現者的角度來看,它們都是同樣的,都是「模板語法」 + 「模板引擎」。
由於這個mvvm類庫是用於學習vue的原理的,因此,咱們得假設「模版語法」已經設計好了。咱們須要思考的問題就是:「給定一套模板語法的前提下,咱們該如何編程實現該模板的模板引擎呢?」。
注意:該學習庫爲代碼的精簡,實現上作了調整。
表達式(expression)是JavaScript中的一個短語,JavaScript解釋器會將其計算(evaluate)出一個結果。
JavaScript犀牛書如是說。換而言之,一切能計算出值的語句都是表達式。
在vue模板裏面,不管雙重花括號裏面的字符串仍是屬性綁定指令值,都是表達式。而表達式的核心要素就是[變量],這個變量就是對應於某個viewModel實例屬性。若是A使用B,咱們就說「A依賴B」的話,那麼咱們能夠將上面的表述轉化爲這樣結論:「在vue裏面, [模板]依賴[表達式],[表達式]依賴[viewModel實例屬性]」。其實,更深刻地講[表達式]依賴的是咱們實例化vue對象時傳入的data對象的屬性。只不過,在後期,咱們將data對象的屬性代理到viewModel實例屬性而已。
對錶達式概念以及它在模板和viewModel實例之間的樞紐做用的理解是相當重要。由於這一點關係到你對mvvm模式中各個角色命名語義上的理解(好比,源碼中「watcher」,「dependency」等等)。
講mvvm模式,天然是離不開數據代理了。那什麼是數據代理呢?
數據代理其實就是變量讀寫的代理,換句話說就是把對原變量的[讀和寫]交由另一個變量來完成。舉個例子,有個對象,它層次很深:
const obj = {
a:{
b:{
c : 'xxx'
}
}
}
複製代碼
咱們每次訪問c屬性都要經過obj.a.b.c
來完成的話,若是次數多了就會顯得很麻煩。咱們能夠將對obj.a.b.c
的讀寫委託到到obj對象新的第一層屬性上,也就是說咱們寫下這麼一行代碼時候:
obj.c = 'xxx'
複製代碼
js引擎在解析的過程當中會幫助咱們將讀寫操做轉接到obj.a.b.c
身上,實際執行的是一下語句:
obj.a.b.c = 'xxx'
複製代碼
簡單的實現以下:
Object.defineProperty(obj,"c",{
get(){
return obj.a.b.c
},
set(value){
obj.a.b.c = value;
}
})
複製代碼
要理解「數據代理」這個概念,具體到這裏例子就是要理解obj.a.b.c
和obj.c
的關係。 廢話很少說,咱們來總結一下這二者的關係。那就是:obj.a.b.c
將本身的「讀和寫」業務委託給obj.c
來完成了,obj.c
是obj.a.b.c
的代理。
vue2.x以前的數據代理是基於ES5的Object.defineProperty這個API來實現的,我相信這是人盡皆知的啦,這裏就不展開說了(據說,3.x是基於原生接口Proxy來實現)。不過,我想強調的一點是,數據代理並非mvvm模式的必要特徵,它只是一個便利之舉而已。具體爲何這麼說,在分析源碼的過程當中,我再來解釋這其中的理由。
咱們每天提「數據綁定」,那麼「數據綁定」究竟是什麼意思?簡而言之,在mvvm模式的話題背景下,「數據綁定」就是指將viewModel實例屬性綁定到HTML模板中,一旦屬性值發生改變,界面就會「自動」更新。時刻注意,「自動」並是真的自動,「自動」是須要咱們去用代碼去實現的。
咱們除了提「數據綁定」外,也常常提「數據單向綁定」和「數據雙向綁定」。其實通常來講,「數據單向綁定」就是指咱們上面所提到的「數據綁定」(viewModel實例屬性 -》 HTML模板
),而「數據雙向綁定」就是在「數據單向綁定」的基礎上增長另一個方向(HTML模板 -》 viewModel實例屬性
)的綁定而已。通常來講,「數據雙向綁定」只是針對表單元素input,select,textarea等等而已。經過監聽這些元素的input事件,在類庫的內部手動地給viewModel實例屬性賦值就能夠實現這個「雙向數據綁定」。
時刻記住,「數據雙向綁定」是創建在「數據單向綁定」之上的。等會咱們在講解代碼實現的時候,咱們會先講如何實現「數據單向綁定」,再講如何實現「數據雙向綁定」就是這個理。
從因果的角度來看,「數據綁定」是一種結果,而數據劫持是達成這種結果的手段。它們二者的關係能夠表述爲數據綁定是經過數據劫持來實現的。
那麼到底什麼是「數據劫持」呢?「數據劫持」就是剝奪原變量讀寫方面的話語權。剝奪以後,我想幹嗎就幹嗎。具體的話,「數據劫持」仍是經過Object.defineProperty這個API來實現的。
const obj = {
a:'xxx',
b:'yyy'
}
Object.defineProperty(obj,"a",{
get(){
// 劫持原屬性的讀的權利
// 目前我什麼都不幹
},
set(){
// 劫持原屬性的寫的權利
// 目前我什麼都不幹
}
})
console.log(obj.a) // undefined
obj.a = "zzz"
console.log(obj.a) // undefined
複製代碼
這個劫持是完徹底全的。什麼意思呢?就像上面這個例子那樣,我劫持以後,我什麼都不幹,那就js引擎是不會爲此搞個兼容降級的機制(好比說,js引擎一旦判斷你getter返回undefined
,它會幫你缺省地返回個原來的值「xxx」)。不,它不會這麼幹的。它是100%地放權給你。這就充分地體現了「劫持」這個詞的語義了。
也許你會問:「數據代理和數據劫持都是經過Object.defineProperty這個API來實現的,感受原理同樣啊,它們有什麼不一樣嗎?」
答曰:「這兩個概念仍是不同的。由於「數據代理」是在對象上產生一個新的屬性,而「數據劫持」則是對對象已存在的屬性進行從新定義。」剛開始我看源碼的時候,也有相似的困惑,後面反覆查看這二者的代碼實現,才發現二者的不一樣。若是你有一樣的疑問,不怪你,多看幾回源碼就行了。
到這裏,咱們將涉及的概念梳理得差很少了,下面咱們接着介紹各個功能模塊的實現流程。
我畫的細緻化的流程圖:
在這裏,我把vue這個類庫的代碼生命週期分爲兩個階段: 初始化階段和運行時階段。
而初始化階段又細分爲三個小階段:
因此,如上圖所示,總共加起來就四個階段。下面,咱們來探討一下各個階段的實現流程和原理。
這個階段其實沒什麼好講的,其核心原理是Object.defineProperty這個API-即經過這個API來將vm._data的讀寫權代理到vm的實例屬性上。在這裏,值得注意的兩點是:
下面,咱們來具體探討一下以上的兩點。
針對第一點,咱們來試驗一下,看看禁掉數據代理,程序是否還能正常運行。怎麼作呢?
第一步:把mvvm.js構造函數MVVM中的與實現數據代理相關的代碼註釋掉。
第二步:去到Watcher類的get方法裏面,把對this.getter方法的調用傳參時的第二個參數從「this.vm」替換爲"this.vm._data"。
第三步:去到Compile類的bind方法裏面,把對this._getVMVal方法的調用傳參時的第一個參數從「vm」替換爲「vm._data」。
最後,保存更新,刷新頁面。你會發現,數據綁定功能並無受到影響,程序正常運行。這也佐證了個人第一個觀點。
只不過,如今你若是想要在改變data的值的時候,你就不能直接對vm實例進行操做了。也就是說,你不能這麼寫了:this.xxx = 'yyy'
或者vm.xxx = 'yyy'
。而是,要這麼寫::this._data.xxx = 'yyy'
或者vm._data.xxx = 'yyy'
。這麼作,咱們會面臨一個問題。假如,咱們要訪問的屬性處在很深的層次呢?好比:a.b.c
, 那麼你就得寫this._data.a.b.c = 'yyy'
或者vm._data.a.b.c = 'yyy'
。一次還好,次數多了,就顯得不夠便利,而一個簡單的數據代理就能幫助咱們減小一個屬性層次,從而讓咱們的數據訪問更加直觀。我想,這就是數據代理存在的意義吧。
至於第二點,咱們細心地觀察【數據代理】和【數據劫持】的實現代碼就能夠發現。
// 數據代理實現代碼
// 這個data就是咱們傳遞到Vue構造函數的的option對象的data字段
Object.keys(data).forEach(function(key) {
me._proxyData(key);
});
_proxyData: function(key, setter, getter) {
var me = this;
setter = setter ||
Object.defineProperty(me, key, {
configurable: false,
enumerable: true,
get: function proxyGetter() {
return me._data[key];
},
set: function proxySetter(newVal) {
me._data[key] = newVal;
}
});
}
// 數據劫持實現代碼
Observer.prototype = {
walk: function(data) {
var me = this;
Object.keys(data).forEach(function(key) {
me.convert(key, data[key]);
});
},
convert: function(key, val) {
this.defineReactive(this.data, key, val);
},
defineReactive: function(data, key, val) {
var dep = new Dep();
var childObj = observe(val);
Object.defineProperty(data, key, {
enumerable: true, // 可枚舉
configurable: false, // 不能再define
get: function() {
if (Dep.target) {
dep.depend();
}
return val;
},
set: function(newVal) {
if (newVal === val) {
return;
}
val = newVal;
// 新的值是object的話,進行監聽
childObj = observe(newVal);
// 通知訂閱者
dep.notify();
}
});
}
};
複製代碼
雖然數據代理和數據劫持都是經過Object.defineProperty這個API來實現的,可是二者針對的【對象】(也就是調用時,傳遞的第一個參數)明顯是不同的。
對於數據代理而言,咱們的【對象】是vm實例,而定義的【屬性】倒是data對象的屬性。咱們從MVVM的構造函數來看,vm實例在此以前並無定義這些屬性,這些屬性在調用Object.defineProperty()方法的時候是不存在的。因此,它們是vm實例的全新屬性;而對於數據劫持而言,咱們的【對象】是data對象,定義的【屬性】仍是data對象的屬性,因此這是從新定義了。正是這個從新定義,才很好地呼應了「劫持」這個詞的語義,不是嗎?
至於數據代理和數據劫持所操做的對象屬性層次數上差別,主要是體如今數據代理只是進行過一次Object.keys().forEach()
調用來遍歷data對象的第一層屬性。而數據劫持則經過在defineReactive方法裏面的var childObj = observe(val);
調用,間接遞歸調用了屢次Object.keys().forEach()
來實現對data對象全部層次屬性的劫持。
到這裏,咱們經過對比數據劫持特性的實現來將數據代理的實現細節梳理了一遍。能夠這麼說,數據代理只是mvvm模式的前菜,數據綁定纔是它的核心部分,下面一塊兒來瞧瞧它的實現過程。
正如在概念介紹部分所講的,「數據綁定」是咱們要實現的一個結果,它並非重點。重點是,實現這個結果的手段-數據劫持。因此在這小節,與其說是講「數據綁定 」,不如說是講「數據劫持」。
「數據劫持」的實現代碼都放在了observer.js文件裏面了。整個文件代碼行數很少,可是由於這個類庫的做者從vue源碼中摘抄得恰到好處,因此顯得短小精悍,十分利於閱讀。
不管在概念介紹部分,仍是上個階段,咱們都對數據劫持的過程簡單地介紹了一遍。將這個過程簡單地用一句來講,那就是:遍歷咱們傳入data對象的每一層屬性,對每個屬性設置相應的訪問器(getter和setter)。儘管裏面涉及到了稍微複雜一點的【間接遞歸調用】,可是這個過程還算簡單直觀,沒啥好講的。咱們要講就要講在數據劫持過程當中所設下的訪問器,由於這裏面隱藏着一條很重要的的關係鏈:
dep實例 -》 屬性 -》 表達式 -》 watcher實例
縱觀初始化階段,這條關係鏈的創建分爲四個階段:
屬性 《-》 表達式
的多對多關係(模板書寫階段)屬性 《-》 dep實例
的一對一關係(數據綁定階段)表達式 《-》 watcher實例
的一對一關係(模板解析階段)dep實例 《-》 watcher實例
的多對多關係(模板解析階段)屬性與表達式多對多的關係是在咱們的模板書寫階段確立的。多對多關係,什麼意思呢?意思就是:一個表達式有可能「依賴」或者「使用」多個屬性,而同一個屬性能夠被多個表達式所使用。看具體的例子:
<div>
<div>第一個表達式:{{a.b.c}}</div>
<div>第二個表達式:{{a.b.c}}</div>
<div>第三個表達式:{{a.b.c}}</div>
</div>
複製代碼
咱們先只看第一個表達式。由於這個表達式使用了三個屬性,分別是「a」,"a.b"和「a.b.c」,因此咱們說一個表達式有可能對應多個屬性。而後,咱們再看模板的所有。同一個屬性「a.b.c」被三個表達式所使用,因此咱們說一個屬性有可能對應多個表達式。
綜上所述,屬性與表達式是「多對多」的關係。
在數據綁定階段,之因此要分析屬性與表達式的關係,是由於這個關係是整條關係鏈的根源,是[屬性與dep實例的關係]的鋪墊。好,如今咱們已經講完了。那麼咱們能夠把重點聚焦到這個階段所發生的屬性與dep實例的關係創建。這個關係的創建是在劫持屬性-定義訪問器的時候發生的。那麼,下面一塊兒來看看相關代碼:
Observer.prototype = {
walk: function(data) {
var me = this;
Object.keys(data).forEach(function(key) {
me.convert(key, data[key]);
});
},
convert: function(key, val) {
// 記住,this.data只是vm._data的一個引用
// 引用鏈是這樣的: this.data -> vm.$option.data -> vm._data
this.defineReactive(this.data, key, val);
},
defineReactive: function(data, key, val) {
var dep = new Dep();
var childObj = observe(val);
Object.defineProperty(data, key, {
enumerable: true, // 可枚舉
configurable: false, // 不能再define
// 這裏經過閉包,將key所對應的dep實例以及對之間的對應關係保存在內存當中了
get: function() {
if (Dep.target) {
dep.depend();
}
return val;
},
set: function(newVal) {
if (newVal === val) {
return;
}
val = newVal;
// 新的值是object的話,進行監聽
childObj = observe(newVal);
// 通知全部訂閱者。這裏的訂閱者就是watcher實例
dep.notify();
}
});
}
}
複製代碼
咱們時刻要記住,咱們實例化vue傳進去的data對象是按引用傳遞的,Object.defineProperty中的形參中的data對象就是咱們傳進去data對象。在defineReactive方法
中,經過var childObj = observe(val);
這條語句對defineReactive方法
進行了間接遞歸調用,從而實現了對data對象全部層次中的遍歷。而遍歷過程當中,它作的第一件事就是new一個Dep實例
。可是到這裏,屬性和Dep實例
一對一的關係還沒法創建起來,由於如你所見,new一個Dep實例
的時候,咱們並無傳遞任何跟該屬性相關的數據給Dep實例
。那它們之間的關係是怎麼創建的呢?答曰:「閉包」。
在defineReactive方法
中有兩層詞法做用域。第一層是defineReactive方法
自己,第二層是該屬性的getter和setter函數。由於這兩層嵌套做用域都訪問了dep
這個變量,因此,咱們的代碼就造成了一個可見的閉包。當defineReactive方法
在運行時被真正調用的時候,咱們的代碼就產生了一個閉包。就是這個閉包,將當前屬性與當前dep實例的一一對應關係保存在內存當中,等待這咱們後面的使用。
能夠這麼說,數據劫持階段,完成了屬性與dep實例之間一一對應關係的創建。不但如此,還爲後面watcher實例與dep實例的關係創建埋下了伏筆。這個伏筆在哪裏呢?對的,就在getter裏面:
// ....此前省略了不少代碼
get: function() {
// 對的,就是這三行簡簡單單的代碼
if (Dep.target) {
dep.depend();
}
return val;
},
// ....此後省略了不少代碼
複製代碼
當程序在下個階段(模板解析階段)進入咱們剛剛說起的閉包,執行到dep.depend()
這條語句時,watcher實例與dep實例關係的創建正式拉開帷幕。那行,咱們一塊兒進入下個階段的流程分析吧。
if (this.$el) {
this.$fragment = this.node2Fragment(this.$el);
this.init();
this.$el.appendChild(this.$fragment);
}
複製代碼
從該學習庫的實現代碼來看,模板解析又能夠分爲三個步驟:
要想理解步驟1的實現代碼,主要要理解好DocumentFragment對象和appendChild()這個API。對於DocumentFragment對象的理解,咱們已經在「概念梳理」部分講解過,這裏就再也不贅述了。而對於appendChild()這個API,最爲關鍵的第一點是,要理解「每個element node只能有一個父節點」這句話。若是你不理解這句話,那麼你就對步驟1的核心實現代碼有困惑:
node2Fragment: function(el) {
var fragment = document.createDocumentFragment(),
child;
// 將原生節點拷貝到fragment
while (child = el.firstChild) {
fragment.appendChild(child);
}
return fragment;
},
複製代碼
當你看到fragment.appendChild(child)
這條語句的時候,你可能會想,append一個現存的節點到fragment
對象以後,不用刪除它嗎?答曰:「不用」。這正是appendChild()這個API負責乾的事。這個API仍是要遵循「每個element node只能有一個父節點」的原則。因此,一番循環下來,真實節點容器裏面的節點都被轉移到了fragment
對象裏面了。
步驟1講完了,步驟2纔是重中之重。下面咱們來看看步驟2。
在這個步驟裏面,咱們主要承接着上個階段還沒講到的兩個關係來說解:
表達式 《-》 watcher實例
的一對一關係(模板解析階段)dep實例 《-》 watcher實例
的多對多關係(模板解析階段)正如我在上文中給出的細緻化的流程圖所描述那樣,整個模板解析流程的最底部作了兩件事情:
換成專業的話說,就是compileElement()函數的調用棧的最頂部函數bind()作了兩件事:
......
bind: function(node, vm, exp, dir) {
var updaterFn = updater[dir + 'Updater'];
// 1. 完成了界面的初始化顯示
updaterFn && updaterFn(node, this._getVMVal(vm, exp));
// 2. 開始着手實例化watcher
new Watcher(vm, exp, function(value, oldValue) {
updaterFn && updaterFn(node, value, oldValue);
});
},
......
複製代碼
從上面實現代碼中,咱們很直觀地看到了這兩件事所對應的代碼:
updaterFn && updaterFn(node, this._getVMVal(vm, exp));
new Watcher(vm, exp, function(value, oldValue) { updaterFn && updaterFn(node, value, oldValue); });
第一件事的實現代碼沒啥好講的,由於代碼執行到了這裏,DOM操做的三要素都肯定了:節點(node),操做(updaterFn)和值(this._getVMVal(vm, exp)),因此接下來就是在特定的節點上對某個屬性賦予某個值。
第二件事的實現代碼纔是模板解析階段的精華之所在。
// 實例化Watcher類
new Watcher(vm, exp, function(value, oldValue) {
updaterFn && updaterFn(node, value, oldValue);
});
// Watcher類的構造函數
function Watcher(vm, expOrFn, cb) {
this.cb = cb;
this.vm = vm;
this.expOrFn = expOrFn;
this.depIds = {};
if (typeof expOrFn === 'function') {
this.getter = expOrFn;
} else {
this.getter = this.parseGetter(expOrFn.trim());
}
this.value = this.get();
}
複製代碼
不管是形參exp仍是expOrFn,都是指代的是模板上的某個表達式。從new Watcher(vm, exp,...)
到this.expOrFn = expOrFn;
,咱們能夠很直觀地看到了watcher實例與表達式已經創建一一對應關係了。
好,到目前爲止,咱們還剩下dep實例與watcher實例之間的關係沒有分析到。要想搞清楚這二者之間的關係是如何創建的,咱們得繼續往watcher實例化所涉及的函數調用棧追查下去。
在追查以前,咱們腦海裏面得有個概念,那就是dep實例已經存在內存中了,它正在等待在watcher實例化過程當中去點燃那根創建二者關係的導火索。
還記得咱們在上一階段所說的那個閉包嗎?
當
defineReactive方法
在運行時被真正調用的時候,咱們的代碼就產生了一個閉包。就是這個閉包,將當前屬性與當前dep實例的一一對應關係保存在內存當中,等待這咱們後面的使用。
顯然,這個閉包的內層詞法做用域就是getter函數。而咱們所說的導火索就是getter函數裏面的dep.depend();
語句。既然導火索是在屬性的getter函數中(也能夠稱之爲屬性訪問器),顧名思義,那麼一旦去讀取該屬性值的時候,咱們就會「點燃」這個根導火索。那在watcher實例化過程當中,哪裏須要讀取屬性的值呢?
咱們順着watcher實例化所涉及的代碼往下找,看到這麼一條語句:
this.value = this.get();
複製代碼
沒錯,就是這裏,就是這條語句(目的是計算當前watcher實例所對應表達式的值)點燃了watcher實例與dep實例關係創建的導火索。嚴謹地來講,在dep實例真正與watcher實例創建關係以前,其實要「敲開兩道門」的。哪兩道門呢?
第一道門是Dep.target
,它就在屬性的getter訪問器裏面:
if (Dep.target) {
dep.depend();
}
複製代碼
第二道門是this.depIds.hasOwnProperty(dep.id)
, 它就在watcher實例的addDep方法裏面:
if (!this.depIds.hasOwnProperty(dep.id)) {
dep.addSub(this);
this.depIds[dep.id] = dep;
}
複製代碼
能夠看出,只有Dep類的靜態屬性target的值不是falsy值的時候,第一道門纔會打開;只有當前dep實例沒有跟當前watcher實例創建過關係的前提下,第二道門纔會打開。
好,首先咱們來看看第一道門開關的狀態。第一道門一開始是關閉的。對應的代碼是observer.js文件裏面的最後一行代碼:
Dep.target = null;
複製代碼
那何時打開了呢?咱們不妨回到this.value = this.get();
這行代碼裏面,繼續往this.get()函數調用棧的頂部追溯。果不其然,在watcher實例的get()方法的實現代碼裏面,咱們看到這麼一條語句:
Dep.target = this;
複製代碼
你沒看錯,第一道門已經打開了。緊接着的一條語句是this.getter.call(this.vm, this.vm);
,對它的執行,程序會進入到屬性的getter訪問器裏面,開始關係創建之旅。
咱們能夠把接下來的事情想象爲一個電影片斷。這個電影片斷裏面的第一個鏡頭就是一我的站在了一道門面前。這我的就叫作「(內存中的)dep實例」。只見dep實例輕輕地敲了敲watcher實例的「閨房門」,說:「親愛的watcher實例兒,你終於開門啦,那咱們創建關係吧」。watcher實例猶抱琵琶半遮面地說:「客官莫急,你還有第二道門要打開呢?」。
因而乎,dep實例來到了第二道門的門口。他一看,原來門是打開的(沒有創建過關係以前,watcher實例的depIds屬性固然沒有當前dep實例的引用)。內心就尋思着想:「這娘們挺能裝的,還騙我。門根本就沒有關着」。因而,dep實例就單槍直入。鏡頭來到這裏就完了.......
好了,如今dep實例已經經過了兩道門,順利進入watcher實例的閨房了。它們倆準備創建關係了。而負責創建雙邊關係的核心語句只有兩行行代碼,也就是:
dep.addSub(this);
this.depIds[dep.id] = dep;
複製代碼
最後,咱們以回答「dep實例和watcher實例創建關係是啥意思呢?」這個問題來結束這個階段的分析吧。
第一行代碼的做用就是實現dep實例主動向watcher實例創建關係。用代碼的語言來講就是,把當前watcher實例存放到dep實例的屬性subs(subscriber)數組中,等待被通知(調用watcher實例的update()方法);
第二行代碼的做用是實現watcher實例主動向dep實例創建關係。用代碼的語言來講就是,在watcher實例屬性depIds對象裏面創建對應的key-value來保存當前dep實例的引用。這個做用至關於對dep實例的訪問存根。當下一次再來創建關係的時候,發現這個dep實例已經有存根了,則能夠將它拒之門外。
以上,咱們算是梳理完了dep實例與watcher實例之間多對多關係創建的整個流程了。至於爲何是多對多呢?咱們上面概念梳理階段已經說過了,如今咱們再來重複一遍。咱們也要緊緊記住,由於表達式是能夠由n個屬性組成的。因此,讀取某個表達式的值頗有可能致使n次的屬性值的讀取 。n個屬性則對應n個dep實例,而n次屬性值的讀取則意味着在一次的watcher實例化過程當中發生n次的關係創建。而另一個角度來看,一個模板能夠有m個表達式,m個表達式則意味着m次的watcher實例化。m *n,最終, dep實例與watcher實例造成了多對多的關係。
好,到這裏,這個階段的流程分析已經完畢了。若是從是否幹了實事的角度總結這個階段,那麼這個階段只作了一件實事。那就是完成界面的初始化顯示。其他的都是爲運行期階段作所的準備工做。行,咱們一塊兒來看看,這個階段作的準備工做是如何接到到下個階段的。
所謂的運行期說白了就是對屬性進行賦值而觸發相關代碼執行這麼的一個階段。在vue裏面,不管是在事件處理器仍是在咱們自定義的method,對於vue而言,其核心操做依然「對屬性進行賦值」。這就這麼一個簡簡單單地賦值,讓咱們對接上個階段所作的一切準備工做。
其實在進入數據劫持屬性的setter以前,是先通過數據代理所註冊的getter,再通過數據劫持屬性的getter,最後才進入數據劫持屬性的setter的。可是,由於此時的Dep.target爲null,因此,這種屬性值讀取是沒法經過dep實例與watcher實例關係創建的第一道大門。所以,這種衝擊到這裏戛然而止了。咱們只須要關注這段旅程(對屬性進行賦值)的終點站就好-也就是數據劫持屬性的setter訪問器。下面來看具體的代碼:
set: function(newVal) {
if (newVal === val) {
return;
}
val = newVal;
// 新的值是object的話,進行監聽
childObj = observe(newVal);
// 通知訂閱者
dep.notify();
}
複製代碼
其實,setter就作了三件事。
對vm實例賦值之因此能進入到數據劫持屬性的setter,dep實例之因此能通知到watcher實例,watcher實例之因此能調用到updater,種種的一切,都是由於咱們在上三個階段作足了準備,因此才讓這些事情的發生成爲可能。
到此,四個階段都分析完了。最大的重點就是dep實例與watcher實例的多對多關係的創建。其實「dep實例與watcher實例的多對多關係的創建」還有個另一個叫法「依賴收集」。如今回過頭來,咱們能夠這麼理解「依賴收集」這個概念:若是說:「誰使用了誰,誰就依賴誰」的話。那麼如今表達式使用了屬性,咱們就說:「表達式依賴了屬性」。接下來,咱們能夠把dep實例看作是屬性的經紀人,把watcher看作是表達式的管家。管家負責收集表達式的全部依賴的屬性,當它逐一去找到對應屬性的時候,這些屬性跟watcher管家說:「有什麼事,你跟個人經紀人dep實例說吧」。到最後,「依賴收集」變成了dep實例和watcher實例之間的事了。簡而言之,「依賴收集」能夠理解爲「watcher實例代替表達式去收集後者所依賴屬性的dep實例」。
若是從data對象屬性這個視角出發,咱們能看到的屬性,表達式,dep實例和watcher實例這四者之間的關係創建泳道圖大概以下:
這個學習庫涉及到了很多技術點的應用,下面作個簡單的記錄。
比較明顯和重要的閉包有如下三個:
1.在被嵌套的詞法做用域getter或者setter訪問了嵌套詞法做用域defineReactive的dep變量:
//在observer.js文件裏面
defineReactive: function(data, key, val) {
var dep = new Dep();
var childObj = observe(val);
Object.defineProperty(data, key, {
enumerable: true, // 可枚舉
configurable: false, // 不能再define
get: function() {
if (Dep.target) {
dep.depend();
}
return val;
},
set: function(newVal) {
if (newVal === val) {
return;
}
val = newVal;
// 新的值是object的話,進行監聽
childObj = observe(newVal);
// 通知訂閱者
dep.notify();
}
});
}
複製代碼
//在compile.js文件裏面
bind: function(node, vm, exp, dir) {
var updaterFn = updater[dir + 'Updater'];
updaterFn && updaterFn(node, this._getVMVal(vm, exp));
new Watcher(vm, exp, function(value, oldValue) {
updaterFn && updaterFn(node, value, oldValue);
});
},
複製代碼
//在watcher.js文件裏面
parseGetter: function(exp) {
if (/[^\w.$]/.test(exp)) return;
var exps = exp.split('.');
return function(obj) {
for (var i = 0, len = exps.length; i < len; i++) {
if (!obj) return;
obj = obj[exps[i]];
}
return obj;
}
}
複製代碼
直接遞歸或者間接遞歸有兩個:
//在observer.js文件裏面
defineReactive: function(data, key, val) {
var dep = new Dep();
var childObj = observe(val);
Object.defineProperty(data, key, {
enumerable: true, // 可枚舉
configurable: false, // 不能再define
get: function() {
if (Dep.target) {
dep.depend();
}
return val;
},
set: function(newVal) {
if (newVal === val) {
return;
}
val = newVal;
// 新的值是object的話,進行監聽
childObj = observe(newVal);
// 通知訂閱者
dep.notify();
}
});
}
複製代碼
//在compile.js文件裏面
compileElement: function(el) {
var childNodes = el.childNodes,
me = this;
[].slice.call(childNodes).forEach(function(node) {
var text = node.textContent;
var reg = /\{\{(.*)\}\}/;
if (me.isElementNode(node)) {
me.compile(node);
} else if (me.isTextNode(node) && reg.test(text)) {
me.compileText(node, RegExp.$1.trim());
}
if (node.childNodes && node.childNodes.length) {
me.compileElement(node);
}
});
},
複製代碼
針對咱們傳入的option對象的data字段,有一條較長的引用傳遞鏈:
咱們實例化傳入的option對象 => mvvm實例的this.$options => mvvm實例的this.$options.data => mvvm實例的this._data => observe(data, this) => observer實例的this.data
複製代碼
因此,到了最後,數據劫持的對象就是咱們實例化mvvm傳遞進去的data對象。
在compile.js文件中將真實容器節點的全部子節點「拷貝」到DocumentFragment對象中去時,使用了appendChild()這個API。從而佐證了這條在DOM世界裏面的規則。
node2Fragment: function(el) {
var fragment = document.createDocumentFragment(),
child;
// 將原生節點拷貝到fragment
while (child = el.firstChild) {
fragment.appendChild(child);
}
return fragment;
},
複製代碼
在進行DOM操做的時候,咱們避免不了要跟類數組(有些人稱之爲僞數組)打交道。
compileElement: function(el) {
var childNodes = el.childNodes,
me = this;
[].slice.call(childNodes).forEach(function(node) {
// ......
});
},
複製代碼
compile: function(node) {
var nodeAttrs = node.attributes,
me = this;
[].slice.call(nodeAttrs).forEach(function(attr) {
// ......
});
},
複製代碼
不管是[].slice.call()仍是Array.prototype.slice.call()這種寫法,都是達到借用真數組方法的目的。不過,我的以爲,理論上說,後者會更好。由於,後者省去了沒必要要的屬性查找的次數,性能表現會更優。
若是真的如這個學習庫的做者所說的那樣(大部分的代碼是摘抄與vue的源碼),那麼我相信,我已經瞭解到了vue的核心原理了。至於後面那些疊加上來的,不太核心的特性,好比說:virtual DOM,componnet機制,各類擴展機制啊等等,我要深刻到vue真正的源碼去研究了。
整篇文章下來,幾乎10000字,是爲本身學習vue原理的階段性總結之用。若有錯誤,望不吝指出,萬般感激。
最後,謝謝閱讀。