vue 2.0 路由切換以及組件緩存源代碼重點難點分析
關於vue 2.0源代碼分析,已經有不少文檔分析功能代碼段比如watcher,history,vnode等,但沒有一個是分析重點難點的,沒有一個是分析大命題的,比如執行router.push之後到底是如何執行代碼實現路由切換的?
本文旨在分享本人研究vue 2.0源代碼重點難點之結果,不涉及每段源代碼具體分析,源代碼功能段每個人都可以去分析,只要有耐心,再參考已有高手發表的源代碼分析文檔,不是太難,主要是要克服一些編程技術問題,比如嵌套回調,遞歸,對象/數組特殊處理方法等等。
希望本文對那些有興趣研究vue源代碼但遇到困難無解的網友有幫助,研究源代碼是個人興趣個人的事情,需要自己去debug跟蹤研究,只是看別人的研究文檔並不代表自己真的懂多少,有多少提高,所以
本文不涉及從頭到尾每段源代碼的具體的分析,那個自己有興趣去看看研究研究即可,不是太難,只要對js對象編程技術有一定理解就可以。
首先要說的是,vue 2.0的復雜性和難點都是由於采用vnode技術引起的,如果不采用vnode技術,像1.0那樣,就沒有這些復雜性和難點。
順帶提一下vue 1.0,Vue 1.0最大的迷惑是組件數據變化時如何處觸發頁面更新?
答案在:
var watcher = new Watcher(vm, expOrFn, cb, options);
以及:
function defineReactive(obj,key,val,customSetter) {
var dep = new Dep(); //每個屬性建立一套dep,會復制/引用保存到set/get方法中與屬性一起存在
Object.defineProperty(obj, key, {
get: function reactiveGetter () { //創建watcher時會訪問執行屬性的get方法獲取表達式的值!!!
if (Dep.target) { //當前正在創建的watcher實例保存在全局!!!
dep.depend(); //把當前正在創建的watcher實例保存到屬性的dep中
set: function reactiveSetter (newVal) {
dep.notify(); //去屬性的dep找watcher/update執行更新頁面中綁定的指令表達式
順帶,vuex是用computed方法實現的,而computed方法是基於defineReactive實現的,就是defineReactive技術。
vue 1.0具體就不再分析,網上已經有幾個文檔分析很透徹。
。
2.0從router.push()開始路由切換時執行transitionTo/confirmtransition的代碼莫名其妙,似乎不太對勁,到底最關鍵的代碼邏輯流程在哪裏?確實很難破解,因為涉及到源代碼總體關鍵設計思想邏輯,甚至可以說是
設計奧秘,vue作者是個了不起的大神,大神的代碼都有很隱蔽很深奧的設計邏輯和編程代碼難以破解,比如angular,它的模塊機制非常復雜深奧,源代碼難以破解。
本文命題破解要點:
1)每個組件都會創建new watcher:
vm._watcher = new Watcher(vm, function () {
vm._update(vm._render(), hydrating); //先產生vnode,再更新組件頁面
根組件watcher/update方法何時如何被執行?
new Vue()初始化根組件時即會執行,根組件有屬性變化時也會觸發執行。
keep-alive組件的watcher/update方法何時如何被執行?????
總不能寫vm._update()吧? (vm假定是keep-alive組件實例)
keep-alive組件沒有template沒有data,沒法用data屬性觸發執行watcher/update吧?
答案是在源代碼中當初始化keep-alive組件的vnode時(也就是執行vnode.data.hook.prepatch方法)會強制執行vm._update()更新keep-alive組件極其頁面,其中vm是keep-alive組件,keep-alive組件的頁面就是
路由組件頁面,router-view負責切換路由組件並且做為keep-alive的子組件,在keep-alive創建vnode時傳遞路由組件,然後保存在keep-alive vnode的componentOptions的children中,keep-alive和router-view都是占位/管理組件,它有子節點就是路由組件vnode,keep-alive只負責處理緩存,而router-view負責路由組件切換,也就是創建一個新的路由組件,並且更新頁面,但當外套<keep-alive>時,router-view不再處理替換,而是把新建的路由組件vnode傳遞給keep-alive,keep-alive可以從緩存恢復路由組件的實例,然後再更新頁面。
2)根組件的_route屬性
從$router.push()開始路由切換,先執行transitionto()以及confirmtransition,這是巨大的坑,這個過程只是處理輔助功能,主要是執行leave和beforeEnter等鉤子函數,鉤子函數可有可無,這段代碼99%都可以
不起任何作用,但看這段代碼跟看天書一樣,已經有滴滴高手分析了這段代碼。
執行transitionto最後會執行回調,在回調代碼中會設置根組件的_route屬性=當前路由,這是一個關鍵點,
vue已經針對根組件的_route屬性建立了watcher,當set這個屬性時,會執行wacther/update,也就是執行
vm._update(vm._render(), hydrating) (其中vm是根組件)
就是從這裏開始真正的路由切換處理,首先執行_render()產生根組件的vnode,再執行_update(vnode)方法調用__patch__(vnode)方法更新根組件頁面。
假定頁面是這樣寫的:
<keep-alive>
<router-view></router-view>
</keep-alive>
執行_render()方法時,大家首先要知道根組件template編譯之後產生的render/code包含有:
_c(‘keep-alive‘,[_c(‘router-view‘)])
首先會執行_c(‘router-view‘)產生router-view的vnode,_c方法會調用_createElement()方法,再調用
createComponent方法(註意有兩個createComponent方法),router-view是functionalComponent,會調用createFunctionalComponent方法,然後執行;
var vnode = Ctor.options.render.call(null, h, {
其中render就是router-view的render方法,是vue特殊構造的,不同於普通組件的render代碼。
router-view的render方從根組件_route屬性獲取路由,再獲取路由組件數據,再創建路由組件vnode返回,這都順理成章沒有什麽問題。
_c(‘router-view‘)執行完之後要執行_c(‘keep-alive‘,註意寫法,_c(‘router-view‘)是keep-alive的子節點,
會把router-view的vnode傳遞給_c(‘keep-alive‘方法,也就是把路由組件vnode傳遞給_c(‘keep-alive‘,我們來看一下_createElement()代碼,這是vue 2.0最關鍵最難理解的函數代碼:
function _createElement (
context,
tag,
data,
children,
needNormalization
) {
會調用createComponent方法,其中有一段代碼:
var vnode = new VNode(
("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : ‘‘)),
data, undefined, undefined, undefined, context,
{ Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children }
);
return vnode
這就是創建keep-alive組件的vnode,其中tag是"vue-componet-3-keep-alive",children就是路由組件的vnode,context就是keep-alive組件實例(keep-alive組件在初始化根組件時就已經建立一直存在)。
大家可以去看一下function VNode()的代碼,其中第七個參數就是componentOptions。
這樣keep-alive的vnode就創建了,其中有componentOptions也就是路由組件vnode,這是router-view傳遞而來的,router-view負責路由切換,只有router-view能創建路由組件vnode,但當它外套<keep-alive>
時,它做為keep-alive組件的子節點傳遞路由組件vnode,而keep-alive取代它成為占位組件占據根組件vnode樹中的那個位置。
到這裏跟組件vnode樹中就多了一個vnode,就是路由組件vnode,路由組件vnode已經成功插入vnode樹。
我們再回到根組件watcher/update方法,執行完_render()產生vnode之後就執行_update(vnode)方法更新根組件頁面,會調用__patch__方法更新根組件頁面,對於每一個vnode,會調用patchVnode方法處理,patchVnode會遞歸每一個vnode,而__patch__方法只是更新組件頁面,不遞歸vnode樹。
在根組件vnode樹種,keep-alive是最底層的vnode,沒有子vnode,但它有componentOptions,就是路由組件vnode,keep-alive的使命就是把自身vnode放在自己占的位置上,而vnode中含路由組件vnode,這是非常
關鍵非常難懂的環節,請繼續看下文。
繼續patch過程,當執行__patch__/patchVnode更新根組件頁面時,當執行到keep-alive的那個vnode時,它有data.hook,會執行vnode.data.hook.prepatch()方法,這個方法會執行_updateFromParent方法,這個方法
的名稱跟天書一樣難理解,其中有以下代碼:
if (hasChildren) {
vm.$slots = resolveSlots(renderChildren, parentVnode.context); //保存路由組件vnode到keep-alive組件
vm.$forceUpdate(); //強制keep-alive組件更新顯示新的路由組件頁面
這就是把路由組件vnode保存到keep-alive組件實例的$slots中,然後執行keep-alive組件的watcher/update:
vm._update(vm._render(), hydrating);
先執行keep-alive的_render方法,這是vue組件通用方法,有以下代碼:
vnode = render.call(vm._renderProxy, vm.$createElement);
其中render就是keep-alive組件的render方法,其中有以下代碼:
var KeepAlive = {
render: function render () {
var vnode = getFirstComponentChild(this.$slots.default);
它是從自身實例的$slots取路由組件vnode返回,再執行update(vnode)更新keep-alive組件頁面,此時vnode是路由組件vnode,那麽頁面就更新為路由組件頁面。
之前在執行_c(‘keep-alive‘時已經創建keep-alive vnode返回,然後執行vnode.data.hook.prepatch()處理,這裏又把keep-alive vnode替換更新為路由組件vnode,路由組件vnode的parent是keep-alivevnode,但在vnode樹中keep-alive vnode並沒有子vnode(children),它是一個占位組件vnode,路由切換時它變換vnode為路由組件vnode,頁面更新顯示的是路由組件頁面,有沒有暈?
再小結一下:
程序中觸發路由切換是從修改_route屬性開始;
順便提一下,router中綁定hashchange/pushState是為了針對直接修改瀏覽器地址欄的情況。
transitionto是跑龍套的“騙人”的,不是關鍵代碼,別誤入歧途;
watcher/update是vue觸發程序執行的隱蔽的殺手鐧,永遠要牢記,創建組件時會針對組件new watcher(),順便提一下,1.0是針對頁面表達式new wacther(),不是針對組件new watcher(),組件屬性變化時
會自動執行watcher,也可能在源代碼中直接執行watcher/update,這就開始一段重要源代碼的執行。
根組件編譯生成的render/code代碼決定了一切,尤其是其中的_c()是vue 2.0精華,與1.0完全不同,_c方法是最重要的切入點,源代碼中很少有調用_c的,因此createElement()方法不知道何時如何被調用,
以及如何傳遞參數,那些神奇的參數數據好像是天上掉下來似的,其實都是執行_c()方法調用createElement再傳遞參數數據,這個過程是系統自動進行的,沒有源代碼,像native code一樣,導致分析源代碼到這個環節
就犧牲了。
keep-alive是組件,有update方法,router-view不是組件,沒有update方法! 它們都有render方法,
一個是根據路由找路由組件數據再產生路由組件vnode,一個是直接取路由組件vnode返回到vnode樹中再更新組件頁面,邏輯設計很清楚啊。
vnode是對象嵌套,以children表示為子節點嵌套,表現為vnode樹。
watcher/update方法是最重要的切入點,觸發一段程序執行的起點,update更新包括新建都是先產生vnode,再根據vnode更新頁面,對於有template的組件,vnode就是與html對應的,對於管理/占位組件或標簽比如
router-view/keep-alive,有設計好的render代碼,其目的其實就是獲取路由組件vnode,之後還幹嘛?就是update更新路由組件頁面。
時間關系,可能還有些關鍵細節沒有提及,有問題歡迎交流,文中有錯誤或不妥之處歡迎拍磚指正,歡迎有興趣的網友一起來探索js框架的神秘世界。
vue 2.0 路由切換以及組件緩存源代碼重點難點分析