摘要:圖在中應用三數(shù)據(jù)渲染過程數(shù)據(jù)綁定實現(xiàn)邏輯本節(jié)正式分析從到數(shù)據(jù)渲染到頁面的過程,在中定義了一個的構(gòu)造函數(shù)。
一、概述 vue已是目前國內(nèi)前端web端三分天下之一,也是工作中主要技術(shù)棧之一。在日常使用中知其然也好奇著所以然,因此嘗試閱讀vue源碼并進行總結(jié)。本文旨在梳理初始化頁面時data中的數(shù)據(jù)是如何渲染到頁面上的。本文將帶著這個疑問一點點“追究”vue的"思路"??傮w來說vue模版渲染大致流程如圖1所示:
圖1:vue模版渲染流程
從圖中可以看到模版渲染過程經(jīng)歷了數(shù)據(jù)處理(initState)、模版編譯(compileToFunctions)生成渲染函數(shù)(render)、render函數(shù)生成虛擬dom、虛擬dom映射為真實DOM (patch)掛載到頁面這幾個過程。上述幾個函數(shù)在數(shù)據(jù)渲染過程中起到了關(guān)鍵作用。因此本文就從這幾個函數(shù)出發(fā),深入研究vue數(shù)據(jù)渲染到頁面上的原理。
二、什么是Virtual DOM "); vue利用虛擬DOM技術(shù)來提高頁面渲染和更新的速度。因此在正式分析數(shù)據(jù)渲染過程之前,有必要先了解一下什么是Virtual DOM,以及Virtual DOM的優(yōu)勢。 2.1 virtual dom 產(chǎn)生的原因 Virtual DOM 產(chǎn)生的前提是瀏覽器中的 DOM操作 是很“昂貴"的,為了更直觀的感受,我把一個簡單的 div 元素的屬性都打印出來,如圖2所示:
圖2:dom元素屬性
可以看到,瀏覽器把 DOM 設(shè)計的非常復雜、非常龐大。在瀏覽器當中,dom的實現(xiàn)和ECMAScript的實現(xiàn)是分離的。因此當我們頻繁的去做 DOM 更新,就是頻繁通過js代碼調(diào)用dom的接口,就相當于兩個相互獨立的模塊發(fā)生了交互。這樣,相比于在同一個模塊當中互相調(diào)用,這種跨模塊的調(diào)用它的性能損耗是非常高的。并且dom操作導致瀏覽器的重繪(repaint)和重排(reflow)會帶來更大的性能損耗。只要在渲染過程中進行一次 DOM 更新,整個渲染流程都會重做一遍。如圖3所示:
圖3:瀏覽器渲染流程
而 Virtual DOM 就是用一個原生的 JS 對象去描述一個 DOM 節(jié)點,所以它比創(chuàng)建一個 DOM 的代價要小很多。圖4所示為vitrual dom結(jié)構(gòu):
圖4:Virtual DOM實例
上述的virtual dom最后會生成真實的dom結(jié)構(gòu)。如圖5所示:
圖5:Virtual DOM映射成真實dom
在 Vue.js 中,Virtual DOM 是用 VNode 這么一個 Class 去描述, 是對真實 DOM 的一種抽象描述,它的核心定義無非就幾個關(guān)鍵屬性,標簽名、數(shù)據(jù)、子節(jié)點、鍵值等。由于 VNode 只是用來映射到真實 DOM 的渲染,不需要包含操作 DOM 的方法,因此它是非常輕量和簡單的。當數(shù)據(jù)發(fā)生改變時是一次性渲染到頁面,同時vue內(nèi)部通過diff算法減少頁面的重繪和重排,從而提高了頁面渲染的速度。 2.2 Virtual DOM 主要思想 VirtualDOM的主要思想就是模擬DOM的樹狀結(jié)構(gòu),在內(nèi)存中創(chuàng)建保存映射DOM信息的節(jié)點數(shù)據(jù),在由于交互等因素需要視圖更新時,先通過對節(jié)點數(shù)據(jù)進行diff得到差異結(jié)果后,再一次性對DOM進行批量更新操作,這就好比在內(nèi)存中創(chuàng)建了一個平行世界,瀏覽器中DOM樹的每一個節(jié)點與屬性數(shù)據(jù)都在這個平行世界中存在著另一個版本的虛擬DOM樹,所有復雜曲折的更新邏輯都在平行世界中的VirtualDOM處理完成,只將最終的更新結(jié)果發(fā)送給瀏覽器中的DOM樹執(zhí)行,這樣就避免了冗余瑣碎的DOM樹操作負擔,進而有效提高了性能?;?Virtual DOM 的數(shù)據(jù)更新與UI同步機制:初始渲染時,首先將數(shù)據(jù)渲染為 Virtual DOM,然后由 Virtual DOM 生成 DOM。
圖6:vitual dom生成dom示意圖
比較兩個 DOM 樹的差異是 Virtual DOM 算法最核心的部分,這也是所謂的 Virtual DOM 的diff 算法。diff算法的核心是比較只會在同層級進行, 不會跨層級比較。而不像逐層逐層搜索遍歷的方式,時間復雜度將會達到 O(n^3)的級別,代價非常高,而只比較同層級的方式時間復雜度可以降低到O(n)??梢杂靡粡垐D示意:
圖7:virtual dom更新示意圖
數(shù)據(jù)更新時,渲染得到新的 Virtual DOM,與上一次的 Virtual DOM 進行 diff,得到所有需要在 DOM 上進行的變更,然后在 patch 過程中應用到 DOM 上實現(xiàn)UI的同步更新。因此 Virtual DOM算法主要包括這幾步: 初始化視圖的時候,用原生JS對象表示DOM樹,生成一個對象樹,然后根據(jù)這個對象樹來生成一個真正的DOM樹,插入到文檔中。 當狀態(tài)更新的時候,重新生成一個對象樹,將新舊兩個對象樹做對比,記錄差異。 把記錄的差異應用到第一步生成的真正的DOM樹上,視圖更新完成。 在vue中也是采用了virtual dom的diff算法如下圖,具體diff算法過程在patch函數(shù)執(zhí)行。
圖8:vitrual dom在vue中應用
三、數(shù)據(jù)渲染過程 3.1 數(shù)據(jù)綁定實現(xiàn)邏輯 ---- initState 本節(jié)正式分析從new vue()到數(shù)據(jù)渲染到頁面的過程,在src/core/instance/index.js 中定義了一個Vue的構(gòu)造函數(shù)。當執(zhí)行new Vue(options)時就會執(zhí)行this._init(options)這個函數(shù)。
圖9: vue構(gòu)造函數(shù)
以一個簡單的實例開始。定義如下模版和js代碼:
圖10: 實例
在調(diào)用Vue構(gòu)造函數(shù)時候傳入el和data。此時傳入this._init(options)中的options = { el: "#app", data: { message: "我是一條信息"}}。在_init函數(shù)中會執(zhí)行一系列初始化操作:初始化生命周期、初始化事件、初始化數(shù)據(jù)等。其中初始化數(shù)據(jù)是本節(jié)關(guān)心的內(nèi)容,跟數(shù)據(jù)綁定關(guān)聯(lián)最大的是 initState。因此我們現(xiàn)在重點研究一下initState(vm)。入口是src/core/instance/state.js。
圖11: initState函數(shù)
傳入data后會調(diào)用initData(vm)函數(shù),對data進行處理。initData函數(shù)將當前傳入的data賦值給vm._data。vm是當前vue實例。然后會執(zhí)行代理函數(shù)proxy。
圖12: proxy函數(shù)
proxy函數(shù)的原理是通過 Object.defineProperty()函數(shù)在實例對象vm上定義與data數(shù)據(jù)字段同名的訪問器屬性,并且這些屬性代理的值是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內(nèi)部使用的。這也就是我們通過this.message就能訪問到data里面的message對應的值。即"我是一條信息‘。
圖13: 數(shù)據(jù)綁定過程
同理,當我們設(shè)置一個屬性值時會通過set方法去設(shè)置vm._data[key]的值。到這一步我們已經(jīng)可以獲取傳入的data里面的數(shù)據(jù)了。那么message是如何渲染到頁面視圖層的呢?下一節(jié)就深入研究vue的掛載過程。 3.2 渲染函數(shù) ---- render 3.2.1 對el的處理 _init函數(shù)執(zhí)行完上述的初始化過程后會判斷是否傳入el,若傳入就執(zhí)行掛載函數(shù)$mount。
圖14: $mount函數(shù)
$mount首先會通過query(el)函數(shù)對傳入的el如下處理:
圖15: query函數(shù)
如果el是字符串則通過document.querySelector(el)方法查找該字符串對應的dom元素。若沒找到,則通過方document.createElement("div")方法動態(tài)創(chuàng)建一個div,若傳入的el是dom元素。那么就返回該元素。最終都是用一個dom元素來掛載實例。值得注意的是Vue 不能掛載在 body、html 這樣的根節(jié)點上。原因是vue在掛載是會將對應的dom對象替換成新的div,但body和html是不適合替換的。如果el是body或者html就會拋出警告,這也就是為什么平時我們通常會用“#app“或者“div“掛載實例的原因。 3.2.2 模版內(nèi)容提取 判斷是否傳入render函數(shù),如果渲染函數(shù)存在會直接調(diào)用運行時版 $mount 函數(shù),我們知道運行時版 $mount 僅有兩句代碼,且真正的掛載是通過調(diào)用 mountComponent 函數(shù)完成的,所以可想而知 mountComponent 完成掛載所需的必要條件就是:提供渲染函數(shù)給 mountComponent。 render分為用戶手寫和模版編譯兩種形式,手寫render函數(shù)格式如下。render函數(shù)的好處是,不會有在html中直接使用插值時,在實際掛載前出現(xiàn){{message}}這樣的內(nèi)容。只有在render函數(shù)執(zhí)行完成后才會把message替換到頁面上去。這樣會有更好的體驗。下面的render函數(shù)最終會渲染成一個id為app1,內(nèi)容為‘我是一條信息"的div元素,替換掉之前的掛載節(jié)點el。所以這也是為什么不使用body或者html進行掛載的原因,因為我們不能覆蓋到整個body或者html。
圖16: 手寫render函數(shù)
在 Vue 2.0 版本中,所有 Vue 的組件的渲染最終都需要 render 方法,無論我們是用單文件 .vue 方式開發(fā)組件,還是寫了 el 或者 template 屬性,最終都會轉(zhuǎn)換成 render 方法。我們的例子中沒有傳入render函數(shù),因此需要來研究一下在沒有傳入render函數(shù)的情況下如何通過模版編譯成render函數(shù)。
圖17: template處理
(1)如果存在template并且傳入的template是字符串,且以#開頭,如下:
圖18: template第一種形式
那么就找到id為#app的元素innerHtml();獲取該innerHtml()是在idToTemplate函數(shù)中進行的。也是調(diào)用query函數(shù)獲取對應的dom元素的innerHTML。并保存在template變量中。
圖19: idToTemplate函數(shù)
(2)如果傳入的template是dom節(jié)點。如下:那么直接將該節(jié)點的innerHtml賦值給template 變量。
圖20: template第二種形式
(3)如果options中沒有template,但是有el,那么就獲取el對應元素的所有內(nèi)容。
圖21: el元素內(nèi)容提取
getOuterHTML 函數(shù)的源碼如下:
圖22: getOuterHTML函數(shù)
它接收一個 DOM 元素作為參數(shù),并返回該元素的 outerHTML。該函數(shù)首先判斷了 el.outerHTML 是否存在,也就是說一個元素的 outerHTML 屬性未必存在,實際上在 IE9-11 中 SVG 標簽元素是沒有 innerHTML 和 outerHTML 這兩個屬性的,解決這個問題的方案很簡單,可以把 SVG 元素放到一個新創(chuàng)建的 div 元素中,這樣新 div 元素的 innerHTML 屬性的值就等價于 SVG標簽 outerHTML 的值。我們最初提供的實例子符合第(3)類情況。經(jīng)過以上邏輯的處理之后,理想狀態(tài)下此時 template 變量應該如下所示的一個模板字符串,將用于渲染函數(shù)的生成。
圖22: 提取的模版字符串
模版提取過程如圖23所示:
圖23: 模版提取
3.2.3 模版編譯成render函數(shù) 拿到模版內(nèi)容后就會調(diào)用compileToFunctions函數(shù)將模版編譯成render函數(shù)。下面看下compileToFunctions生成render方法的具體實現(xiàn)。編譯主要有三個過程:
圖23: 編譯過程
1.解析模版字符串生成AST ---- parse(template.trim(), options)。 parse 會用正則等方式解析 template模板中的指令、class、style等數(shù)據(jù),形成AST樹。AST是一種用Javascript對象的形式來描述整個模版。parse會調(diào)用parseHTML函數(shù),由于 parseHTML 的邏輯也非常復雜,因此我也用了偽代碼的方式表達。
圖24: parseHTML偽代碼
整體來說它的邏輯就是循環(huán)解析 template ,用正則做各種匹配,對于不同情況分別進行不同的處理,直到整個 template 被解析完畢。 在匹配的過程中會利用 advance 函數(shù)不斷前進整個模板字符串,直到字符串末尾。
圖25: advance函數(shù)
為了更加直觀地說明 advance 的作用,可以通過一副圖表示:調(diào)用 advance 函數(shù):advance(4) 得到結(jié)果:
圖26: advance函數(shù)執(zhí)行示意圖
所以在整個html循環(huán)中會不斷調(diào)用advance函數(shù),達到把這個html解析完畢的目的。詳細過程有興趣的小伙伴自行去了解。那么至此,parse 的過程就分析完了,看似復雜,但我們可以拋開細節(jié)理清它的整體流程。
圖27: parse流程圖
parse 的目標是把 template 模板字符串轉(zhuǎn)換成 AST 樹,它是一種用 JavaScript 對象的形式來描述整個模板。那么整個 parse 的過程是利用正則表達式順序解析模板,當解析到開始標簽、閉合標簽、文本的時候都會分別執(zhí)行對應的回調(diào)函數(shù),來達到構(gòu)造 AST 樹的目的。個人理解就是把template(模板)解析成一個對象,該對象是包含這個模板所以信息的一種數(shù)據(jù),而這種數(shù)據(jù)瀏覽器是不支持的,為Vue后面的處理template提供基礎(chǔ)數(shù)據(jù)。本實例中會生成如下AST樹。
圖28: ast樹形結(jié)構(gòu)
2.優(yōu)化AST語法樹 ---- optimize(ast, options)。 為什么此處會有優(yōu)化過程?我們知道Vue是數(shù)據(jù)驅(qū)動,是響應式的,但是template模版中并不是所有的數(shù)據(jù)都是響應式的,也有許多數(shù)據(jù)是初始化渲染之后就不會有變化的,那么這部分數(shù)據(jù)對應的DOM也不會發(fā)生變化。后面有一個 update 更新界面的過程,在這當中會有一個 patch 的過程, diff 算法會直接跳過靜態(tài)節(jié)點,從而減少了比較的過程,優(yōu)化了 patch 的性能。
圖29: optimize流程
3.codegen:將優(yōu)化后的AST樹轉(zhuǎn)換成可執(zhí)行的代碼。
圖30: codegen流程
template模版經(jīng)歷過parse->optimize->codegen三個過程之后,就可以得到render function函數(shù)了。
圖31: 編譯后生成的render函數(shù)
從模版提取到render函數(shù)的生成的過程總結(jié)如下:
圖32: 編譯render函數(shù)的過程
3.3 render到VNode的生成 調(diào)用 render.call(vm._renderProxy, vm.$createElement)函數(shù)并返回生成的虛擬節(jié)點(vnode)??梢钥吹?,render 函數(shù)中 createElement 方法就是 vm.$createElement 方法。
圖33: initRender函數(shù)
vm.$createElement 方法定義是在執(zhí)行 initRender 方法的時候,可以看到除了 vm.$createElement 方法,還有一個 vm._c 方法,它是被模板編譯成的 render 函數(shù)使用,而 vm.$createElement 是用戶手寫 render 方法使用的,這倆個方法支持的參數(shù)相同,并且內(nèi)部都調(diào)用了 createElement 方法。
圖34: createElement函數(shù)
createElement 方法實際上是對 _createElement 方法的封裝,它允許傳入的參數(shù)更加靈活,在處理這些參數(shù)后,調(diào)用真正創(chuàng)建 VNode 的函數(shù) _crateElement 。_createElement最終實例化VNode,返回vnode或者一個空的vnode。
圖35: vnode實例化
本文例子生成如下的虛擬dom:
圖36:生成的vnode
簡單的梳理了createElement函數(shù)流程圖,可以參考下圖:
圖37:createElement函數(shù)流程圖
3.3虛擬DOM映射為真實DOM ----patch vm._render 函數(shù)的作用是生成的虛擬節(jié)點(vnode)。vm._update 函數(shù)的作用是把 vm._render 函數(shù)生成的虛擬節(jié)點渲染成真正的 DOM。
圖38:_update函數(shù)
update方法會在兩種情況下被調(diào)用,一是new Vue初始化的時候,還有一種情況就是當我們改變data數(shù)據(jù),頁面重新渲染時調(diào)用。update最終會調(diào)用patch方法。patch實際調(diào)用的是createPatchFunction({ nodeOps, modules })。這個方法接收兩個參數(shù),nodeOps,modules。摘取一部分nodeOps內(nèi)容
圖39:nodeOps
可以看到,里面都是一些原生dom操作的封裝,摘取modules一部分內(nèi)容。
圖40:modules
可以看到,是一些對原生dom特性控制的封裝,以及一些輔助函數(shù), 下面我們回到createPatchFunction,createPatchFunction 方法中首先定義了好多的輔助函數(shù),最后返回了一個函數(shù),即patch,來看下這個patch。在該函數(shù)中,第一個參數(shù)是dom元素,第二個參數(shù)是vnode。
圖41:patch函數(shù)
一系列判斷過后會執(zhí)行emptyNodeAt()輔助函數(shù),可以看到,emptyNodeAt()函數(shù)的功能是創(chuàng)建一個新的vnode。因此oldVnode = emptyNodeAt(oldNode)創(chuàng)新了新的vnode替換,而原來的oldNode(dom節(jié)點)可以在該vnode節(jié)點的elm元素中訪問到。
圖42:patch函數(shù)
取到當前的el對應的dom節(jié)點和其父節(jié)點后,開始利用createElm函數(shù)創(chuàng)建新的dom節(jié)點。在最初的實例中,oldElm就是id為app的div。而parentElm就是其父元素,即body。接下來調(diào)用createElm方法,這個方法定義于傳入的modules輔助函數(shù)中, 這個方法才是真實dom操作的核心所在,它的作用就是將vnode掛載到真實的dom上。我們進入createElm。
圖43:createElm函數(shù)
在createElm()函數(shù)中,主要完成的功能是將構(gòu)建dom子節(jié)點插入到父節(jié)點中,并且一直循環(huán)到該節(jié)點沒有子節(jié)點為止。這個過程createElm()函數(shù)和createChildren函數(shù)一起完成。創(chuàng)建子節(jié)點
圖44:創(chuàng)建子節(jié)點
可以看到,在createChildren()函數(shù)中,如果該vnode的子節(jié)點是矩陣的話,就會調(diào)用createElm()函數(shù)。因此兩個函數(shù)是相互調(diào)用生成dom節(jié)點,然后插入到父節(jié)點的過程。如果該子節(jié)點是最后一個節(jié)點,則直接在dom節(jié)點后面插入該文本節(jié)點。最后調(diào)用insert將整個dom樹一次性插入到body中。上面從主線上完成模版和數(shù)據(jù)的渲染。
圖45:插入節(jié)點
此外,因為新建了一個div來渲染視圖,因此應該把原來的就定義的用來掛載的dom節(jié)點(一般是個div)刪掉。所以,可以看到,在vue的渲染過程中,會創(chuàng)建新的dom節(jié)點替換掉以前的節(jié)點,因此我們在初始化的時候不能將節(jié)點選擇掛載在html和body上。
圖46:刪除原節(jié)點
四、總結(jié) 回過頭來看,數(shù)據(jù)的渲染邏輯并不是特別復雜,核心關(guān)鍵的幾步流程還是非常清晰的:new Vue,執(zhí)行初始化,將傳入的data數(shù)據(jù)綁定到當前實例,就可以通過this.message的形式訪問傳入的數(shù)據(jù)。這個過程是執(zhí)行initData()函數(shù)完成的。
掛載$mount方法,通過自定義Render方法、template、el等生成Render函數(shù)。如果傳入了模版(template)就將模版里面的內(nèi)容編譯成render函數(shù),否則將傳入的el對應的元素的內(nèi)容編譯成render函數(shù)。編譯是調(diào)用compileToFunctions函數(shù)完成的。也可以自己手寫render函數(shù),可以減少編譯這一環(huán)節(jié)。其中render渲染函數(shù)的優(yōu)先級最高,template次之且需編譯成渲染函數(shù),而掛載點el屬性對應的元素若存在,則在前兩者均不存在時,其outerHTML才會用于編譯與渲染。
生成render函數(shù)后,調(diào)用_createElement函數(shù)生成vnode。
將虛擬DOM映射為真實DOM頁面上。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/6775.html
摘要:哪吒別人的看法都是狗屁,你是誰只有你自己說了才算,這是爹教我的道理。哪吒去他個鳥命我命由我,不由天是魔是仙,我自己決定哪吒白白搭上一條人命,你傻不傻敖丙不傻誰和你做朋友太乙真人人是否能夠改變命運,我不曉得。我只曉得,不認命是哪吒的命。 showImg(https://segmentfault.com/img/bVbwiGL?w=900&h=378); 出處 查看github最新的Vue...
摘要:而組件在創(chuàng)建時,又怎么會去調(diào)用呢這是由于將自身作為一個插件安裝到了,通過注冊了一個鉤子函數(shù),從而在之后所有的組件創(chuàng)建時都會調(diào)用該鉤子函數(shù),給了檢查是否有參數(shù),從而進行初始化的機會。 vue-router 是 Vue.js 官方的路由庫,本著學習的目的,我對 vue-router 的源碼進行了閱讀和分析,分享出來給其他感興趣的同學做個參考吧。 參考 源碼:vuejs/vue-route...
摘要:五六月份推薦集合查看最新的請點擊集前端最近很火的框架資源定時更新,歡迎一下。蘇幕遮燎沈香宋周邦彥燎沈香,消溽暑。鳥雀呼晴,侵曉窺檐語。葉上初陽乾宿雨,水面清圓,一一風荷舉。家住吳門,久作長安旅。五月漁郎相憶否。小楫輕舟,夢入芙蓉浦。 五、六月份推薦集合 查看github最新的Vue weekly;請::點擊::集web前端最近很火的vue2框架資源;定時更新,歡迎 Star 一下。 蘇...
摘要:五六月份推薦集合查看最新的請點擊集前端最近很火的框架資源定時更新,歡迎一下。蘇幕遮燎沈香宋周邦彥燎沈香,消溽暑。鳥雀呼晴,侵曉窺檐語。葉上初陽乾宿雨,水面清圓,一一風荷舉。家住吳門,久作長安旅。五月漁郎相憶否。小楫輕舟,夢入芙蓉浦。 五、六月份推薦集合 查看github最新的Vue weekly;請::點擊::集web前端最近很火的vue2框架資源;定時更新,歡迎 Star 一下。 蘇...
閱讀 3765·2021-11-22 13:52
閱讀 3633·2019-12-27 12:20
閱讀 2401·2019-08-30 15:55
閱讀 2156·2019-08-30 15:44
閱讀 2274·2019-08-30 13:16
閱讀 589·2019-08-28 18:19
閱讀 1903·2019-08-26 11:58
閱讀 3450·2019-08-26 11:47