最近公司面試了一些中高級前端,因爲公司技術棧以 Vue 爲主,而對於中高級前端,必不可少要問及 Vue 源碼的問題。不少面試者,對於源碼只能簡單講到響應式是基於 Object.defineProperty
或者 Proxy
等老生常談的基礎概念。Vue 通過這麼多年的發展,成了不少前端開發者職業生涯不可或缺的一個框架。誠然,每一個人均可以在短期學習一個框架的使用,可是要深刻閱讀它的源碼確實不是一件容易的事。這裏面有不少因素,除了業務開發繁忙外,面對一個複雜龐大的代碼庫,以及衆多平時不常用的構建工具和新的編程語言等干擾因素,咱們時常不知道該從哪裏切入。爲了應付面試,只能經過一些面經文章和博客,快速得到一些基本的認知,但一旦面試官深刻拷問,真正看過源碼仍是隻看過文章,就水落石出。真正讀懂源碼不是靠一場突擊戰就能作到的,而是像澆花種樹同樣,日積月累,反覆刻意的練習和回顧,到最後甚至能夠本身寫出一個框架,纔算真正掌握。既然是一場持久戰,咱們就不能期望在短期內把整個框架一口吃進去,而是將其分割成一個個小的技術點,一次消化一個單一技術點,連點成線,最後就能吃下整個框架。本文以及接下來一系列文章,嘗試將 Vue 源碼拆分紅獨立的技術點,並動手編碼實現。html
雖然,絕大多數開發者,職業生涯幾乎不會參與到一個框架的開發,更不用說開發一個成功的被普遍使用的框架。可是,咱們不妨假設,開發一個框架和開發一個業務產品的基本邏輯是同樣的,就是首先,咱們須要產品需求分析,而後將需求拆分紅不一樣子模塊,分別開發各個子模塊後,再集成到一塊兒組成一個完整的系統。前端
開發一個框架也應如此。vue
首先,需求分析,咱們應該先問本身,這個框架要提供的核心功能是什麼;其次,要實現這些功能,咱們須要實現哪些技術點;最後,如何將這些分離的技術點組合複用成一個完整知足需求的框架。node
按照這個邏輯,那麼,Vue 的核心功能是什麼?Vue2 爲例,建立一個最簡單的 Vue 應用的代碼以下:面試
<div id="app"></div> <script src="vue.js"></script> <script> var vm = new Vue( { data: { text: 'hello world!' }, render(h) { return h('div', this.text) } } ).$mount('#app') </script> 複製代碼
這段代碼,使用框架導出的一個構造函數 Vue
,傳入包含字段data
和render
的選項對象,建立一個 Vue
實例 vm
,並掛載到id
爲app
的dom
元素上。算法
這段代碼在瀏覽器運行後,能夠看到原來的dom
元素<div id="app"></div>
被替換成<div>hello world!</div>
, 並能夠在控制檯鍵入 vm.text = 'hello china!'
,能夠看到在實例的text
屬性改變後,對應的dom
元素的文本內容當即改變了。編程
這裏包含如下三個環節:數組
data
定義的字段(例如text
)被映射到Vue
實例的屬性中;render
函數傳入了一個函數h
,並用h
函數建立虛擬節點
,調用h
使用了 1. 中映射的屬性字段(this.text
);$mout
將render
返回的虛擬節點
渲染到真實dom
中;首先,咱們定義Vue
的構造函數,讀取選項對象的data
字段,遍歷data
的全部鍵值,並克隆到實例對象this
上。瀏覽器
function Vue(options) { var data = options.data var keys = Object.keys(data) for (var i = 0, l = keys.length; i < l; i++) { var key = keys[i] this[key] = data[key] } } 複製代碼
第二步,在 Vue
構造函數調用選項傳入的render
函數,經過call
將render
函數上下文對象this
指向Vue
實例,這樣render
函數內部能夠經過this
訪問實例的數據,也就是選項對象傳入的data
。markdown
var render = options.render this.vnode = render.call(this, createVNode) 複製代碼
這裏傳入的函數createVNode
也就是上文中的h
函數。createVNode
能夠接受3個參數。
返回一個VNode
對象,也就是一般我所說的虛擬DOM。要實現createVNode
函數,咱們須要先知道VNode
到底爲什麼物。所謂虛擬DOM,就是用一個普通的JS對象去建模真實的DOM,所以,直接修改虛擬DOM的屬性,不會觸發咱們在頁面可見DOM的改變,可是,它的結構是和真實DOM節點一一對應的。咱們知道在瀏覽器中,每個DOM節點都是一棵「樹」。做爲樹中一個節點,至少包含兩個部分,即節點數據和子節點。對應到DOM,一個節點自身的數據就是元素的標籤和屬性,子節點能夠包含任意多個,所以使用數組表示。createVNode
函數用於提供給應用構建視圖的虛擬節點樹,建立樹的過程由外部提供,所以自身不須要遞歸建立子節點,而是簡單接受參數,並根據參數傳入類型和數量來決定VNode對應屬性賦值。
目前,我須要的VNode的完整字段包含:
var vnode = { tag, data, children, text } 複製代碼
tag
爲元素標籤,data
爲屬性數據,當節點是葉子節點,沒有children
,那麼就用text
表示節點顯示的文本(事實上,文本在真實DOM中也是一個特殊的節點,它沒有tag,所以爲了處理方便,在虛擬節點中,children 中表示是有 tag 的元素節點)。
所以,createVNode
接受的參數與咱們返回的結果基本一致,僅僅對傳入的第2個參數進行判斷,若是是字符串,就認爲要建立的是一個只有文本的葉子節點,不然將第二個參數做爲節點屬性數據,第三個參數做爲子節點數組。
function createVNode(tag, data, children) { var vnode = { tag: tag, data: undefined, children: undefined, text: undefined } if (typeof data === 'string') { vnode.text = data } else { vnode.data = data if (Array.isArray(children)) { vnode.children = children } else { vnode.children = [ children ] } } return vnode } 複製代碼
因爲children
參數的存在,在外部,可使用createVNode
或h
建立一個節點樹,例如:
var vnode = createVNode('ul', {}, [ createVNode('li', {}, [ createVNode('span', 'text') ]), createVNode('li', {}, [ createVNode('span', 'text') ]) ]) 複製代碼
建立的虛擬節點樹,只是框架對應用視圖的內部表示,要得到真實可見的DOM,須要一個函數將VNode
轉換成真實DOM
。定義這個函數爲createElm
。這個函數除了將VNode
轉換成真實DOM元素,同時還將建立的DOM元素插入頁面中。插入的位置包含了兩個真實DOM元素,即插入元素的父節點,以及參考節點,參考節點是要替換的節點,是可選的,存在則插入到參考節點前面,並刪除參考節點,不存在則直接將新建立的節點(根據VNode建立的真實DOM節點)插入到父節點中。和createVNode
不一樣的是,createElm
接受的vnode
參數是一課樹,所以,須要使用遞歸遍歷整個VNode
樹,最後獲得實際也是一個真實DOM節點樹。
function createElm(vnode, parentElm, refElm) { var elm // 建立真實DOM節點 if (vnode.tag) { elm = document.createElement(vnode.tag) } else if (vnode.text) { elm = document.createTextNode(vnode.text) } // 將真實DOM節點插入到文檔中 if (refElm) { parentElm.insertBefore(elm, refElm) parentElm.removeChild(refElm) } else { parentElm.appendChild(elm) } // 遞歸建立子節點 if (Array.isArray(vnode.children)) { for (var i = 0, l = vnode.children.length; i < l; i++) { var childVNode = vnode.children[i] createElm(childVNode, elm) } } else if (vnode.text) { elm.textContent = vnode.text } return elm } 複製代碼
有了createElm
函數,實現$mount
方法的基本功能也就簡單了。
Vue.prototype.$mount = function (id) { var refElm = document.querySelector(id) var parentElm = refElm.parentNode createElm(this.vnode, parentElm, refElm) return this } 複製代碼
到此爲止,彷佛已經將前文建立簡單Vue應用用到的全部功能實現了一遍。接下來,咱們將代碼整合一下,保存到文件myvue.js
:
function Vue(options) { var data = options.data var keys = Object.keys(data) for (var i = 0, l = keys.length; i < l; i++) { var key = keys[i] this[key] = data[key] } var render = options.render this.vnode = render.call(this, createVNode) } function createVNode(tag, data, children) { var vnode = { tag: tag, data: undefined, children: undefined, text: undefined } if (typeof data === 'string') { vnode.text = data } else { vnode.data = data if (Array.isArray(children)) { vnode.children = children } else { vnode.children = [ children ] } } return vnode } function createElm(vnode, parentElm, refElm) { var elm // 建立真實DOM節點 if (vnode.tag) { elm = document.createElement(vnode.tag) } else if (vnode.text) { elm = document.createTextNode(vnode.text) } // 將真實DOM節點插入到文檔中 if (refElm) { parentElm.insertBefore(elm, refElm) parentElm.removeChild(refElm) } else { parentElm.appendChild(elm) } // 遞歸建立子節點 if (Array.isArray(vnode.children)) { for (var i = 0, l = vnode.children.length; i < l; i++) { var childVNode = vnode.children[i] createElm(childVNode, elm) } } else if (vnode.text) { elm.textContent = vnode.text } return elm } Vue.prototype.$mount = function (id) { var refElm = document.querySelector(id) var parentElm = refElm.parentNode createElm(this.vnode, parentElm, refElm) return this } 複製代碼
而後將html文件中的vue.js
改爲myvue.js
:
<div id="app"></div> <script src="myvue.js"></script> <script> var vm = new Vue( { data: { text: 'hello world!' }, render(h) { return h('div', this.text) } } ).$mount('#app') </script> 複製代碼
在瀏覽器打開html文件,能夠看到,結果與vue.js
顯示一致。爲了測試節點樹的渲染,咱們不妨修改一下選項對象:
{ data: { items: [ 'item1', 'item2', 'item3', ] }, render(h) { var children = this.items.map(item => h('li', item)) var vnode = h('ul', null, children) console.log(vnode) return vnode } } 複製代碼
眨一看,好像一切如咱們所料。它成功利用咱們傳入的數據和渲染函數,建立虛擬節點,而且掛載到真實DOM上。可是,目前來看它至少還缺乏兩個關鍵功能。
vm.text
)並不能觸發頁面的從新渲染,也就是沒有響應式;vnode
的diff
算法,實現只對發生改變的節點從新建立;別急,萬丈高樓平地起,正如本文開篇所講,咱們須要的是一場持久戰,而不是突擊戰。有了最小可用功能,後面就是在此基礎上作迭代和優化。感興趣的讀者,請關注後續系列更新。