本文主要和大家分享vue-router源碼閱讀學習,如同分析vuex源碼我們首先透過一個簡單例子進行了解vue-router是如何使用的,然後在分析在源碼中是如何實現的,希望能幫助大家。
範例
下列範例來自example/basica/app.js
import Vue from 'vue' import VueRouter from 'vue-router' Vue.use(VueRouter) const Home = { template: '<div>home</div>' } const Foo = { template: '<div>foo</div>' } const Bar = { template: '<div>bar</div>' } const router = new VueRouter({ mode: 'history', base: __dirname, routes: [ { path: '/', component: Home }, { path: '/foo', component: Foo }, { path: '/bar', component: Bar } ] }) new Vue({ router, template: ` <div id="app"> <h1>Basic</h1> <ul> <li><router-link to="/">/</router-link></li> <li><router-link to="/foo">/foo</router-link></li> <li><router-link to="/bar">/bar</router-link></li> <router-link tag="li" to="/bar" :event="['mousedown', 'touchstart']"> <a>/bar</a> </router-link> </ul> <router-view class="view"></router-view> </div> ` }).$mount('#app')
首先呼叫Vue.use(VueRouter),Vue.use()方法是Vue用來進行插件安裝的方法,這裡主要用來安裝VueRouter。然後實例化了VueRouter,讓我們來看看VueRouter這個建構子到底做了什麼。
從原始碼入口檔src/index.js開始看
import type { Matcher } from './create-matcher'export default class VueRouter { constructor (options: RouterOptions = {}) { this.app = null this.apps = [] this.options = options this.beforeHooks = [] this.resolveHooks = [] this.afterHooks = [] this.matcher = createMatcher(options.routes || [], this) let mode = options.mode || 'hash' this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false if (this.fallback) { mode = 'hash' } if (!inBrowser) { mode = 'abstract' } this.mode = mode switch (mode) { case 'history': this.history = new HTML5History(this, options.base) break case 'hash': this.history = new HashHistory(this, options.base, this.fallback) break case 'abstract': this.history = new AbstractHistory(this, options.base) break default: if (process.env.NODE_ENV !== 'production') { assert(false, `invalid mode: ${mode}`) } } } init (app: any /* Vue component instance */) { this.apps.push(app) // main app already initialized. if (this.app) { return } this.app = app const history = this.history if (history instanceof HTML5History) { history.transitionTo(history.getCurrentLocation()) } else if (history instanceof HashHistory) { const setupHashListener = () => { history.setupListeners() } history.transitionTo( history.getCurrentLocation(), setupHashListener, setupHashListener ) } history.listen(route => { this.apps.forEach((app) => { app._route = route }) }) } getMatchedComponents (to?: RawLocation | Route): Array<any> { const route: any = to ? to.matched ? to : this.resolve(to).route : this.currentRoute if (!route) { return [] } return [].concat.apply([], route.matched.map(m => { return Object.keys(m.components).map(key => { return m.components[key] }) })) } }
程式碼一步一步地看,先從constructor函數的實作,首先進行初始化我們來看看這些初始化條件分別代表的是什麼
this.app表示當前Vue實例
this.apps表示所有app元件
this.options表示傳入的VueRouter的選項
this.resolveHooks表示resolve鉤子回呼函數的數組,resolve用於解析目標位置
this.matcher建立匹配函數
#程式碼中有createMatcher()函數,來看看他的實作
function createMatcher ( routes, router ) { var ref = createRouteMap(routes); var pathList = ref.pathList; var pathMap = ref.pathMap; var nameMap = ref.nameMap; function addRoutes (routes) { createRouteMap(routes, pathList, pathMap, nameMap); } function match ( raw, currentRoute, redirectedFrom ) { var location = normalizeLocation(raw, currentRoute, false, router); var name = location.name; // 命名路由处理 if (name) { // nameMap[name]的路由记录 var record = nameMap[name]; ... location.path = fillParams(record.path, location.params, ("named route \"" + name + "\"")); // _createRoute用于创建路由 return _createRoute(record, location, redirectedFrom) } else if (location.path) { // 普通路由处理 } // no match return _createRoute(null, location) } return { match: match, addRoutes: addRoutes } }
createMatcher()有兩個參數routes表示建立VueRouter傳入的routes設定資訊,router表示VueRouter實例。 createMatcher()的功能是傳入的routes透過createRouteMap建立對應的map,和一個建立map的方法。
我們先來看看createRouteMap()方法的定義
function createRouteMap ( routes, oldPathList, oldPathMap, oldNameMap) { // 用于控制匹配优先级 var pathList = oldPathList || []; // name 路由 map var pathMap = oldPathMap || Object.create(null); // name 路由 map var nameMap = oldNameMap || Object.create(null); // 遍历路由配置对象增加路由记录 routes.forEach(function (route) { addRouteRecord(pathList, pathMap, nameMap, route); }); // 确保通配符总是在pathList的最后,保证最后匹配 for (var i = 0, l = pathList.length; i < l; i++) { if (pathList[i] === '*') { pathList.push(pathList.splice(i, 1)[0]); l--; i--; } } return { pathList: pathList, pathMap: pathMap, nameMap: nameMap } }
createRouteMap()有4個參數:routes代表的配置訊息,oldPathList包含所有路徑的陣列用於匹配優先權,oldNameMap表示name map,oldPathMap表示path map。 createRouteMap就是更新pathList,nameMap和pathMap。 nameMap到底代表的是什麼呢?它是包含路由記錄的一個對象,每個屬性值名是每個記錄的path屬性值,屬性值就是具有這個path屬性值的路由記錄。這裡有一個叫路由記錄的東西,這是什麼意思呢?路由記錄就是routes 設定陣列中的物件副本(還有在children 陣列),路由記錄都是包含在matched屬性中例如
#const router = new VueRouter({ routes: [ // 下面的对象就是 route record { path: '/foo', component: Foo, children: [ // 这也是个 route record { path: 'bar', component: Bar } ] } ] })
在上面程式碼中用一段程式碼用來為每個route新增路由記錄,那麼路由記錄的實作是如何的呢,以下是addRouteReord()的實作
function addRouteRecord ( pathList, pathMap, nameMap, route, parent, matchAs ) { var path = route.path; var name = route.name; var normalizedPath = normalizePath( path, parent ); var record = { path: normalizedPath, regex: compileRouteRegex(normalizedPath, pathToRegexpOptions), components: route.components || { default: route.component }, instances: {}, name: name, parent: parent, matchAs: matchAs, redirect: route.redirect, beforeEnter: route.beforeEnter, meta: route.meta || {}, props: route.props == null ? {} : route.components ? route.props : { default: route.props } }; if (route.children) { route.children.forEach(function (child) { addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs); }); } if (route.alias !== undefined) { // 如果有别名的情况 } if (!pathMap[record.path]) { pathList.push(record.path); pathMap[record.path] = record; } }
addRouteRecord()這個函數的參數我都懶得說什麼意思了,新增的parent也表示路由記錄,首先取得path,name。然後透過normalizePath()規範格式,然後就是record這個物件的建立,然後遍歷routes的子元素加入路由記錄如果有別名的情況還需要考慮別名的情況然後更新path Map。
History
我們在回到VueRouter的建構子中,往下看是模式的選擇,一共這麼多種模式一種history,hash和abstract三種。 · 預設hash: 使用URL hash值作為路由,支援所有瀏覽器
· history: 依賴HTML5 History API和伺服器設定
· abstract:支援所有 JavaScript 運行環境,如 Node.js 伺服器端。如果發現沒有瀏覽器的 API,路由會自動強制進入這個模式。
預設是hash,路由透過「#」隔開,但是如果工程中有錨點連結或是路由中有hash值,原先的「#」就會對頁面跳躍產生影響;所以就需要使用history模式。
在應用程式中我們常用的基本上都是history模式,我們來看看HashHistory的建構子
var History = function History (router, base) { this.router = router; this.base = normalizeBase(base); this.current = START; this.pending = null; this.ready = false; this.readyCbs = []; this.readyErrorCbs = []; this.errorCbs = []; };
因為hash和history有一些相同的地方,所以HashHistory會在History建構子上進行擴充下面是各個屬性所代表的意義:
this.router表示VueRouter實例
this.base表示應用的基底路徑。例如,如果整個單頁應用程式服務在 /app/ 下,然後 base 就應該設為
"/app/"。 normalizeBase()用於格式化basethis.current开始时的route,route使用createRoute()创建
this.pending表示进行时的route
this.ready表示准备状态
this.readyCbs表示准备回调函数
creatRoute()在文件src/util/route.js中,下面是他的实现
function createRoute ( record, location, redirectedFrom, router ) { var stringifyQuery$$1 = router && router.options.stringifyQuery; var query = location.query || {}; try { query = clone(query); } catch (e) {} var route = { name: location.name || (record && record.name), meta: (record && record.meta) || {}, path: location.path || '/', hash: location.hash || '', query: query, params: location.params || {}, fullPath: getFullPath(location, stringifyQuery$$1), matched: record ? formatMatch(record) : [] }; if (redirectedFrom) { route.redirectedFrom = getFullPath(redirectedFrom, stringifyQuery$$1); } return Object.freeze(route) }
createRoute有三个参数,record表示路由记录,location,redirectedFrom表示url地址信息对象,router表示VueRouter实例对象。通过传入的参数,返回一个冻结的route对象,route对象里边包含了一些有关location的属性。History包含了一些基本的方法,例如比较重要的方法有transitionTo(),下面是transitionTo()的具体实现。
History.prototype.transitionTo = function transitionTo (location, onComplete, onAbort) { var this$1 = this; 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); }); } }); };
首先match得到匹配的route对象,route对象在之前已经提到过。然后使用confirmTransition()确认过渡,更新route,ensureURL()的作用就是更新URL。如果ready为false,更改ready的值,然后对readyCbs数组进行遍历回调。下面来看看HTML5History的构造函数
var HTML5History = (function (History$$1) { function HTML5History (router, base) { var this$1 = this; History$$1.call(this, router, base); var initLocation = getLocation(this.base); window.addEventListener('popstate', function (e) { var current = this$1.current; var location = getLocation(this$1.base); if (this$1.current === START && location === initLocation) { return } }); } if ( History$$1 ) HTML5History.__proto__ = History$$1; HTML5History.prototype = Object.create( History$$1 && History$$1.prototype ); HTML5History.prototype.constructor = HTML5History; 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); }; HTML5History.prototype.replace = function replace (location, onComplete, onAbort) { var this$1 = this; var ref = this; var fromRoute = ref.current; this.transitionTo(location, function (route) { replaceState(cleanPath(this$1.base + route.fullPath)); handleScroll(this$1.router, route, fromRoute, false); onComplete && onComplete(route); }, onAbort); }; return HTML5History; }(History))
在HTML5History()中代码多次用到了getLocation()那我们来看看他的具体实现吧
function getLocation (base) { var path = window.location.pathname; if (base && path.indexOf(base) === 0) { path = path.slice(base.length); } return (path || '/') + window.location.search + window.location.hash }
用一个简单的地址来解释代码中各个部分的含义。例如http://example.com:1234/test/test.htm#part2?a=123,window.location.pathname=>/test/test.htm=>?a=123,window.location.hash=>#part2。
把我们继续回到HTML5History()中,首先继承history构造函数。然后监听popstate事件。当活动记录条目更改时,将触发popstate事件。需要注意的是调用history.pushState()或history.replaceState()不会触发popstate事件。我们来看看HTML5History的push方法。location表示url信息,onComplete表示成功后的回调函数,onAbort表示失败的回调函数。首先获取current属性值,replaceState和pushState用于更新url,然后处理滚动。模式的选择就大概讲完了,我们回到入口文件,看看init()方法,app代表的是Vue的实例,现将app存入this.apps中,如果this.app已经存在就返回,如果不是就赋值。this.history是三种的实例对象,然后分情况进行transtionTo()操作,history方法就是给history.cb赋值穿进去的回调函数。
下面看getMatchedComponents(),唯一需要注意的就是我们多次提到的route.matched是路由记录的数据,最终返回的是每个路由记录的components属性值的值。
Router-View
最后讲讲router-view
var View = { name: 'router-view', functional: true, props: { name: { type: String, default: 'default' } }, render: function render (_, ref) { var props = ref.props; var children = ref.children; var parent = ref.parent; var data = ref.data; // 解决嵌套深度问题 data.routerView = true; var h = parent.$createElement; var name = props.name; // route var route = parent.$route; // 缓存 var cache = parent._routerViewCache || (parent._routerViewCache = {}); // 组件的嵌套深度 var depth = 0; // 用于设置class值 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; if (inactive) { return h(cache[name], data, children) } var matched = route.matched[depth]; if (!matched) { cache[name] = null; return h() } var component = cache[name] = matched.components[name]; 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; } } ;(data.hook || (data.hook = {})).prepatch = function (_, vnode) { matched.instances[name] = vnode.componentInstance; }; var propsToPass = data.props = resolveProps(route, matched.props && matched.props[name]); if (propsToPass) { propsToPass = data.props = extend({}, propsToPass); 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) } };
router-view比较简单,functional为true使组件无状态 (没有 data ) 和无实例 (没有 this 上下文)。他们用一个简单的 render 函数返回虚拟节点使他们更容易渲染。props表示接受属性,下面来看看render函数,首先获取数据,然后缓存,_inactive用于处理keep-alive情况,获取路由记录,注册Route实例,h()用于渲染。很简单我也懒得一一再说。
相关推荐: