圖1:vue模版渲染流程html
從圖中能夠看到模版渲染過程經歷了數據處理(initState)、模版編譯(compileToFunctions)生成渲染函數(render)、render函數生成虛擬dom、虛擬dom映射爲真實DOM (patch)掛載到頁面這幾個過程。上述幾個函數在數據渲染過程當中起到了關鍵做用。所以本文就從這幾個函數出發,深刻研究vue數據渲染到頁面上的原理。前端
圖2:dom元素屬性vue
能夠看到,瀏覽器把 DOM 設計的很是複雜、很是龐大。在瀏覽器當中,dom的實現和ECMAScript的實現是分離的。所以當咱們頻繁的去作 DOM 更新,就是頻繁經過js代碼調用dom的接口,就至關於兩個相互獨立的模塊發生了交互。這樣,相比於在同一個模塊當中互相調用,這種跨模塊的調用它的性能損耗是很是高的。而且dom操做致使瀏覽器的重繪(repaint)和重排(reflow)會帶來更大的性能損耗。只要在渲染過程當中進行一次 DOM 更新,整個渲染流程都會重作一遍。如圖3所示:圖3:瀏覽器渲染流程node
而 Virtual DOM 就是用一個原生的 JS 對象去描述一個 DOM 節點,因此它比建立一個 DOM 的代價要小不少。圖4所示爲vitrual dom結構:圖4:Virtual DOM實例web
上述的virtual dom最後會生成真實的dom結構。如圖5所示:圖5:Virtual DOM映射成真實dom正則表達式
在 Vue.js 中,Virtual DOM 是用 VNode 這麼一個 Class 去描述, 是對真實 DOM 的一種抽象描述,它的核心定義無非就幾個關鍵屬性,標籤名、數據、子節點、鍵值等。因爲 VNode 只是用來映射到真實 DOM 的渲染,不須要包含操做 DOM 的方法,所以它是很是輕量和簡單的。當數據發生改變時是一次性渲染到頁面,同時vue內部經過diff算法減小頁面的重繪和重排,從而提升了頁面渲染的速度。圖6:vitual dom生成dom示意圖算法
比較兩個 DOM 樹的差別是 Virtual DOM 算法最核心的部分,這也是所謂的 Virtual DOM 的diff 算法。diff算法的核心是比較只會在同層級進行, 不會跨層級比較。而不像逐層逐層搜索遍歷的方式,時間複雜度將會達到 O(n^3)的級別,代價很是高,而只比較同層級的方式時間複雜度能夠下降到O(n)。能夠用一張圖示意:圖7:virtual dom更新示意圖瀏覽器
數據更新時,渲染獲得新的 Virtual DOM,與上一次的 Virtual DOM 進行 diff,獲得全部須要在 DOM 上進行的變動,而後在 patch 過程當中應用到 DOM 上實現UI的同步更新。所以 Virtual DOM算法主要包括這幾步: 初始化視圖的時候,用原生JS對象表示DOM樹,生成一個對象樹,而後根據這個對象樹來生成一個真正的DOM樹,插入到文檔中。 當狀態更新的時候,從新生成一個對象樹,將新舊兩個對象樹作對比,記錄差別。 把記錄的差別應用到第一步生成的真正的DOM樹上,視圖更新完成。 在vue中也是採用了virtual dom的diff算法以下圖,具體diff算法過程在patch函數執行。圖8:vitrual dom在vue中應用app
圖9: vue構造函數dom
以一個簡單的實例開始。定義以下模版和js代碼:圖10: 實例
在調用Vue構造函數時候傳入el和data。此時傳入this._init(options)中的options = { el: '#app', data: { message: '我是一條信息'}}。在_init函數中會執行一系列初始化操做:初始化生命週期、初始化事件、初始化數據等。其中初始化數據是本節關心的內容,跟數據綁定關聯最大的是 initState。所以咱們如今重點研究一下initState(vm)。入口是src/core/instance/state.js。圖11: initState函數
傳入data後會調用initData(vm)函數,對data進行處理。initData函數將當前傳入的data賦值給vm._data。vm是當前vue實例。而後會執行代理函數proxy。圖12: proxy函數
proxy函數的原理是經過 Object.defineProperty()函數在實例對象vm上定義與data數據字段同名的訪問器屬性,而且這些屬性代理的值是vm._data上對應屬性的值。當咱們訪問vm[key] 就會經過get方法去訪問vm[sourceKey][key] 即vm._data[key]。也就是說vm.message 就會去訪問vm._data.message也就是vm.data.message。因此 this.message就是this._data.message,只不過_data是vue內部使用的。這也就是咱們經過this.message就能訪問到data裏面的message對應的值。即'我是一條信息‘。圖13: 數據綁定過程
同理,當咱們設置一個屬性值時會經過set方法去設置vm._data[key]的值。到這一步咱們已經能夠獲取傳入的data裏面的數據了。那麼message是如何渲染到頁面視圖層的呢?下一節就深刻研究vue的掛載過程。圖14: $mount函數
$mount首先會經過query(el)函數對傳入的el以下處理:圖15: query函數
若是el是字符串則經過document.querySelector(el)方法查找該字符串對應的dom元素。若沒找到,則經過方document.createElement('div')方法動態建立一個div,若傳入的el是dom元素。那麼就返回該元素。最終都是用一個dom元素來掛載實例。值得注意的是Vue 不能掛載在 body、html 這樣的根節點上。緣由是vue在掛載是會將對應的dom對象替換成新的div,但body和html是不適合替換的。若是el是body或者html就會拋出警告,這也就是爲何平時咱們一般會用「#app「或者「div「掛載實例的緣由。圖16: 手寫render函數
在 Vue 2.0 版本中,全部 Vue 的組件的渲染最終都須要 render 方法,不管咱們是用單文件 .vue 方式開發組件,仍是寫了 el 或者 template 屬性,最終都會轉換成 render 方法。咱們的例子中沒有傳入render函數,所以須要來研究一下在沒有傳入render函數的狀況下如何經過模版編譯成render函數。圖17: template處理
(1)若是存在template而且傳入的template是字符串,且以#開頭,以下:圖18: template第一種形式
那麼就找到id爲#app的元素innerHtml();獲取該innerHtml()是在idToTemplate函數中進行的。也是調用query函數獲取對應的dom元素的innerHTML。並保存在template變量中。圖19: idToTemplate函數
(2)若是傳入的template是dom節點。以下:那麼直接將該節點的innerHtml賦值給template 變量。圖20: template第二種形式
(3)若是options中沒有template,可是有el,那麼就獲取el對應元素的全部內容。圖21: el元素內容提取
getOuterHTML 函數的源碼以下:圖22: getOuterHTML函數
它接收一個 DOM 元素做爲參數,並返回該元素的 outerHTML。該函數首先判斷了 el.outerHTML 是否存在,也就是說一個元素的 outerHTML 屬性未必存在,實際上在 IE9-11 中 SVG 標籤元素是沒有 innerHTML 和 outerHTML 這兩個屬性的,解決這個問題的方案很簡單,能夠把 SVG 元素放到一個新建立的 div 元素中,這樣新 div 元素的 innerHTML 屬性的值就等價於 SVG標籤 outerHTML 的值。咱們最初提供的實例子符合第(3)類狀況。通過以上邏輯的處理以後,理想狀態下此時 template 變量應該以下所示的一個模板字符串,將用於渲染函數的生成。
圖22: 提取的模版字符串
模版提取過程如圖23所示:圖23: 模版提取
圖23: 編譯過程
1.解析模版字符串生成AST ---- parse(template.trim(), options)。 parse 會用正則等方式解析 template模板中的指令、class、style等數據,造成AST樹。AST是一種用Javascript對象的形式來描述整個模版。parse會調用parseHTML函數,因爲 parseHTML 的邏輯也很是複雜,所以我也用了僞代碼的方式表達。圖24: parseHTML僞代碼
總體來講它的邏輯就是循環解析 template ,用正則作各類匹配,對於不一樣狀況分別進行不一樣的處理,直到整個 template 被解析完畢。 在匹配的過程當中會利用 advance 函數不斷前進整個模板字符串,直到字符串末尾。圖25: advance函數
爲了更加直觀地說明 advance 的做用,能夠經過一副圖表示:圖26: advance函數執行示意圖
因此在整個html循環中會不斷調用advance函數,達到把這個html解析完畢的目的。詳細過程有興趣的小夥伴自行去了解。那麼至此,parse 的過程就分析完了,看似複雜,但咱們能夠拋開細節理清它的總體流程。圖27: parse流程圖
parse 的目標是把 template 模板字符串轉換成 AST 樹,它是一種用 JavaScript 對象的形式來描述整個模板。那麼整個 parse 的過程是利用正則表達式順序解析模板,當解析到開始標籤、閉合標籤、文本的時候都會分別執行對應的回調函數,來達到構造 AST 樹的目的。我的理解就是把template(模板)解析成一個對象,該對象是包含這個模板因此信息的一種數據,而這種數據瀏覽器是不支持的,爲Vue後面的處理template提供基礎數據。本實例中會生成以下AST樹。圖28: ast樹形結構
2.優化AST語法樹 ---- optimize(ast, options)。 爲何此處會有優化過程?咱們知道Vue是數據驅動,是響應式的,可是template模版中並非全部的數據都是響應式的,也有許多數據是初始化渲染以後就不會有變化的,那麼這部分數據對應的DOM也不會發生變化。後面有一個 update 更新界面的過程,在這當中會有一個 patch 的過程, diff 算法會直接跳過靜態節點,從而減小了比較的過程,優化了 patch 的性能。圖29: optimize流程
3.codegen:將優化後的AST樹轉換成可執行的代碼。圖30: codegen流程
template模版經歷過parse->optimize->codegen三個過程以後,就能夠獲得render function函數了。圖31: 編譯後生成的render函數
從模版提取到render函數的生成的過程總結以下:圖32: 編譯render函數的過程
圖33: initRender函數
vm.$createElement 方法定義是在執行 initRender 方法的時候,能夠看到除了 vm.$createElement 方法,還有一個 vm._c 方法,它是被模板編譯成的 render 函數使用,而 vm.$createElement 是用戶手寫 render 方法使用的,這倆個方法支持的參數相同,而且內部都調用了 createElement 方法。圖34: createElement函數
createElement 方法其實是對 _createElement 方法的封裝,它容許傳入的參數更加靈活,在處理這些參數後,調用真正建立 VNode 的函數 _crateElement 。_createElement最終實例化VNode,返回vnode或者一個空的vnode。圖35: vnode實例化
本文例子生成以下的虛擬dom:圖36:生成的vnode
簡單的梳理了createElement函數流程圖,能夠參考下圖:圖37:createElement函數流程圖
圖38:_update函數
update方法會在兩種狀況下被調用,一是new Vue初始化的時候,還有一種狀況就是當咱們改變data數據,頁面從新渲染時調用。update最終會調用patch方法。patch實際調用的是createPatchFunction({ nodeOps, modules })。這個方法接收兩個參數,nodeOps,modules。摘取一部分nodeOps內容圖39:nodeOps
能夠看到,裏面都是一些原生dom操做的封裝,摘取modules一部份內容。圖40:modules
能夠看到,是一些對原生dom特性控制的封裝,以及一些輔助函數, 下面咱們回到createPatchFunction,createPatchFunction 方法中首先定義了好多的輔助函數,最後返回了一個函數,即patch,來看下這個patch。在該函數中,第一個參數是dom元素,第二個參數是vnode。圖41:patch函數
一系列判斷事後會執行emptyNodeAt()輔助函數,能夠看到,emptyNodeAt()函數的功能是建立一個新的vnode。所以oldVnode = emptyNodeAt(oldNode)創新了新的vnode替換,而原來的oldNode(dom節點)能夠在該vnode節點的elm元素中訪問到。圖42:patch函數
取到當前的el對應的dom節點和其父節點後,開始利用createElm函數建立新的dom節點。在最初的實例中,oldElm就是id爲app的div。而parentElm就是其父元素,即body。接下來調用createElm方法,這個方法定義於傳入的modules輔助函數中, 這個方法纔是真實dom操做的核心所在,它的做用就是將vnode掛載到真實的dom上。咱們進入createElm。圖43:createElm函數
在createElm()函數中,主要完成的功能是將構建dom子節點插入到父節點中,而且一直循環到該節點沒有子節點爲止。這個過程createElm()函數和createChildren函數一塊兒完成。圖44:建立子節點
能夠看到,在createChildren()函數中,若是該vnode的子節點是矩陣的話,就會調用createElm()函數。所以兩個函數是相互調用生成dom節點,而後插入到父節點的過程。若是該子節點是最後一個節點,則直接在dom節點後面插入該文本節點。最後調用insert將整個dom樹一次性插入到body中。上面從主線上完成模版和數據的渲染。圖45:插入節點
此外,由於新建了一個div來渲染視圖,所以應該把原來的就定義的用來掛載的dom節點(通常是個div)刪掉。因此,能夠看到,在vue的渲染過程當中,會建立新的dom節點替換掉之前的節點,所以咱們在初始化的時候不能將節點選擇掛載在html和body上。圖46:刪除原節點