Vue Router 源码学习

发布于 2023-07-12 16:27:41 字数 15267 浏览 37 评论 0

1. 我的疑问

Vue Router 的使用频率还是很高的,作为开发者,我们可能知道 hash 路由和 history 路由 的区别和实现原理。但是一些还是东西是值得理解的。

  1. 内置组件 router-view 是怎么实现的
  2. 路由守卫是怎么实现的
  3. 和 Transition 是怎么搭配合作的
  4. routes 数据是怎么解析的
  5. 和 keep-alive 是怎么配合

2. 基本介绍

这里就不介绍基本的使用

项目地址: https://github.com/vuejs/vue-router
构建工具: Rollup
入口文件:src/index.js

3. 入口文件分析

3.1 install

看下 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 的组件。

然后 registerInstance 方法暂时不知道用法,

3.2 VueRouter

VueRouter 类在 src/index.js 中,默认导出的就是 VueRouter , 我们在业务开发时也通过实例化 VueRouter 来生成 router 给应用使用。

3.2.1 构造函数

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 是一样的。

3.2.2 init

install 的过程中, VueRouterVuebeforeCreated 逻辑, 对有 routeroptions 进行了路由初始化

this._router.init(this)

因此我们再看看 routerinit 方法

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

3.2.3 transitionTo

看下 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 方法,主要逻辑分为几步:

  1. 检查是否重复路由,进行处理
  2. reolveQuene 筛选出当前路由和跳转路由的差异
  3. 然后对迭代一个队列,队列包含
    1. 激活失效组件 beforeLeave 路由守卫
    2. 全局路由 beforHooks
    3. 重用组件 update 路由守卫
    4. 激活路由 配置的 beforeEnter hook
    5. 处理异步组件加载逻辑
      如果顺序执行中有一个任务失败,则不会继续下面的任务
  4. 队列跑完之后,执行新的队列任务。队列包括
    1. 激活组件的 beforeEnter hook
    2. 全局路由的 resolveHooks
      执行完成后,在 nextTick 后执行路由完成后的回调操作,调用全局的 afterEach 钩子。

3.2.4 matcher

上面的路由切换方法中,进场会比较 routematched 属性,而在构造函数中也有

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 解析为 listmap .

  • 将用户传入的配置进行遍历,对每个路由生成 RouteRecord 存放到 pathList
  • 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 }
 }

在平时的业务开发中,路由的配置不是固定的,根据用户的权限生产对应的路由才是合理的。因此会使用两个方法:

  1. addRoutes
function addRoutes (routes) {
  createRouteMap(routes, pathList, pathMap, nameMap)
}
  1. 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 等方法传入的参数。

 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 中通过 name 取到路由
  • 如果包含 path 则通过遍历 pathList , 正则匹配到对应的路由
    从这里我们可以知道,如果传递 name 来获取路由是比较方便的, path 的话会进行比较匹配,写在前面会被优先匹配到的

最终的到的东西就是一个 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() ,因此实际使用时,我们无法更改上面的属性

4 内置组件

4.1 router-link

router-link 自动处理了 a 标签 点击跳转的情况,在点击时会触发路由跳转的事件

const handler = e => {
	// 默认事件
	if (guardEvent(e)) {
		if (this.replace) {
		router.replace(location, noop)

		} else {

		router.push(location, noop)

		}
	}
}

4.2 router-view

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)

}

主要逻辑就是:

  1. 标识当前路由为 routerview
  2. 往父级遍历,得到当前的 routerview 深度,确定 route
  3. 兼容 keep-alive 的逻辑,处理缓存逻辑
  4. 渲染组件

5. 最后总结

看完核心逻辑后,我最初的疑问基本得到了解答。感觉还是挺有收获的。知道了路由的第三种模式,路由切换的整体过程,路由的匹配逻辑。也了解了 router-view 这种函数式组件的实现。
我还有一个问题未得到答案

3. 和 Transition 是怎么搭配合作的

看来只有到时候看 Vue 源码时才能有收获了.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

文章
评论
328 人气
更多

推荐作者

櫻之舞

文章 0 评论 0

弥枳

文章 0 评论 0

m2429

文章 0 评论 0

野却迷人

文章 0 评论 0

我怀念的。

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文