[Vue源码分析] Virtual DOM
最近小組有個(gè)關(guān)于vue virtual dom的分享會(huì),提前準(zhǔn)備一下…
讀前須知:
本文章涉及源碼版本為Vue 2.5.2,文中涉及到源碼部分,解釋直接寫(xiě)在源碼中(中文部分為本人添加),截圖盡量放完整代碼,但由于截圖的大小限制,部分只能放關(guān)鍵截圖,建議結(jié)合源碼閱讀此文章。
附上尤雨溪大佬的github vue倉(cāng)庫(kù)地址:https://github.com/vuejs/vue
為什么使用virtual dom
做一件事一般都先問(wèn)問(wèn)為什么,那么為什么使用virtual dom?真正的 DOM 元素是非常龐大的,因?yàn)闉g覽器的標(biāo)準(zhǔn)把 DOM 設(shè)計(jì)的很復(fù)雜。如果頻繁地操作 DOM ,會(huì)產(chǎn)生一定的性能問(wèn)題。
舉個(gè)例子:創(chuàng)建一個(gè)header標(biāo)簽,并打印dom的描述信息:
可以看到,輸出的信息雖然挺多看不懂,但可以看得出來(lái)這是很龐大的一段內(nèi)容。
相比之下:Virtual DOM 用一個(gè)原生的 JS 對(duì)象去描述一個(gè) DOM 節(jié)點(diǎn),所以它比創(chuàng)建一個(gè) DOM 的代價(jià)要小很多。
virtual dom 的數(shù)據(jù)結(jié)構(gòu)
virtual dom的定義在src/core/vdom/vnode.js 中,是一個(gè)VNode 類,定義如圖:
virtual dom映射到真實(shí)的 DOM 實(shí)際上要經(jīng)歷 VNode 的 create、diff、patch 等過(guò)程。
virtual dom 的創(chuàng)建
VNode 的 create 是通過(guò)createElement 方法創(chuàng)建的,源碼位于src/core/vdom/create-elemenet.js中:
這個(gè)方式實(shí)際調(diào)用的是_createElement()方法,此方法同樣位于src/core/vdom/create-elemenet.js,為了方便閱讀,關(guān)鍵代碼解釋已經(jīng)寫(xiě)在源碼中,如圖
VNode children的VNode化
VNode化的意思是什么?Virtual DOM 實(shí)際上是一個(gè)樹(shù)狀結(jié)構(gòu),每一個(gè) VNode 可能會(huì)有若干個(gè)子節(jié)點(diǎn),這些子節(jié)點(diǎn)應(yīng)該也是 VNode 的類型,由于上邊傳入的children是any類型的,因此需要將children轉(zhuǎn)為VNode類型。
這個(gè)過(guò)程用到兩個(gè)方法:normalizeChildren(children)、simpleNormalizeChildren(children)
這兩個(gè)方法的定義在src/core/vdom/helpers/normalzie-children.js中:
當(dāng)一個(gè) childrean 包含組件的時(shí)候,由于 functional component 函數(shù)式組件返回的是一個(gè)數(shù)組而不是一個(gè)根節(jié)點(diǎn),所以需要通過(guò)Array.prototype.concat方法把整個(gè) children 轉(zhuǎn)化為深度只有一層。
如果children是數(shù)組類型,normalizeChildren方法實(shí)際上調(diào)用的是normalizeArrayChildren方法,該方法在同文件下,源碼中已經(jīng)添加關(guān)鍵代碼解釋,如下:
至此,我們知道了Virtual DOM是通過(guò)createElement方法創(chuàng)建的,但是createElement方法是什么時(shí)候調(diào)用的呢?
接下來(lái)我們將從主線上分析模板和數(shù)據(jù)如何渲染成最終的DOM。
new Vue()發(fā)生了那些事
我們都知道,vue項(xiàng)目中的入口是src下的main.js,如圖:
new Vue()發(fā)生了什么?接下來(lái)我們看一下Vue源碼,源碼位于src/core/instance/index.js中
可以看到,Vue實(shí)際上是一個(gè)類,源碼中判斷了this instanceof Vue,用于限制此類只能通過(guò)new關(guān)鍵字初始化,這個(gè)類看起來(lái)很簡(jiǎn)單,只是調(diào)用了一個(gè)this._init()方法,這個(gè)方法做了什么?this_init()方法在src/core/instance/init.js中定義,主要是做了一堆初始化操作,關(guān)鍵源碼已添加中文注釋:
可以看到this_init()方法在做了一堆初始化操作后調(diào)用的是vm.$mount()方法,這個(gè)方法時(shí)怎么掛載實(shí)例的?接下來(lái)看一下這個(gè)方法的源碼,此方法的定義位于src/platform/web/entry-runtime-with-compiler.js中,關(guān)鍵代碼已經(jīng)添加中文注釋:
逗了一圈,發(fā)現(xiàn)這個(gè)方法只是對(duì)options做了一些規(guī)范化的操作,最后調(diào)用的還是最初緩存下來(lái)的Vue原型上的$mount方法,那么這個(gè)方法是什么時(shí)候定義的?查找一下,發(fā)現(xiàn)此方法位于:src/platform/web/runtime/index.js:
以下為$mount中使用到的query方法的源碼,源碼位于src/platform/web/util/index.js中:
最后$mount方法調(diào)用的是mountComponent方法,此方法在src/core/instance/lifecycle.js中定義,關(guān)鍵代碼注釋已經(jīng)添加到源碼中:
從上邊源碼可以看出,mountComponent最核心的方法有三個(gè):vm._render()、vm._update()、new Watcher,Watcher暫時(shí)不作介紹。
接下來(lái)分析一下vm._render(),此方法在src/core/instance/render.js中定義,如下:
可以看到,這個(gè)方法返回的是虛擬dom,而虛擬dom的創(chuàng)建是調(diào)用vm.$createElement,vm.$createElement是什么?
可以看到,它正是文章開(kāi)頭介紹到的createElement方法,createElement方法就是這時(shí)候調(diào)用的,兜了一大圈,終于解決了之前提到的createElement方法什么時(shí)候調(diào)用的問(wèn)題。
vm._update()又是什么?
上邊介紹mountComponent時(shí)提到過(guò),vm.update()的作用是把創(chuàng)建好的虛擬dom渲染成真實(shí)的dom,vm._update()源碼位于:src/core/instance/lifecycle.js,如圖:
從源碼中可以看到,_update方法在首次渲染和更新時(shí)都調(diào)用了vm.__patch__方法,只是傳入?yún)?shù)不一樣,可以想象得到vm.__patch__會(huì)有兩套邏輯,在這里先分析首次渲染時(shí)vm.__patch__做了什么。
vm.__patch__源碼位于src/platforms/web/runtime/index.js,如下:
patch應(yīng)該就是要將虛擬dom轉(zhuǎn)換為真實(shí)dom的函數(shù),但是noop是什么?從上邊的導(dǎo)入路徑可以得知此方法地址位于src/shared/util.js,如下:
可見(jiàn),noop是一個(gè)空函數(shù),也就是當(dāng)前如果不是瀏覽器環(huán)境的話,vm.__patch__將會(huì)是一個(gè)空函數(shù)。可以得知,在非瀏覽器渲染(服務(wù)端渲染)中,不需要把虛擬dom轉(zhuǎn)換為真實(shí)dom。
接下來(lái),重點(diǎn)看一下patch方法,源碼位于src/platforms/web/runtime/patch.js中:
這個(gè)函數(shù)的定義很簡(jiǎn)單,就一句代碼,接受的是一個(gè)Function類型的返回值,該值由createPatchFunction返回,createPatchFunction接受一個(gè)對(duì)象,這個(gè)對(duì)象包含nodeOps以及modules兩個(gè)模塊。
createPatchFunction源碼位于src/core/vdom/patch.js中,這個(gè)方法很復(fù)雜,但總的來(lái)說(shuō)就是定義了一系列的輔助方法,輔助方法那么多,不可能一開(kāi)始就全部看一遍,應(yīng)該先想想應(yīng)該怎么看。把輔助方法都收縮起來(lái),會(huì)發(fā)現(xiàn)這個(gè)函數(shù)最后會(huì)返回一個(gè)方法——patch,我們可以從這個(gè)方法開(kāi)始閱讀父方法的源碼,從而跟蹤用到了哪些輔助方法,做了些什么。
接著看一些patch函數(shù)的具體內(nèi)容,解釋請(qǐng)看源碼中的中文注釋:
接上圖:
做完了上邊的一波操作之后,調(diào)用createElm方法,這個(gè)方法位于同文件中,也就是輔助方法,解釋請(qǐng)看源碼中的中文注釋:
createChildren方法位于同文件中,是一個(gè)遍歷虛擬節(jié)點(diǎn)的操作:
最后會(huì)調(diào)用insert方法將dom插入父節(jié)點(diǎn)中。
insert方法中nodeOps.insertBefore以及nodeOps.appendChild實(shí)際調(diào)用的是原生的dom操作函數(shù),源碼位于:web/runtime/node-ops.js
至此,從new Vue()到真實(shí)dom掛載的整個(gè)過(guò)程主線分析完畢,最后上個(gè)圖。
總結(jié)
以上是生活随笔為你收集整理的[Vue源码分析] Virtual DOM的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 米游社app怎么看原神抽卡记录
- 下一篇: 使用mpvue开发微信小程序——原生微信