Vue-Router提供了倆個元件 `router-link` `router-view`, 提供了倆個原型上的屬性`$route` `$router` ,我現在跟著原始碼來把它實現一下
開始
先看平時使用的 `Vue-Router` ,引入`Router` , `Vue.use` 註冊外掛。直接從這裡開始入手
使用場景
import Vue from 'vue' import Router from "../vue-router" import routes from './routes' Vue.use(Router) let router = new Router({ routes })
index
先看`vue-router.js`檔案,先生成一個`VueRouter`類,然後匯入`install`方法,因為`Vue-Router`的`install`方法比`Vuex`複雜一些,所以將`install`單獨作為一個檔案。
import install from './install'; class VueRouter { constructor(options) { } } VueRouter.install = install; export default VueRouter
Vue.use()
來先看 `Vue.use()`的原始碼中的一部分,這裡面判斷註冊的外掛裡的`install`是不是一個函式,有就執行外掛裡的`install`函式。或者判斷外掛本身是不是一個函式,有就執行外掛本身。這裡本質上是沒有區別,有沒有`install`都可以。而`VueRouter`使用了`install`,目的是為了將`install`作為入口函式,方便封裝,同時也將`install`和其他程式碼分開。
if (typeof plugin.install === 'function') { plugin.install.apply(plugin, args) } else if (typeof plugin === 'function') { plugin.apply(null, args) }
install
上述已經將`vue-router`的類構建好,現在`VueRouter`例項已經有了,然後執行`vue.use()`,然後會執行`VueRouter`類裡的`install`函式,那來看`install.js`。
install裡使用了`Vue.mixin`,混入程式碼,下面程式碼是在當前元件生命週期`beforeCreate`裡混入程式碼,程式碼邏輯是判斷當前元件是否為根元件,如果是則將`_routerRoot`作為鍵放入當前元件中,值為`Vue`例項。再將`_router`作為鍵放入當前元件中,值為`VueRouter`例項。然後執行初始化操作。
如果不為當前元件不是根元件,則該元件為根元件的子元件。將`_routerRoot`作為鍵放入當前元件中,值為父元件的`_routerRoot`,從父親身上獲取.
`$route`和`$router`是利用`Object.definePropert`代理`_routerRoot`裡的`_router`和`_route`,訪問到的。
接著註冊全域性元件`router-view`和`router-link`
import RouterView from '../components/router-view' import RouterLink from '../components/router-link' const install = (_Vue, s) => { _Vue.mixin({ beforeCreate() { if (this.$options.router) { // 判斷是不是根元件 this._routerRoot = this; // 把vue例項放在當前元件的——routerRoot屬性上 this._router = this.$options.router // 這裡直接獲取根例項 this._router.init(this); // 初始化 _Vue.util.defineReactive(this, '_route', this._router.history.current) // 將屬性_route成為響應式屬性 } else { this._routerRoot = this.$parent && this.$parent._routerRoot // 這裡的是從父親最頂層獲取Router例項 } } }) Object.defineProperty(_Vue.prototype, '$route', { // 代理$route get() { return this._routerRoot._route } }) Object.defineProperty(_Vue.prototype, '$router', { // 代理$router get() { return this._routerRoot._router } }) _Vue.component('router-view', RouterView) // 註冊元件router-view _Vue.component('router-link', RouterLink) // 註冊元件router-view } export default install
init
vue-router 預設 hash 模式 —— 使用 URL 的 hash 來模擬一個完整的 URL,於是當 URL 改變時,頁面不會重新載入。
然後回到`VueRouter`類中,此時多了個`init`函式。目前做的路由方式是`hash`方式,還有另外倆種方式,`history`,`abstract`。因為有三種方式,所以`Vue-Router`做了一個父類`base`執行同樣的邏輯,子類三種方式繼承父類`base`,再獨自執行自己方式的程式碼。
通過`new HashHistory` 獲取 `history`例項,初始化`init`執行`history`例項對應函式。將目光放到`history`例項上,這些函式來自於`base.js`和`hash.js`。
import install from './install'; class VueRouter { constructor(options) { this.history = new HashHistory(this); } init(app) { const history = this.history; const setupHashLister = () => { history.setupLister(); // hash的跳轉 } history.transitionTo( history.getCurrentLocation(), setupHashLister ) history.listen((route) => { app._route = route }) } } VueRouter.install = install; export default VueRouter
base.js
先看建構函式`construcror`,將`router`作為鍵放在自身例項上,值為`VueRouter`例項,`curent`為當前導航正要離開的路由,也就是路由守衛的引數裡的`from`<br/>
1.`transitionTo()`為跳轉後立即執行的函式,傳入當前路徑和回撥函式,`r`為`$route`,是扁平化後的配置,也就是即將要進入的目標 路由物件<br/>
2.`cb`是`History`的`listen()`函式,將`$route`放入當前元件上供使用者使用。<br/>
3.`callback`是執行`HashHistory`的`setupHashLister()`函式,是給當前`window`新增監聽事件`onhashChange`,`onhashChange`後續通過`hash`變化執行`transitionTo`進行更新。<br/>
4.最後將`r`賦值給`current`,更新路由資訊。
class History { constructor(router) { this.router = router; this.current = createRoute(null, { path: '/' }) } transitionTo(location, callback) { let r = this.router.match(location) if (location == this.current.path && r.matched.length == this.current.matched.length) { // 防止重複跳轉 return } this.cb && this.cb(r); callback && callback(); this.current = r; } listen(cb) { this.cb = cb; } }
hash.js
`hash`方式的函式就簡單介紹一下,看建構函式`constructor`,跟父類一樣賦值`router`。執行`ensureSlash`函式,因為`hash`相比其他函式,一進入頁面就會多個#。所以就初始化的時候處理一下。`getCurrentLocation`函式是獲取當前路徑的,`push`是`hash`方式的跳轉,`setupLister`函式是剛剛所述的監聽函式`hashchange`。
import Histroy from './base'; function ensureSlash() { if (window.location.hash) { return } window.location.hash = '/'; } class HashHistory extends Histroy { constructor(router) { super(); this.router = router; ensureSlash(); } getCurrentLocation() { return window.location.hash.slice(1); } push(location){ this.transitionTo(location,()=>{ window.location.hash = location }) } setupLister() { window.addEventListener('hashchange', () => { this.transitionTo(window.location.hash.slice(1)); }) } } export default HashHistory
扁平化
剛剛`base.js`裡執行的`this.router.match(location)`以及`createRoute()`,都是需要建立在扁平化配置基礎之上的。
平時配置的路由是這樣的,需要將配置進行扁平化,才能用得上。
[ { path: '/', name: 'home', component: Home }, { path: '/about', name: 'about', component: About, children: [ { path: 'add', name: 'add', component: Add }, { path: 'bull', name: 'bull', component: Bull } ] } ]
扁平化後是這樣的
/: {path: "/", component: {…}, parnent: undefined} /about: {path: "about", component: {…}, parnent: {…}} /about/add: {path: "add", component: {…}, parnent: {…}} /about/bull: {path: "bull", component: {…}, parnent: {…}}
接著看扁平化函式createMatcher以及createRouteMap
createMatcher
`createMatcher`返回一個`match`函式,`match`方法是匹配路徑,根據路徑拿扁平化物件裡的配置,然後執行`createRoute`方法,將其轉化為`route`,返回。`pathMap`由`createRouteMap`生成
import createRouteMap from './create-route-map' import { createRoute } from './history/base'; export default function createMatcher(routes) { let { pathList, pathMap } = createRouteMap(routes); function match(location) { console.log(pathMap) let record = pathMap[location]; return createRoute(record,{ path: location }) } return { match } }
createRouteMap
將`routes`配置傳入`createRouteMap`中,遍歷`routes`,進行扁平化操作
`pathMap`以路徑為鍵名,值為一個物件包裹著路徑,元件,父元件。
將路徑匹配上父元件的路徑和自身的路徑
如果有子元件就進行遞迴,全部轉為扁平化返回。
export default function createRouteMap(routes, oldPathList, oldpathMap) { let pathList = oldPathList || []; let pathMap = oldpathMap || Object.create(null); routes.forEach(route => { addRouteRecord(route, pathList, pathMap); }) return { pathList, pathMap } } function addRouteRecord(route, pathList, pathMap,parnent) { let path = parnent ? `${parnent.path}/${route.path}`: route.path; let record = { path: route.path, component: route.component, parnent } if (!pathMap[path]) { pathList.push(path); pathMap[path] = record; } if(route.children){ route.children.forEach(route=>{ addRouteRecord(route, pathList, pathMap,record) }) } }
createRoute
`createRoute`是生成`$route`的函式,傳入引數為扁平化配置,路徑。將`res`作為空陣列,如果傳進來的扁平化配置有值,則進行`while`迴圈,將自己從陣列頭部插入,取出父元件再從頭部插入,如此反覆,得到一個含著層次關係的陣列。將`loaction`和陣列包裹為物件返回。
export function createRoute(record, location) { let res = []; if (record) { while (record) { res.unshift(record) record = record.parnent } } return { ...location, matched: res } }
router-view
然後在來看看`routerview`
是一個函式式元件,
返回`render`方法
進行`while`迴圈,遍歷出巢狀的`routerview`
用`depth`作為深度,也是`matched`的`index`.
每遍歷一次,就在`$vnode.data.rouView` 改為`true`,將深度加1
返回對應的元件即可
export default { name:'routerView', functional: true, render(h,{parent,data}) { let route = parent.$route let depth = 0; while (parent) { if (parent.$vnode && parent.$vnode.data.routeView) { depth++; } parent = parent.$parent; } data.routeView = true; let record = route.matched[depth]; if (!record) { return h(); } return h(record.component, data); } }
router-link
再來看看`routerlink`
沒什麼東西就返回一個`a`標籤,用插槽把對應的文字顯示出來,在新增的跳轉事件
呼叫`$router`的`push`方法,也就是`Router`類上的`push`
export default { name: 'routerLink', props: { to: { type: String, required: true }, tag: { type: String, default: 'a' } }, methods: { handler(to) { this.$router.push(to) // 路由跳轉 } }, render() { return <a onClick={this.handler.bind(this,this.to)}>{this.$slots.default[0].text}</a> } }
vuerouter草稿:https://juejin.im/post/6862215979745673224