最近利用空閒時間又翻看了一遍Vue的源碼,只不過此次不一樣的是看了Flow版本的源碼。說來慚愧,最先看的第一遍時對Flow不瞭解,所以閱讀的是打包以後的vue文件,你們能夠想象這過程的痛苦,沒有類型的支持,看代碼時摸索了很長時間,因此咱們此次對Vue源碼的剖析是Flow版本的源碼,也就是從Github上下載下來的源碼中src目錄下的代碼。不過,在分析以前,我想先說說閱讀Vue源碼所須要的一些知識點,掌握這些知識點以後,相信再閱讀源碼會較爲輕鬆。javascript
我我的認爲要想深刻理解Vue的源碼,至少須要如下知識點: html
下面我們一一介紹前端
相信你們都知道,javascript是弱類型的語言,在寫代碼灰常爽的同時也十分容易犯錯誤,因此Facebook搞了這麼一個類型檢查工具,能夠加入類型的限制,提升代碼質量,舉個例子:vue
function sum(a, b) {
return a + b;
}
複製代碼
但是這樣,咱們若是這麼調用這個函數sum('a', 1) 甚至sum(1, [1,2,3])這麼調用,執行時會獲得一些你想不到的結果,這樣編程未免太不穩定了。那咱們看看用了Flow以後的結果:java
function sum(a: number, b:number) {
return a + b;
}
複製代碼
咱們能夠看到多了一個number的限制,標明對a和b只能傳遞數字類型的,不然的話用Flow工具檢測會報錯。其實這裏你們可能有疑問,這麼寫仍是js嗎? 瀏覽器還能認識執行嗎?固然不認識了,因此須要翻譯或者說編譯。其實如今前端技術發展太快了,各類插件層出不窮--Babel、Typescript等等,其實都是將一種更好的寫法編譯成瀏覽器認識的javascript代碼(咱們之前都是寫瀏覽器認識的javascript代碼的)。咱們繼續說Flow的事情,在Vue源碼中其實出現的Flow語法都比較好懂,好比下面這個函數的定義:node
export function renderList (
val: any,
render: (
val: any,
keyOrIndex: string | number,
index?: number
) => VNode
): ?Array<VNode>{
...
}
複製代碼
val是any表明能夠傳入的類型是任何類型, keyOrIndex是string|number類型,表明要不是string類型,要不是number,不能是別的;index?:number這個咱們想一想正則表達式中?的含義---0個或者1個,這裏其實意義也是一致的,可是要注意?的位置是在冒號以前仍是冒號以後--由於這兩種可能性都有,上面代碼中問號是跟在冒號前面,表明index能夠不傳,可是傳的話必定要傳入數字類型;若是問號是在冒號後面的話,則表明這個參數必需要傳遞,可是能夠是數字類型也能夠是空。這樣是否是頓時感受嚴謹多了?同時,代碼意義更明確了。爲啥這麼說呢? 以前看打包後的vue源碼,其中看到觀察者模式實現時因爲沒有類型十分難看懂,可是看了這個Flow版本的源碼,感受容易懂。 固然,若是想學習Flow更多的細節, 能夠看看下面這個學習文檔: Flow學習資料python
Vue中的組件相信你們都使用過,而且組件之中能夠有子組件,那麼這裏就涉及到父子組件了。組件其實初始化過程都是同樣的,顯然有些方法是能夠繼承的。Vue代碼中是使用原型繼承的方式實現父子組件共享初始化代碼的。因此,要看懂這裏,須要瞭解js中原型的概念;這裏很少談,只是提供幾個學習資料供你們參考: 廖雪峯js教程 js原型理解 1.3 Object.defineProperty 這個方法在js中十分強大,Vue正是使用了它實現了響應式數據功能。咱們先瞄一眼Vue中定義響應式數據的代碼:react
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
.....
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const 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) {
const 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 (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify()
}
})
}
複製代碼
其中咱們看到Object.defineProperty這個函數的運用,其中第一個參數表明要設置的對象,第二個參數表明要設置的對象的鍵值,第三個參數是一個配置對象,對象裏面能夠設置參數以下: value: 對應key的值,無需多言 configurable:是否能夠刪除該key或者從新配置該key enumerable:是否能夠遍歷該key writable:是否能夠修改該key get: 獲取該key值時調用的函數 set: 設置該key值時調用的函數 咱們經過一個例子來了解一下這些屬性:程序員
let x = {}
x['name'] = 'vue'
console.log(Object.getOwnPropertyDescriptor(x,'name'))
Object.getOwnPropertyDescriptor能夠獲取對象某個key的描述對象,打印結果以下:
{
value: "vue",
writable: true,
enumerable: true,
configurable: true
}
複製代碼
從上可知,該key對應的屬性咱們能夠改寫(writable:true),能夠從新設置或者刪除(configurable: true),同時能夠遍歷(enumerable:true)。那麼讓咱們修改一下這些屬性,好比configurable,代碼以下:正則表達式
Object.defineProperty(x, 'name', {
configurable: false
})
複製代碼
執行成功以後,若是你再想刪除該屬性,好比delete x['name'],你會發現返回爲false,即沒法刪除了。 那enumerable是什麼意思呢?來個例子就明白了,代碼以下:
let x = {}
x[1] = 2
x[2] = 4
Object.defineProperty(x, 2, {
enumerable: false
})
for(let key in x){
console.log("key:" + key + "|value:" + x[key])
}
複製代碼
結果以下: key:1|value:2 爲何呢? 由於咱們把2設置爲不可遍歷了,那麼咱們的for循環就取不到了,固然咱們仍是能夠用x[2]去取到2對應的值得,只是for循環中取不到而已。這個有什麼用呢?Vue源碼中Observer類中有下面一行代碼: def(value, 'ob', this);
這裏def是個工具函數,目的是想給value添加一個key爲__ob__,值爲this,可是爲何不直接 value.ob = this 反而要大費周章呢? 由於程序下面要遍歷value對其子內容進行遞歸設置,若是直接用value.__ob__這種方式,在遍歷時又會取到形成,這顯然不是本意,因此def函數是利用Object.defineProperty給value添加的屬性,同時enumerable設置爲false。 至於get和set嘛?這個就更強大了,相似於在獲取對象值和設置對象值時加了一個代理,在這個代理函數中能夠作的東西你就能夠想象了,好比設置值時再通知一下View視圖作更新。也來個例子體會一下吧: let x = {} Object.defineProperty(x, 1, { get: function(){ console.log("getter called!") }, set: function(newVal){ console.log("setter called! newVal is:" + newVal) } })
當咱們訪問x[1]時便會打印getter called,當咱們設置x[1] = 2時,打印setter called。Vue源碼正是經過這種方式實現了訪問屬性時收集依賴,設置屬性時源碼有一句dep.notify,裏面即是通知視圖更新的相關操做。
Vnode,顧名思義,Virtual node,虛擬節點,首先聲明,這不是Vue本身獨創的概念,其實Github上早就有一個相似的項目:Snabbdom。我我的認爲,Vue應該也參考過這個庫的實現,由於這個庫包含了完整的Vnode以及dom diff算法,甚至實現的具體代碼上感受Vue和這個庫也是有點相像的。爲啥要用Vnode呢?其實緣由主要是原生的dom節點對象太大了,咱們運行一下代碼:
let dom = document.createElement('div');
for(let key in dom){
console.log(key)
}
複製代碼
打印的結果灰常長!!!說明這個dom對象節點有點重量級,而咱們的html網頁常常數以百計個這種dom節點,若是採用以前的Jquery這種方式直接操做dom,性能上確實稍微low一點。因此snabbdom或者Vue中應用了Vnode,Vnode對象啥樣呢? 看看Vue源碼對Vnode的定義:
export default class VNode {
tag: string | void;
data: VNodeData | void;
children: ?Array<VNode>;
text: string | void;
elm: Node | void;
ns: string | void;
context: Component | void; // rendered in this component's scope key: string | number | void; componentOptions: VNodeComponentOptions | void; componentInstance: Component | void; // component instance parent: VNode | void; // component placeholder node // strictly internal raw: boolean; // contains raw HTML? (server only) isStatic: boolean; // hoisted static node isRootInsert: boolean; // necessary for enter transition check isComment: boolean; // empty comment placeholder? isCloned: boolean; // is a cloned node? isOnce: boolean; // is a v-once node? asyncFactory: Function | void; // async component factory function asyncMeta: Object | void; isAsyncPlaceholder: boolean; ssrContext: Object | void; fnContext: Component | void; // real context vm for functional nodes fnOptions: ?ComponentOptions; // for SSR caching fnScopeId: ?string; .... } 複製代碼
相比之下, Vnode對象的屬性確實少了不少;其實光屬性少也不見得性能就能高到哪兒去,另外一個方面即是針對新舊Vnode的diff算法了。這裏其實有一個現象:其實大多數場景下即使有不少修改,可是若是從宏觀角度觀看,其實修改的點很少。舉個例子: 好比有如下三個dom節點A B C 咱們的操做中依次會改爲 B C D 若是採用Jquery的改法,當碰到第一次A改成B時,修改了一次,再碰到B改成C,又修改了一次,再次碰到C改成D,又又修改了一次,顯然其實從宏觀上看,只須要刪除A,而後末尾加上D便可,修改次數獲得減小;可是這種優化是有前提的,也就是說可以從宏觀角度看才行。之前Jquery的修改方法在碰到第一次修改的時候,須要把A改成B,這時代碼尚未執行到後面,它是不可能知道後面的修改的,也就是沒法以全局視角看問題。因此從全局看問題的方式就是異步,先把修改放到隊列中,而後整成一批去修改,作diff,這個時候從統計學意義上來說確實能夠優化性能。這也是爲啥Vue源碼中出現下述代碼的緣由: queueWatcher(this);
函數柯里化是什麼鬼呢?其實就是將多參數的函數化做多個部分函數去調用。舉個例子:
function getSum(a,b){
return a+b;
}
複製代碼
這是個兩個參數的函數,能夠直接getSum(1,2)調用拿到結果;然而,有時候並不會兩個參數都能肯定,只想先傳一個值,另一個在其餘時間點再傳入,那咱們把函數改成:
function getSum(a){
return function(b){
return a+b;
}
}
複製代碼
那咱們如何調用這個柯里化以後的函數呢?
let f = getSum(2)
console.log(f(3))
console.log(getSum(2)(3)) //結果同上
複製代碼
可見,柯里化的效果即是以前必須同時傳入兩個參數才能調用成功而如今兩個參數能夠在不一樣時間點傳入。那爲毛要這麼作嘛?Vue源碼是這麼應用這個特性的,Vue源碼中有一個platform目錄,專門存放和平臺相關的源碼(Vue能夠在多平臺上運行 好比Weex)。那這些源碼中確定有些操做是和平臺相關的,好比會有些如下僞代碼所表示的邏輯: if(平臺A){ .... }else if(平臺B){ .... }
但是若是這麼寫會有個小不舒服的地方,那就是其實代碼運行時第一次走到這裏根據當前平臺就已經知道走哪個分支了,而如今這麼寫必當致使代碼再次運行到這裏的時候還會進行平臺判斷,這樣總感受會多一些無聊的多餘判斷,所以Vue解決此問題的方式就是應用了函數柯里化技巧,相似聲明瞭如下一個函數: function ...(平臺相關參數){ return function(平臺不相關參數){ 處理邏輯 } }
在Vue的patch以及編譯環節都應用了這種方式,講到那部分代碼時咱們再細緻的看,讀者提早先了解一下能夠幫助理解Vue的設計。
可能有的讀者第一次聽到這兩個詞,實際上這個和js的事件循環機制息息相關。在上面咱們也提到,Vue更新不是數據一改立刻同步更新視圖的,這樣確定會有性能問題,好比在一個事件處理函數裏先this.data = A 而後再this.data=B,若是要渲染兩次,想一想都感受很low。Vue源碼其實是將更改都放入到隊列中,同一個watcher不會重複(不理解這些概念沒關係,後面源碼會重點介紹),而後異步處理更新邏輯。在實現異步的方式時,js實際提供了兩種task--Macrotask與Microtask。兩種task有什麼區別呢?先從一個例子講起:
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
Promise.resolve().then(function() {
console.log('promise3');
}).then(function() {
console.log('promise4');
});
}, 0);
Promise.resolve().then(function() {
console.log('promise1');
}).then(function() {
console.log('promise2');
});
console.log('script end');
複製代碼
以上代碼運行結果是什麼呢?讀者能夠思考一下,答案應該是:
script start
script end
promise1
promise2
setTimeout
promise3
promise4
複製代碼
簡單能夠這麼理解,js事件循環中有兩個隊列,一個叫MacroTask,一個MircroTask,看名字就知道Macro是大的,Micro是小的(想一想宏觀經濟學和微觀經濟學的翻譯)。那麼大任務隊列跑大任務--好比主流程程序了、事件處理函數了、setTimeout了等等,小任務隊列跑小任務,目前讀者記住一個就能夠--Promise。js老是先從大任務隊列拿一個執行,而後再把全部小任務隊列所有執行再循環往復。以上面示例程序,首先總體上個這個程序是一個大任務先執行,執行完畢後要執行全部小任務,Promise就是小任務,因此又打印出promise1和promise2,而setTimeout是大任務,因此執行完全部小任務以後,再取一個大任務執行,就是setTimeout,這裏面又往小任務隊列扔了一個Promise,因此等setTimeout執行完畢以後,又去執行全部小任務隊列,因此最後是promise3和promise4。說的有點繞,把上面示例程序拷貝到瀏覽器執行一下多思考一下就明白了,關鍵是要知道上面程序自己也是一個大任務。必定要理解了以後再去看Vue源碼,不然不會理解Vue中的nextTick函數。 推薦幾篇文章吧(我都認真讀完了,受益不淺) Macrotask Vs Microtask 理解js中Macrotask和Microtask 阮一峯 Eventloop理解
不少程序員比較懼怕遞歸,可是遞歸真的是一種灰常灰常強大的算法。Vue源碼中大量使用了遞歸算法--好比dom diff算法、ast的優化、目標代碼的生成等等....不少不少。並且這些遞歸不只僅是A->A這麼簡單,大多數源碼中的遞歸是A->B->C...->A等等這種複雜遞歸調用。好比Vue中經典的dom diff算法:
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isUndef(oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx]; // Vnode has been moved left
} else if (isUndef(oldEndVnode)) {
oldEndVnode = oldCh[--oldEndIdx];
} else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);
oldStartVnode = oldCh[++oldStartIdx];
newStartVnode = newCh[++newStartIdx];
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);
oldEndVnode = oldCh[--oldEndIdx];
newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm));
oldStartVnode = oldCh[++oldStartIdx];
newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm);
oldEndVnode = oldCh[--oldEndIdx];
newStartVnode = newCh[++newStartIdx];
} else {
if (isUndef(oldKeyToIdx)) { oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx); }
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx);
if (isUndef(idxInOld)) { // New element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx);
} else {
vnodeToMove = oldCh[idxInOld];
if (sameVnode(vnodeToMove, newStartVnode)) {
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue);
oldCh[idxInOld] = undefined;
canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm);
} else {
// same key but different element. treat as new element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx);
}
}
newStartVnode = newCh[++newStartIdx];
}
複製代碼
上面代碼是比較新舊Vnode節點更新孩子節點的部分源碼,調用者是patchVnode函數,咱們發現這部分函數中又會調用會patchVnode,調用鏈條爲:patchVnode->updateChildren->patchVnode。同時,即使沒有直接應用遞歸,在將模板編譯成AST(抽象語法樹)的過程當中,其使用了棧去模擬了遞歸的思想,因而可知遞歸算法的重要性。這也難怪,畢竟無論是真實dom仍是vnode,其實本質都是樹狀結構,原本就是遞歸定義的東西。咱們也會單獨拿出一篇文章講講遞歸,好比用遞歸實現一下JSON串的解析。但願讀者注意查看。
這恐怕比遞歸更讓某些程序員蛋疼,可是我相信只要讀者認真把Vue這部分代碼看懂,絕對比看N遍編譯原理的課本更能管用。咱們看看Vue源碼這裏的實現:
const ast = parse(template.trim(), options)
if (options.optimize !== false) {
optimize(ast, options)
}
const code = generate(ast, options)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
複製代碼
上述代碼首先經過parse函數將template編譯爲抽象語法樹ast,而後對ast進行代碼優化,最後生成render函數。其實這個過程就是翻譯,好比gcc把c語言翻譯爲彙編、又好比Babel把ES6翻譯爲ES5等等,這裏面的流程十分都是十分地類似。Vue也玩了這麼一把,把模板html編譯爲render函數,什麼意思呢?
<li v-for="record in commits">
<span class="date">{{record.commit.author.date}}</span>
</li>
複製代碼
好比上面的html,你以爲瀏覽器會認識嘛?顯然v-for不是html原生的屬性,上述代碼若是直接在瀏覽器運行,你會發現{{record.commit.author.date}}就直接展現出來了,v-for也沒有起做用,固然仍是會出現html裏面(畢竟html容錯性很高的);可是通過Vue的編譯系統一編譯生成一些函數,這些函數一執行就是瀏覽器認識的html元素了,神奇吧? 其實僅僅是應用了編譯原理課本的部分知識罷了,這部分咱們後面會灰常灰常詳細的介紹源碼,只要跟着看下來,一定會對編譯過程有所理解。如今能夠這麼簡單理解一下AST(抽象語法樹),好比java能夠寫一個if判斷,C語言也能夠寫,js、python等等也能夠(以下所示): java: if(x > 5){ .... }
python: if x>5: ....
雖然從語法形式上寫法不太一致,可是抽象出共同點其實都是一個if語句跟着一個x>5 的條件, 綜上,Vue源碼其實代碼行數並非不少,可是其簡約凝練的風格深深吸引了我。我會重點分析Vue源碼中觀察者模式的實現、Vnode以及dom diff算法的實現以及模板編譯爲render函數的實現。這三者我感受就是Vue源碼中最精彩的地方,但願你我均可以從中汲取營養,不斷提升!