vue-router 執行過程及原始碼解析。
我們都知道,vue提供的cli腳手架工具中直接會詢問是否安裝vue-router,可見vue-router和vue的情之深思之切。
vue-router其實主要通過push操作修改route,從而觸發setter完成路由元件router-view的重新渲染,
下面我們將從何時觸發的push和如何觸發元件重新渲染對vue-router核心原理進行講解
我們在使用vue-router第一個步驟都是先執行Vue.use(Router)
,Vue.use其實是一個vue外掛的安裝過程,最後執行到外掛中 的install方法,我們先來看vue-router的install做了什麼
function install (Vue) { if (install.installed && _Vue === Vue) { return } install.installed = true; _Vue = Vue; var isDef = function (v) { return v !== undefined; }; var registerInstance = function (vm, callVal) { var i = vm.$options._parentVnode; if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) { i(vm, callVal); } }; Vue.mixin({ beforeCreate: function beforeCreate () { if (isDef(this.$options.router)) { this._routerRoot = this; this._router = this.$options.router; this._router.init(this); Vue.util.defineReactive(this, '_route', this._router.history.current); } else { this._routerRoot = (this.$parent && this.$parent._routerRoot) || this; } registerInstance(this, this); }, destroyed: function destroyed () { registerInstance(this); } }); Object.defineProperty(Vue.prototype, '$router', { get: function get () { return this._routerRoot._router } }); Object.defineProperty(Vue.prototype, '$route', { get: function get () { return this._routerRoot._route } }); Vue.component('RouterView', View); Vue.component('RouterLink', Link); var strats = Vue.config.optionMergeStrategies; // use the same hook merging strategy for route hooks strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created; }
首先會對重複安裝進行過濾,然後向全域性Vue的options中混入了beforeCreate方法和destroyed方法,我們知道所有元件的options都會繼承Vue的option,所以,在每個元件初始化過程中都會混入這兩個鉤子函式,
除了混入這兩個鉤子函式外,還對$route
r和$route
兩個屬性進行了劫持,使我們可以直接通過Vue物件例項訪問到,這兩個一個儲存的是VueRouter物件一個儲存的是匹配到的路由資訊,最後全域性註冊了Routerview
和RouterLink
兩個元件,所以我們才可以在任何地方使用這兩個元件,這兩個元件的內容我們稍後分析,
我們先看混入每個元件的beforeCreate鉤子函式
if (isDef(this.$options.router)) { this._routerRoot = this; this._router = this.$options.router; this._router.init(this); Vue.util.defineReactive(this, '_route', this._router.history.current); } else { this._routerRoot = (this.$parent && this.$parent._routerRoot) || this; } registerInstance(this, this);
呼叫Vue中定義的defineReactive對_route
進行劫持,這個劫持之前有說過,其實是執行的依賴收集的過程,執行_route
的get就會對當前的元件進行依賴收集,如果對_route
進行重新賦值觸發setter就會使收集的元件重新渲染,這裡也是路由重新渲染的核心所在,這裡還執行了init方法,我們來看下init
VueRouter.prototype.init = function init (app /* Vue component instance */) { var this$1 = this; process.env.NODE_ENV !== 'production' && assert( install.installed, "not installed. Make sure to call `Vue.use(VueRouter)` " + "before creating root instance." ); this.apps.push(app); // main app already initialized. if (this.app) { return } this.app = app; var history = this.history; if (history instanceof HTML5History) { history.transitionTo(history.getCurrentLocation()); } else if (history instanceof HashHistory) { var setupHashListener = function () { history.setupListeners(); }; history.transitionTo( history.getCurrentLocation(), setupHashListener, setupHashListener ); } history.listen(function (route) { debugger this$1.apps.forEach(function (app) { app._route = route; }); }); };
重點是根據我們初使化的路由型別,執行transitionTo方法,這個是路由跳轉的核心所在,這裡同時還對瀏覽器hashChange事件進行了監聽,比如我們執行瀏覽器後退事件也會觸發transitionTo方法,這裡在初始化的時候會根據當前路徑執行一次transitionTo,,這也是為什麼當我們直接在位址列輸入localhost:8080
最後會變成localhost:8080/#/
,
最後執行了history.listen方法,這裡是當路由改變時對_route進行重新賦值從而觸發元件更新,setter的執行也是在這裡,這裡的呼叫是在transitionTo完成之後,我們看下transitionTo的定義
History.prototype.transitionTo = function transitionTo (location, onComplete, onAbort) { var this$1 = this; debugger var route = this.router.match(location, this.current); this.confirmTransition(route, function () { this$1.updateRoute(route); onComplete && onComplete(route); this$1.ensureURL(); // fire ready cbs once if (!this$1.ready) { this$1.ready = true; this$1.readyCbs.forEach(function (cb) { cb(route); }); } }, function (err) { if (onAbort) { onAbort(err); } if (err && !this$1.ready) { this$1.ready = true; this$1.readyErrorCbs.forEach(function (cb) { cb(err); }); } }); };
這裡是根據傳入的路徑從我們定義的所有路由中匹配到對應路由,然後執行confirmTransition
我們來看程式碼
History.prototype.confirmTransition = function confirmTransition (route, onComplete, onAbort) { var this$1 = this; var current = this.current; var abort = function (err) { if (isError(err)) { if (this$1.errorCbs.length) { this$1.errorCbs.forEach(function (cb) { cb(err); }); } else { warn(false, 'uncaught error during route navigation:'); console.error(err); } } onAbort && onAbort(err); }; if ( isSameRoute(route, current) && // in the case the route map has been dynamically appended to route.matched.length === current.matched.length ) { this.ensureURL(); return abort() } var ref = resolveQueue(this.current.matched, route.matched); var updated = ref.updated; var deactivated = ref.deactivated; var activated = ref.activated; var queue = [].concat( // in-component leave guards extractLeaveGuards(deactivated), // global before hooks this.router.beforeHooks, // in-component update hooks extractUpdateHooks(updated), // in-config enter guards activated.map(function (m) { return m.beforeEnter; }), // async components resolveAsyncComponents(activated) ); this.pending = route; var iterator = function (hook, next) { if (this$1.pending !== route) { return abort() } try { hook(route, current, function (to) { if (to === false || isError(to)) { // next(false) -> abort navigation, ensure current URL this$1.ensureURL(true); abort(to); } else if ( typeof to === 'string' || (typeof to === 'object' && ( typeof to.path === 'string' || typeof to.name === 'string' )) ) { // next('/') or next({ path: '/' }) -> redirect abort(); if (typeof to === 'object' && to.replace) { this$1.replace(to); } else { this$1.push(to); } } else { // confirm transition and pass on the value next(to); } }); } catch (e) { abort(e); } }; runQueue(queue, iterator, function () { var postEnterCbs = []; var isValid = function () { return this$1.current === route; }; // wait until async components are resolved before // extracting in-component enter guards var enterGuards = extractEnterGuards(activated, postEnterCbs, isValid); var queue = enterGuards.concat(this$1.router.resolveHooks); runQueue(queue, iterator, function () { if (this$1.pending !== route) { return abort() } this$1.pending = null; onComplete(route); if (this$1.router.app) { this$1.router.app.$nextTick(function () { postEnterCbs.forEach(function (cb) { cb(); }); }); } }); }); };
首先會有重複路由的判斷,如果進入相同的路由,直接呼叫abort回撥函式,函式退出,不會執行後面的各元件的鉤子函式,這也是為什麼我們重複進入相同路由不會觸發組建的重新渲染也不會觸發路由的各種鉤子函式,
如果判斷不是相同路由,就會執行各元件的鉤子函式,這裡的邏輯我們不講,可以根據文件描述來看路由執行順序
這裡我們貼一下官網描述的導航守衛執行順序
導航被觸發。 在失活的元件裡呼叫離開守衛。 呼叫全域性的 beforeEach 守衛。 在重用的元件裡呼叫 beforeRouteUpdate 守衛 (2.2+)。 在路由配置裡呼叫 beforeEnter。 解析非同步路由元件。 在被啟用的元件裡呼叫 beforeRouteEnter。 呼叫全域性的 beforeResolve 守衛 (2.5+)。 導航被確認。 呼叫全域性的 afterEach 鉤子。 觸發 DOM 更新。 用建立好的例項呼叫 beforeRouteEnter 守衛中傳給 next 的回撥函式。
按順序執行好導航守衛後,就會執行傳入的成功的回撥函式,也就是
function () { this$1.updateRoute(route); onComplete && onComplete(route); this$1.ensureURL(); // fire ready cbs once if (!this$1.ready) { this$1.ready = true; this$1.readyCbs.forEach(function (cb) { cb(route); }); } }
這裡會執行初始化的時候通過listen註冊的回撥函式,從而對_route
進行賦值,觸發setter,從而使元件重新渲染
下面我們來看下平時我們通過this.$router.push
或者router-link
是如何執行transitionTo的,我們先來看push的定義
HTML5History.prototype.push = function push (location, onComplete, onAbort) { var this$1 = this; var ref = this; var fromRoute = ref.current; this.transitionTo(location, function (route) { pushState(cleanPath(this$1.base + route.fullPath)); handleScroll(this$1.router, route, fromRoute, false); onComplete && onComplete(route); }, onAbort); }
可以看到,主要就是執行了transitionTo的方法,我們再來看下routerLink這個全域性註冊的元件
render: function render (h) { var this$1 = this; var router = this.$router; var current = this.$route; var ref = router.resolve(this.to, current, this.append); var location = ref.location; var route = ref.route; var href = ref.href; var classes = {}; var globalActiveClass = router.options.linkActiveClass; var globalExactActiveClass = router.options.linkExactActiveClass; // Support global empty active class var activeClassFallback = globalActiveClass == null ? 'router-link-active' : globalActiveClass; var exactActiveClassFallback = globalExactActiveClass == null ? 'router-link-exact-active' : globalExactActiveClass; var activeClass = this.activeClass == null ? activeClassFallback : this.activeClass; var exactActiveClass = this.exactActiveClass == null ? exactActiveClassFallback : this.exactActiveClass; var compareTarget = location.path ? createRoute(null, location, null, router) : route; classes[exactActiveClass] = isSameRoute(current, compareTarget); classes[activeClass] = this.exact ? classes[exactActiveClass] : isIncludedRoute(current, compareTarget); var handler = function (e) { if (guardEvent(e)) { if (this$1.replace) { router.replace(location); } else { router.push(location); } } }; var on = { click: guardEvent }; if (Array.isArray(this.event)) { this.event.forEach(function (e) { on[e] = handler; }); } else { on[this.event] = handler; } var data = { class: classes }; if (this.tag === 'a') { data.on = on; data.attrs = { href: href }; } else { // find the first <a> child and apply listener and href var a = findAnchor(this.$slots.default); if (a) { // in case the <a> is a static node a.isStatic = false; var aData = a.data = extend({}, a.data); aData.on = on; var aAttrs = a.data.attrs = extend({}, a.data.attrs); aAttrs.href = href; } else { // doesn't have <a> child, apply listener to self data.on = on; } } return h(this.tag, data, this.$slots.default) }
這是routerLink組建的render函式,可以看到為此元件註冊了click事件,事件的回撥函式便是呼叫router.push,所以,可以看到所有的路由跳轉最後都是呼叫的transitionTo方法,
最後我們再看下routerView這個全域性元件的定義
render: function render (_, ref) { debugger var props = ref.props; var children = ref.children; var parent = ref.parent; var data = ref.data; // used by devtools to display a router-view badge data.routerView = true; // directly use parent context's createElement() function // so that components rendered by router-view can resolve named slots var h = parent.$createElement; var name = props.name; var route = parent.$route; var cache = parent._routerViewCache || (parent._routerViewCache = {}); // determine current view depth, also check to see if the tree // has been toggled inactive but kept-alive. var depth = 0; var inactive = false; while (parent && parent._routerRoot !== parent) { if (parent.$vnode && parent.$vnode.data.routerView) { depth++; } if (parent._inactive) { inactive = true; } parent = parent.$parent; } data.routerViewDepth = depth; // render previous view if the tree is inactive and kept-alive if (inactive) { return h(cache[name], data, children) } var matched = route.matched[depth]; // render empty node if no matched route if (!matched) { cache[name] = null; return h() } var component = cache[name] = matched.components[name]; // attach instance registration hook // this will be called in the instance's injected lifecycle hooks data.registerRouteInstance = function (vm, val) { // val could be undefined for unregistration var current = matched.instances[name]; if ( (val && current !== vm) || (!val && current === vm) ) { matched.instances[name] = val; } } // also register instance in prepatch hook // in case the same component instance is reused across different routes ;(data.hook || (data.hook = {})).prepatch = function (_, vnode) { matched.instances[name] = vnode.componentInstance; }; // resolve props var propsToPass = data.props = resolveProps(route, matched.props && matched.props[name]); if (propsToPass) { // clone to prevent mutation propsToPass = data.props = extend({}, propsToPass); // pass non-declared props as attrs var attrs = data.attrs = data.attrs || {}; for (var key in propsToPass) { if (!component.props || !(key in component.props)) { attrs[key] = propsToPass[key]; delete propsToPass[key]; } } } return h(component, data, children) }
其實就是執行$createElement,把路由匹配到的元件渲染到routerView上,這裡會有巢狀路由的判斷,這裡先不展開講了,
這篇文章到這裡位置,整個vue-