Open vivipure opened 2 years ago
Vue Router 的使用频率还是很高的,作为开发者,我们可能知道 hash路由和 history路由 的区别和实现原理。但是一些还是东西是值得理解的。
这里就不介绍基本的使用
项目地址:https://github.com/vuejs/vue-router 构建工具: Rollup 入口文件:src/index.js
看下 install 函数执行的逻辑
export function install (Vue) { // 避免重复注册 if (install.installed && _Vue === Vue) return install.installed = true _Vue = Vue const isDef = v => v !== undefined // 暂时不知道少用 const registerInstance = (vm, callVal) => { let i = vm.$options._parentVnode if ( isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance) ){ i(vm, callVal) } } // 混入逻辑, Vue.mixin({ beforeCreate () { if (isDef(this.$options.router)) { // 自身 this._routerRoot = this // 路由实例 this._router = this.$options.router this._router.init(this) // 定义route,指向当前激活路由 Vue.util.defineReactive( this, '_route', this._router.history.current) } else { // 由于是树形结构,因此子组件会找到离自己最近的 带有router的组件 this._routerRoot = (this.$parent && this.$parent._routerRoot) || this } registerInstance(this, this) }, destroyed () { registerInstance(this) } }) // 定义全局属性 Object.defineProperty(Vue.prototype, '$router', { get () { return this._routerRoot._router } }) Object.defineProperty(Vue.prototype, '$route', { get () { return this._routerRoot._route } }) // 组件注册 Vue.component('RouterView', View) Vue.component('RouterLink', Link) }
相关注释我都写在代码中了,主要逻辑就是在组件中混入了路由的属性,定义全局的属性,注册了两个内置组件。
这里比较巧妙的是通过树形结构的特性,保证了拥有 options.router 的组件进行了路由初始化,子组件根据父组件层层查找,找到离自己最近的带有 router 的组件。
options.router
router
然后 registerInstance 方法暂时不知道用法,
registerInstance
VueRouter 类在 src/index.js 中,默认导出的就是 VueRouter, 我们在业务开发时也通过实例化 VueRouter 来生成 router 给应用使用。
VueRouter
src/index.js
constructor (options: RouterOptions = {}) { this.app = null this.apps = [] this.options = options this.beforeHooks = [] this.resolveHooks = [] this.afterHooks = [] this.matcher = createMatcher(options.routes || [], this) // 根据options结合实际浏览器确定 路由模式 let mode = options.mode || 'hash' this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false if (this.fallback) { mode = 'hash' } // 如果不在浏览器则为 abstract模式 if (!inBrowser) { mode = 'abstract' } this.mode = mode // 根据路由模式生成 history 对象 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}`) } } }
这里主要的逻辑就是通过options.router 生成 matcher, 和通过路由模式生成对应的 history对象. 通过这里我才知道原来 路由还有 abstract 模式, 提供给服务端或者ssr模式使用,应该和V4版本的 Memory mode是一样的。
matcher
history
abstract
Memory mode
在 install的过程中,VueRouter 往 Vue 中 beforeCreated 逻辑, 对有 router 的 options 进行了路由初始化
install
Vue
beforeCreated
options
this._router.init(this)
因此我们再看看 router 的 init 方法
init
init (app: any) { // 将当前组件推入 app 中 this.apps.push(app) if (this.app) { return } this.app = app const history = this.history // 属于hash 和 history 模式 if (history instanceof HTML5History || history instanceof HashHistory) { // scrollBehavior支持 const handleInitialScroll = routeOrError => { const from = history.current const expectScroll = this.options.scrollBehavior const supportsScroll = supportsPushState && expectScroll if (supportsScroll && 'fullPath' in routeOrError) { handleScroll(this, routeOrError, from, false) } } const setupListeners = routeOrError => { history.setupListeners() handleInitialScroll(routeOrError) } // 切换到当前链接对应的路由 history.transitionTo( history.getCurrentLocation(), setupListeners, setupListeners ) } // 路由更新后,更新组件的_route history.listen(route => { this.apps.forEach((app) => { app._route = route }) }) }
初始化时将当前组件进行保存,将当前路由切换到当前链接对应路由,也设置了订阅,当 history 改变时,会更新组件的 route。
route
看下 tansitionTo 的实际逻辑
tansitionTo
// 匹配到路由 route = this.router.match(location, this.current) const prev = this.current // 进行切换动画 this.confirmTransition( route, () => { // 更新当前路由 this.updateRoute(route) onComplete && onComplete(route) // 更改url this.ensureURL() // hook this.router.afterHooks.forEach(hook => { hook && hook(route, prev) }) // 初始化回调 if (!this.ready) { ... } }, err => { // 错误处理 }) confirmTransition (route: Route, onComplete: Function, onAbort?: Function) { const current = this.current this.pending = route // 错误处理 const abort = err => {} // 当前路由重复导航处理 if (isDuplicatedRoute) { this.ensureURL() if (route.hash) { handleScroll(this.router, current, route, false) } return abort(createNavigationDuplicatedError(current, route)) } const { updated, deactivated, activated } = resolveQueue( this.current.matched, route.matched ) const queue: Array<?NavigationGuard> = [].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(m => m.beforeEnter), // async components resolveAsyncComponents(activated) ) const iterator = (hook: NavigationGuard, next) => { if (this.pending !== route) { return abort(createNavigationCancelledError(current, route)) } try { hook(route, current, (to: any) => { if (to === false) { // next(false) -> abort navigation, ensure current URL this.ensureURL(true) abort(createNavigationAbortedError(current, route)) } else if (isError(to)) { this.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(createNavigationRedirectedError(current, route)) if (typeof to === 'object' && to.replace) { this.replace(to) } else { this.push(to) } } else { // confirm transition and pass on the value next(to) } }) } catch (e) { abort(e) } } runQueue(queue, iterator, () => { // wait until async components are resolved before // extracting in-component enter guards const enterGuards = extractEnterGuards(activated) const queue = enterGuards.concat(this.router.resolveHooks) runQueue(queue, iterator, () => { if (this.pending !== route) { return abort(createNavigationCancelledError(current, route)) } this.pending = null onComplete(route) if (this.router.app) { this.router.app.$nextTick(() => { handleRouteEntered(route) }) } }) }) }
这里主要执行 confirmTransition 方法,主要逻辑分为几步:
confirmTransition
reolveQuene
beforeLeave
beforHooks
update
beforeEnter hook
resolveHooks
nextTick
afterEach
上面的路由切换方法中,进场会比较 route 的 matched 属性,而在构造函数中也有
matched
this.matcher = createMatcher(options.routes || [], this)
因此我们来研究下 matcher
function createMatcher ( routes: Array<RouteConfig>, router: VueRouter ): Matcher { const { pathList, pathMap, nameMap } = createRouteMap(routes) ... return { match, addRoute, getRoutes, addRoutes }
首先是根据传入的 routes 通过 createRouteMap 解析为 list 和 map .
routes
createRouteMap
list
map
pathMap, nameMap 是存放了 path 和 name 对 RouteRecord 的映射
const record: RouteRecord = { path: normalizedPath, // 规范后的路径 regex: compileRouteRegex(normalizedPath, pathToRegexpOptions), // 路径对应正则 components: route.components || { default: route.component }, // 组件 instances: {}, name, parent, matchAs, redirect: route.redirect, beforeEnter: route.beforeEnter, meta: route.meta || {}, props: route.props == null ? {} : route.components ? route.props : { default: route.props } }
在平时的业务开发中,路由的配置不是固定的,根据用户的权限生产对应的路由才是合理的。因此会使用两个方法:
function addRoutes (routes) { createRouteMap(routes, pathList, pathMap, nameMap) }
addRoute
function addRoute (parentOrRoute, route) { const parent = (typeof parentOrRoute !== 'object') ? nameMap[parentOrRoute] : undefined createRouteMap([route || parentOrRoute], pathList, pathMap, nameMap, parent) if (parent && parent.alias.length) { createRouteMap( parent.alias.map(alias => ({ path: alias, children: [route] })), pathList, pathMap, nameMap, parent ) } }
逻辑都很简单,直接调用 createRouteMap 的方法即可。在添加单个 route 时则会处理父级路由和别名的相关逻辑。
createMatcher 的返回值中还包括 match 方法, 通过这个方法将 Location 转化为 Route. 这里的 Location 就是我们平时使用 push 等方法传入的参数。
createMatcher
match
Location
Route
type Location = { _normalized?: boolean; name?: string; path?: string; hash?: string; query?: Dictionary<string>; params?: Dictionary<string>; append?: boolean; replace?: boolean; } type RawLocation = string | Location
现在看看 match 方法的执行逻辑
function match ( raw: RawLocation, currentRoute?: Route, redirectedFrom?: Location ): Route { const location = normalizeLocation(raw, currentRoute, false, router) const { name } = location if (name) { const record = nameMap[name] if (process.env.NODE_ENV !== 'production') { warn(record, `Route with name '${name}' does not exist`) } if (!record) return _createRoute(null, location) const paramNames = record.regex.keys .filter(key => !key.optional) .map(key => key.name) if (typeof location.params !== 'object') { location.params = {} } if (currentRoute && typeof currentRoute.params === 'object') { for (const key in currentRoute.params) { if (!(key in location.params) && paramNames.indexOf(key) > -1) { location.params[key] = currentRoute.params[key] } } } location.path = fillParams(record.path, location.params, `named route "${name}"`) return _createRoute(record, location, redirectedFrom) } else if (location.path) { location.params = {} for (let i = 0; i < pathList.length; i++) { const path = pathList[i] const record = pathMap[path] if (matchRoute(record.regex, location.path, location.params)) { return _createRoute(record, location, redirectedFrom) } } } // no match return _createRoute(null, location) }
location
name
nameMap
path
pathList
最终的到的东西就是一个 Route对象
const route: Route = { name: location.name || (record && record.name), meta: (record && record.meta) || {}, path: location.path || '/', hash: location.hash || '', query, params: location.params || {}, fullPath: getFullPath(location, stringifyQuery), matched: record ? formatMatch(record) : [] // 这里通过遍历父级,得到完整的路径数组 }
由于 这个对象最终被 Object.freeze() ,因此实际使用时,我们无法更改上面的属性
router-link 自动处理了 a 标签 点击跳转的情况,在点击时会触发路由跳转的事件
const handler = e => { // 默认事件 if (guardEvent(e)) { if (this.replace) { router.replace(location, noop) } else { router.push(location, noop) } } }
render (_, { props, children, parent, data }) { // routerView标识 data.routerView = true const h = parent.$createElement const name = props.name const route = parent.$route let depth = 0 let inactive = false while (parent && parent._routerRoot !== parent) { const vnodeData = parent.$vnode ? parent.$vnode.data : {} if (vnodeData.routerView) { depth++ } // keep-alive 逻辑 if (vnodeData.keepAlive && parent._directInactive && parent._inactive) { inactive = true } parent = parent.$parent } // 得到routerview的深度,确定当前的路由 data.routerViewDepth = depth // render previous view if the tree is inactive and kept-alive if (inactive) { const cachedData = cache[name] const cachedComponent = cachedData && cachedData.component if (cachedComponent) { if (cachedData.configProps) { fillPropsinData(cachedComponent, data, cachedData.route, cachedData.configProps) } return h(cachedComponent, data, children) } else { return h() } } const matched = route.matched[depth] const component = matched && matched.components[name] // cache component cache[name] = { component } // 这个方法也是在初始化的时候进行的调用 // 用来设置当前路由匹配的组件实例 data.registerRouteInstance = (vm, val) => { const current = matched.instances[name] if ( (val && current !== vm) || (!val && current === vm) ) { matched.instances[name] = val } } // hook注入 data.hook.prepatch = (_, vnode) => { matched.instances[name] = vnode.componentInstance } data.hook.init = (vnode) => { if (vnode.data.keepAlive && vnode.componentInstance && vnode.componentInstance !== matched.instances[name] ) { matched.instances[name] = vnode.componentInstance } handleRouteEntered(route) } const configProps = matched.props && matched.props[name] return h(component, data, children) }
主要逻辑就是:
看完核心逻辑后,我最初的疑问基本得到了解答。感觉还是挺有收获的。知道了路由的第三种模式,路由切换的整体过程,路由的匹配逻辑。也了解了 router-view 这种函数式组件的实现。 我还有一个问题未得到答案
3. 和Transition是怎么搭配合作的
看来只有到时候看 Vue 源码时才能有收获了.
1. 我的疑问
Vue Router 的使用频率还是很高的,作为开发者,我们可能知道 hash路由和 history路由 的区别和实现原理。但是一些还是东西是值得理解的。
2. 基本介绍
这里就不介绍基本的使用
项目地址:https://github.com/vuejs/vue-router 构建工具: Rollup 入口文件:src/index.js
3. 入口文件分析
3.1 install
看下 install 函数执行的逻辑
相关注释我都写在代码中了,主要逻辑就是在组件中混入了路由的属性,定义全局的属性,注册了两个内置组件。
这里比较巧妙的是通过树形结构的特性,保证了拥有
options.router
的组件进行了路由初始化,子组件根据父组件层层查找,找到离自己最近的带有router
的组件。然后
registerInstance
方法暂时不知道用法,3.2 VueRouter
VueRouter
类在src/index.js
中,默认导出的就是VueRouter
, 我们在业务开发时也通过实例化VueRouter
来生成router
给应用使用。3.2.1 构造函数
这里主要的逻辑就是通过
options.router
生成matcher
, 和通过路由模式生成对应的history
对象. 通过这里我才知道原来 路由还有abstract
模式, 提供给服务端或者ssr模式使用,应该和V4版本的Memory mode
是一样的。3.2.2 init
在
install
的过程中,VueRouter
往Vue
中beforeCreated
逻辑, 对有router
的options
进行了路由初始化因此我们再看看
router
的init
方法初始化时将当前组件进行保存,将当前路由切换到当前链接对应路由,也设置了订阅,当
history
改变时,会更新组件的route
。3.2.3 transitionTo
看下
tansitionTo
的实际逻辑这里主要执行
confirmTransition
方法,主要逻辑分为几步:reolveQuene
筛选出当前路由和跳转路由的差异beforeLeave
路由守卫beforHooks
update
路由守卫beforeEnter hook
beforeEnter hook
resolveHooks
执行完成后,在nextTick
后执行路由完成后的回调操作,调用全局的afterEach
钩子。3.2.4 matcher
上面的路由切换方法中,进场会比较
route
的matched
属性,而在构造函数中也有因此我们来研究下
matcher
首先是根据传入的
routes
通过createRouteMap
解析为list
和map
.pathMap, nameMap 是存放了 path 和 name 对 RouteRecord 的映射
在平时的业务开发中,路由的配置不是固定的,根据用户的权限生产对应的路由才是合理的。因此会使用两个方法:
addRoute
逻辑都很简单,直接调用
createRouteMap
的方法即可。在添加单个route
时则会处理父级路由和别名的相关逻辑。createMatcher
的返回值中还包括match
方法, 通过这个方法将Location
转化为Route
. 这里的Location
就是我们平时使用 push 等方法传入的参数。现在看看 match 方法的执行逻辑
location
包含name
那么在nameMap
中通过name
取到路由path
则通过遍历pathList
, 正则匹配到对应的路由 从这里我们可以知道,如果传递name
来获取路由是比较方便的,path
的话会进行比较匹配,写在前面会被优先匹配到的最终的到的东西就是一个 Route对象
由于 这个对象最终被 Object.freeze() ,因此实际使用时,我们无法更改上面的属性
4 内置组件
4. 1 router-link
router-link 自动处理了 a 标签 点击跳转的情况,在点击时会触发路由跳转的事件
4.2 router-view
主要逻辑就是:
5. 最后总结
看完核心逻辑后,我最初的疑问基本得到了解答。感觉还是挺有收获的。知道了路由的第三种模式,路由切换的整体过程,路由的匹配逻辑。也了解了 router-view 这种函数式组件的实现。 我还有一个问题未得到答案
看来只有到时候看 Vue 源码时才能有收获了.