- 摘要
關於vue 2.0原始碼分析,已經有不少文件分析功能程式碼段比如watcher,history,vnode等,但沒有一個是分析重點難點的,沒有一個是分析大命題的,比如執行router.push之後到底是如何執行程式碼實現路由切換的?
本文旨在分享本人研究vue 2.0原始碼重點難點之結果,不涉及每段原始碼具體分析,原始碼功能段每個人都可以去分析,只要有耐心,再參考已有高手發表的原始碼分析文件,不是太難,主要是要克服一些程式設計技術問題,比如巢狀回撥,遞迴,物件/陣列特殊處理方法等等。
首先要說的是,vue 2.0的複雜性和難點都是由於採用vnode技術引起的,如果不採用vnode技術,像1.0那樣,
就沒有這些複雜性和難點。
我們先簡單回顧一下vue 1.0的路由切換和元件更新的入口程式碼,Vue2.0基本上也是用類似的入口機制,但觸發機制不同。
- vue 1.0 元件更新入口程式碼
vue 1.0會針對頁面指令表示式建立watcher:
var watcher = new Watcher(vm, expOrFn, cb, options);
會針對元件的data屬性執行響應式方法為屬性建立set/get方法:
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原始碼分析不是本文目的,網上已經有幾個文件分析很透徹,有興趣可以去檢視。
- vue 2.0路由切換入口程式碼
vue 2.0從router.push()開始路由切換時執行transitionTo方法開始路由切換流程,但transitionTo方法其實只是處理輔助功能,比如執行leave和beforeEnter鉤子函式,真正的路由切換處理程式碼並不在這兒,而是通過updateRoute方法修改_route屬性觸發執行真正的路由切換程式碼。
首先每個元件都會建立new watcher:
vm._watcher = new Watcher(vm, function () {
vm._update(vm._render(), hydrating); //先產生vnode,再更新元件頁面
new Vue()初始化根元件時即會執行根元件的_update方法,根元件有屬性變化時也會觸發執行_update方法,這是vue響應式機制實現的功能,具體細節可以參考已有文件,有1-2篇文件分析非常透徹,vue響應式機制原理已經不再是什麼祕密。
說過了根元件,那麼有個問題就是keep-alive元件的watcher/update方法何時如何被執行?
首先,keep-alive元件沒有template沒有data,沒法用data屬性觸發執行watcher/update。
在原始碼中當初始化keep-alive元件的vnode時(也就是執行vnode.data.hook.prepatch方法)會強制
執行vm._update()更新keep-alive元件極其頁面,其中vm是keep-alive元件,keep-alive元件的頁面就是
路由元件頁面。
vue 2.0由於採用元件標籤<keep-alive><router-view>方式實現路由元件快取,因此具有以下特殊機制:
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可以從快取恢復路由元件的例項,然後再更新頁面。
我們再從$router.push()開始,從$router.push()開始路由切換,先執行transitionto()以及confirmtransition(),關於這段原始碼,已經有滴滴高手發表了詳細的分析文件,有興趣的可以去檢視。
執行transitionto最後會執行回撥,在回撥程式碼中會設定根元件的_route屬性=當前路由,為了啟動路由切換入口,vue 2.0專門在根元件設計了一個_route屬性,vue已經針對根元件的_route屬性建立了watcher,當set這個屬性時,會執行wacther/update,也就是執行vm._update(vm._render(), hydrating) (其中vm是根元件),
就是從這裡開始進入真正的路由切換處入口,這是一個關鍵環節,如果沒找到這個關鍵環節,把原始碼看來看去,也還是不知道路由切換入口程式碼在哪裡,transitionTo()方法並不處理路由切換。
- vue 2.0 路由元件切換的快取機制
從執行vm._update(vm._render(), hydrating)就開始,首先執行_render()產生根元件的vnode,再執行_update(vnode)方法呼叫patch(vnode)方法更新根元件頁面。
vue 2.0規定的頁面寫法是<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,頁面更新顯示的是路由元件頁面,有沒有暈?因為vnode可以是對應html節點,也可以對應元件節點,元件vnode又分為管理元件vnode和應用元件vnode,它們的render方法是不同的,產生的vnode也是不同的,處理方法也是不同的。
- 小結回顧
程式中觸發路由切換是從修改_route屬性開始。
順便提一下,router中繫結hashchange/pushState是為了針對直接修改瀏覽器位址列的情況。
transitionto()方法是跑龍套的非關鍵程式碼,它只是處理路由切換之前以及之後執行鉤子函式,鉤子函式不是必須的,假定沒有鉤子函式,它實際上就是空執行一遍流程,如果看原始碼時把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方法的,它是在編譯template生成的render/code中含_c()方法,執行render/code時就會執行其中的_c()方法。
keep-alive是元件,有update方法,router-view不是元件,沒有update方法! 它們都有render方法,
一個是根據路由找路由元件資料再產生路由元件vnode,一個是直接取路由元件vnode返回到vnode樹中再更新元件頁面,邏輯設計很清楚是不是?
vnode是物件巢狀,以children表示為子節點巢狀,表現為vnode樹。
watcher/update方法是路由切換和頁面更新最重要的切入點/入口,update更新包括新建都是先執行_render方法產生vnode,再根據vnode更新頁面,對於有template的元件,vnode就是與html對應的,對於管理/佔位元件或標籤比如router-view/keep-alive,有設計好的render程式碼,其目的其實就是獲取路由元件vnode,之後還幹嘛?就是
update更新路由元件頁面。
大致邏輯挺簡單的,但要把原始碼走通很難,因為原始碼太分散,設計邏輯和程式設計技術高超,超出一般想象,
有些原始碼是非同步同時執行的,有些函式比如_c()方法的呼叫方法比較隱蔽比較特殊,很難追朔debug看重要關鍵引數資料是怎麼來的,原始碼中的註釋太少太短,尤其在關鍵之處甚至沒有註釋。
時間關係,可能還有些關鍵細節沒有提及,有問題歡迎交流,文中有錯誤或不妥之處歡迎拍磚指正,歡迎有興趣的網友一起來探索js框架的神祕世界。