1. 程式人生 > >vue 2.0 路由切換以及組件緩存源代碼重點難點分析

vue 2.0 路由切換以及組件緩存源代碼重點難點分析

基於 代碼實現 而是 答案 html fine 傳遞參數 並且 等等

關於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 路由切換以及組件緩存源代碼重點難點分析