Vue執(zhí)行流程及渲染示例解析
正文
最近想對之前看過的vue一些較原理的一些東西進行總結(jié),今天就談談vue實例創(chuàng)建到渲染的一個流程概述。說的不對希望可以補充評論。
相信絕大多數(shù)的前端小伙伴已記不清做了多少項目,寫了多少代碼了,每個人如同教科書般地寫著Vue代碼:
// 入口文件中的常見代碼 new Vue({ el: '#app', router: router, render: h => h(App) })
大家是否有想過Vue內(nèi)部是如何運轉(zhuǎn)的呢,做了哪些事情呢?怎么在界面中渲染處預期效果呢!接下來我們慢慢探究!
初始化
Vue的構(gòu)造函數(shù)
// Vue構(gòu)造函數(shù) function Vue (options) { if (process.env.NODE_ENV !== 'production' && !(this instanceof Vue) ) { warn('Vue is a constructor and should be called with the `new` keyword') } // 執(zhí)行初始化邏輯 this._init(options) }
通過上面的函數(shù)可以看出當我們執(zhí)行new Vue()的時候,只執(zhí)行了一個_init方法。_init會根據(jù)傳入的選項對vue進行初始化。我們初始化data的時候,vue會通過 Object.defineProperty 的方式將data的屬性定義到vue實例上。這也就解釋了為什么我們可以在vue中通過this.name進行賦值,可以修改data中name屬性的值了。
為了能實現(xiàn)的響應式動態(tài)變化數(shù)據(jù),vue又做了處理,創(chuàng)建一個observer對象,該對象與data綁定,通過 Object.defineProperty 將data中的所有的屬性轉(zhuǎn)換成getter/setter。當data中的屬性在vue實例中被訪問(會觸發(fā)getter),observer 對象就會把該屬性收集為watcher實例的依賴,之后當data中的屬性在vue實例中被改變(會觸發(fā)setter), observer 會通知依賴該屬性的 watcher 實例重新渲染頁面。
vue官網(wǎng)上的一張示意圖幫助大家再理解下這個處理過程:
上面我們分析了vue是如和做到數(shù)據(jù)更新的,接下來我們看看他是如何做到渲染界面的。首先,vue會把將我們編寫的HTML模板解析成一個AST描述對象,該對象是通過children和parent鏈接而成的樹形結(jié)構(gòu),完整地描述了HTML標簽的所有信息。
HTML模板
<div id="app"> <p>{{msg}}</p> </div>
最終會解析成下面這種AST對象
{ attrs: [{name: "id", value: ""app"", dynamic: undefined, start: 5, end: 13}], attrsList: [{name: "id", value: "app", start: 5, end: 13}], attrsMap: {id: "app"}, children: [{ attrsList: [], attrsMap: {}, children: [], end: 33, parent: {type: 1, tag: "div", ...}, plain: true, pre: undefined, rawAttrsMap:{}, start: 19 tag: "p", type: 1 }], end: 263, parent: undefined, plain: false, rawAttrsMap:{id: {name: "id", value: "app", start: 5, end: 13}}, start: 0 tag: "div", type: 1 }
然后 vue 根據(jù)AST對象生成 render 函數(shù),該函數(shù)的函數(shù)體大致如下:
with(this){ return _c('div', {attrs:{"id":"app"}}, [_c('p', [_v(_s(msg))])]) }
也就是說,我們的模板最終在vue內(nèi)部都是會以一個render函數(shù)的形式存在。
函數(shù) _c 是在初始化render環(huán)境的時候添加到vue實例上,用來創(chuàng)建 vnode 的全局實例方法。它可以通vue實例直接調(diào)用,主要是給vue內(nèi)部使用的vnode創(chuàng)建方法。
我們得到render函數(shù)之后,vue并未直接渲染成DOM樹,而是先通過render函數(shù)得到一個vnode。實際上這一步是非常有必要的,我們都知道頻繁大量地操作DOM節(jié)點是極耗性能的。vue在渲染之前通過對vnode的比較,可以大大規(guī)避非必要的DOM操作。下面是一個vnode大致結(jié)構(gòu):
{ tag: "div", // 元素標簽,如div children: [{tag: "p", ...}], // vnode 子節(jié)點數(shù)組 data: {attrs: {id: "app"}}, // 數(shù)據(jù)對象例如,{attrs: {id: 'app'}} elm: DOM節(jié)點(div#app),// 所對應的dom節(jié)點 parent: undefined, // 父節(jié)點vnode context: Vue實例, // 所對應的vue實例 ... }
方法 _v 也是vue實例方法,內(nèi)部用以創(chuàng)建文本類型的vnode,在本例中,{{msg}}是一個文本節(jié)點,所以需要使用 _v 來創(chuàng)建文本vnode。不過無論是文本類型的vnode還是非文本類型的vnode都是Vnode對象的實例。兩者的區(qū)別在于,文本類型的vnode不存在 tag 和 children。
// 創(chuàng)建一個文本類型的VNode function createTextVNode (val) { return new VNode(undefined, undefined, undefined, String(val)) }
方法 _s 同樣也是vue的實例方法,內(nèi)部用來將接收的參數(shù)變成字符串返回,對于字符串和數(shù)值使用 Object.toString() 轉(zhuǎn)換,如果接收到的是一個對象,則使用 JSON.stringify()轉(zhuǎn)換。
function toString (val){ return val == null ? '' : Array.isArray(val) || (isPlainObject(val) && val.toString === Object.prototype.toString) ? JSON.stringify(val, null, 2) : String(val) }
vnode 通過 parent 和 children 連接父節(jié)點和子節(jié)點,組成vnode樹。最后,vue根據(jù)diff之后的結(jié)果,執(zhí)行真正的dom節(jié)點的插入更新刪除等操作,同時觸發(fā)vue實例的生命周期鉤子函數(shù)。之后,vue要做的就是觀察數(shù)據(jù)的變化,進而決定是否重新渲染頁面了。
繼續(xù)分析vue是如何進行渲染的
創(chuàng)建DOM節(jié)點
有了vnode后,vue還需要根據(jù)vnode來創(chuàng)建DOM節(jié)點。如果是首次渲染,那么vue會走創(chuàng)建的邏輯。如果是數(shù)據(jù)的更新導致的重新渲染,那么vue會走更新的邏輯。
首次渲染
因為是首次渲染,所以不存在先前老的vnode,因此無需進行比較。vue直接調(diào)用 createElm 方法創(chuàng)建DOM元素。具體的創(chuàng)建步驟如下:
1.首先為vnode創(chuàng)建DOM元素。
2.如果vnode有子節(jié)點,逐個為其子節(jié)點創(chuàng)建DOM元素,并將子DOM元素插入到vnode的DOM元素上。
3.調(diào)用setAttribute 為vnode的DOM元素添加屬性。
4.將vnode的DOM元素插入到其父元素上。
重新渲染
如果不是首次渲染,而是由數(shù)據(jù)變化所觸發(fā)的重新渲染,那么vue會最大限度地復用已創(chuàng)建的DOM元素。而復用的前提就是通過比較新老vnode,找出需要更新的內(nèi)容,然后最小限度地進行替換。這也是vue設計vnode的核心用途。vue源碼中可以看到(此處先忽略),當新老vnode完全相等的情況下,vue不會對該節(jié)點重新渲染,直接跳過了。
如果新vnode發(fā)生了變化,那么vue會遵循以下步驟更新DOM元素:
1.更新DOM元素的屬性。
這個在首次渲染那部分提到了一些。vue內(nèi)實現(xiàn)了若干個屬性處理模塊,專門用于DOM元素屬性的創(chuàng)建和更新。這些模塊中基本都實現(xiàn)了create、update這兩個處理函數(shù)。create 負責DOM元素屬性的創(chuàng)建,update 負責DOM元素屬性的更新。cbs.update[i](oldVnode, vnode) 的意思就是逐個調(diào)用這些模塊上的 update 方法,以更新發(fā)生改變的DOM元素屬性。
2.更新DOM元素的子元素。關于DOM子元素的更新分為幾種情況
- 如果新老vnode的子節(jié)點都是文本節(jié)點且文本內(nèi)容不同,處理方式更新DOM元素的textContent屬性值。
- 如果新老vnode的子節(jié)點都是非文本節(jié)點,需要調(diào)用 updateChildren 遞歸地去更新子節(jié)點。
- 如果新vnode的子節(jié)點是非文本節(jié)點,而老vnode的子節(jié)點是文本節(jié)點,需要清除DOM元素的文本,并創(chuàng)建子vnode的DOM元素插入到其父節(jié)點的DOM元素上。
- 如果新vnode的子節(jié)點不存在,但老vnode的子節(jié)點存在,那么調(diào)用 removeVnode 刪除老vnode的子節(jié)點對應的DOM元素。
- 如果老vnode的子節(jié)點是文本節(jié)點,而新vnode的子節(jié)點不存在,則清空老DOM元素的文本。
大量的DOM操作會極損耗瀏覽器性能。vue在每次數(shù)據(jù)發(fā)生變化后,都會重新生成vnode節(jié)點。通過比較新老vnode節(jié)點,找出需要進行操作的最小DOM元素子集。根據(jù)變化點,進行DOM元素屬性、DOM子節(jié)點的更新。這種設計方式大大減少了DOM操作的次數(shù)
這次文章大部分都是看一些博客文章所了解的內(nèi)容,基本上可以了解vue如何創(chuàng)建和如何渲染界面,還是老話好記性不如爛筆頭 自己做了一些總結(jié) 可以加深理解!
更多關于Vue執(zhí)行流程及渲染解析的資料請關注腳本之家其它相關文章!
相關文章
Vue3 Element-plus el-menu無限級菜單組件封裝過程
對于element中提供給我們的el-menu組件最多可以實現(xiàn)三層嵌套,如果多一層數(shù)據(jù)只能自己通過變量去加一層,如果加了兩層、三層這種往往是行不通的,所以只能進行封裝,這篇文章主要介紹了Vue3 Element-plus el-menu無限級菜單組件封裝,需要的朋友可以參考下2023-04-04使用proxytable 配置解決 vue-cli 的跨域請求問題【推薦】
這篇文章主要介紹了利用 proxytable 配置解決 vue-cli 的跨域請求問題,本文的目錄結(jié)構(gòu)基于 webpack 模板結(jié)構(gòu),需要的朋友可以參考下2018-05-05