vue-router@3.x实现分析

本文最后更新于:2022年5月31日 早上

vue-router

模式

hash模式

原理:监听onHashChange和修改URLhash#xxx

  • 不刷新页面
  • hash不会发送到服务器
  • hash的改变会保存历史记录
  • 通过<a>标签的href或者location.hash修改

history模式

原理:监听history.popState,通过pushState``replaceState修改历史栈

  • 不刷新页面
  • H5 history API
  • 服务器需要配合设置地址不匹配回退
  • SEO比 hash 好

abstract模式

参数

  • params
const routes = [
	// 动态字段以冒号开始
	{ path: "/users/:id", component: User },
];
  • query

URL 地址的 query 部分

/users?id=123

路由钩子

// 全局
router.beforeEach((to,form,next)) // 导航触发时
router.beforeResolve((to,form,next)) // 所有组件内守卫和异步路由组件被解析之后
router.afterEach((to, from, failure))
// 全局钩子可以定义多个,顺序执行完后才会resolve路由

// 路由
beforeEnter() // 不会在 params、query 或 hash 改变触发,只会在导航路径改变触发
{
  path:'/a',
  component:()=>import('@/component/A'),
  beforeEnter:((to,from,next))=>{} //
}

// 组件内
beforeRouteEnter((to,from,next:(vm)=>{/*访问this*/})) // 组件实例创建之前
beforeRouteUpdate((to,from))
/* CompositionAPI onBeforeRouteUpdate */
beforeRouteLeave((to,from))
/* CompositionAPI onBeforeRouteLeave */

触发流程:pageA=>pageB

  1. pageA.beforeRouteLeave

  2. router.beforeEach

  3. routes[pageB].beforeEnter

  4. pageB.beforeRouteEnter

    如果 pageB 渲染过,忽略【3,4】执行 pageB.beforeRouteUpdate

  5. router.beforeResolve

  6. router.afterEach

  7. DOM 更新/挂载

  8. 执行第【4】步中next回调

初始化阶段

Vue.use 安装路由

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;
		// 伪代码
		registerRouteInstance(vm, callVal);
		// router-view render函数的registerRouteInstance方法
	};

	Vue.mixin({
		beforeCreate() {
			if (isDef(this.$options.router)) {
				this._routerRoot = this; //  路由的root组件
				this._router = this.$options.router; // 路由实例
				this._router.init(this); // init见下文
				Vue.util.defineReactive(this, "_route", this._router.history.current);
				// 🔥对router-view的响应式绑定
			} else {
				this._routerRoot = (this.$parent && this.$parent._routerRoot) || this;
				// 如果是子组件,将追溯到拥有router的父组件作为root,执行上面几行逻辑
			}
			registerInstance(this, this);
		},
		destroyed() {
			registerInstance(this);
		},
	});

	// 为Vue的原型绑定`$router``$route`的getter和全局注册两个组件
	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);

	const strats = Vue.config.optionMergeStrategies;
	strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created;
}

实例化一个 router

class VueRouter {
	constructor(options: RouterOptions = {}) {
		this.app = null; // 根组件
		this.apps = []; // options中有router项的实例
		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) {
			// 回退到hash模式
			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;
		}
	}

	init(app: any) {
		this.apps.push(app);

		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;
			});
		});
	}
}

路径匹配

createMatcher

export function createMatcher(routes: Array<RouteConfig>, router: VueRouter): Matcher {
	// 根据routes构建出path的集合pathList,
	// path 到 RouteRecord的映射表pathMap
	// name 到 RouteRecord的映射表nameMap
	//  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 }
	// }

	const { pathList, pathMap, nameMap } = createRouteMap(routes);

	// 动态添加路由的方法
	function addRoutes(routes) {
		createRouteMap(routes, pathList, pathMap, nameMap);
	}

	//
	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 (!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];
					}
				}
			}

			if (record) {
				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);
				}
			}
		}
		return _createRoute(null, location);
	}

	// ...

	function _createRoute(record: ?RouteRecord, location: Location, redirectedFrom?: Location): Route {
		// 重定向
		if (record && record.redirect) {
			return redirect(record, redirectedFrom || location);
		}
		// alias
		if (record && record.matchAs) {
			return alias(record, location, record.matchAs);
		}
		return createRoute(record, location, redirectedFrom, router);
	}

	//
	function createRoute(
		record: ?RouteRecord,
		location: Location,
		redirectedFrom?: ?Location,
		router?: VueRouter
	): Route {
		const stringifyQuery = router && router.options.stringifyQuery;

		let query: any = location.query || {};
		try {
			query = clone(query);
		} catch (e) {}

		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) : [],
		};
		if (redirectedFrom) {
			route.redirectedFrom = getFullPath(redirectedFrom, stringifyQuery);
		}
		return Object.freeze(route);
	}

	return {
		match,
		addRoutes,
	};
}

路径切换

transitionTo (location: RawLocation, onComplete?: Function, onAbort?: Function) {
  const route = this.router.match(location, this.current)
  // 根据目标 location 和当前路径 this.current 执行 this.router.match 方法去匹配到目标的路径

  this.confirmTransition(route, () => {
    this.updateRoute(route)
    onComplete && onComplete(route)

    if (!this.ready) {
      this.ready = true
      this.readyCbs.forEach(cb => { cb(route) })
    }
  }
  //...
  )
}

updateRoute (route: Route) {
  const prev = this.current
  this.current = route
  this.cb && this.cb(route)
  this.router.afterHooks.forEach(hook => { /* router.afterEach */
    hook && hook(route, prev)
  })
}

confirmTransition

confirmTransition (route: Route, onComplete: Function, onAbort?: Function) {
  const current = this.current

  const {
    updated,
    deactivated,
    activated
  } = resolveQueue(this.current.matched, route.matched)
  // matched数组属性保存了从匹配到的record循环向上一直到最外层的所有的record
  // resolve解析出三个队列
  // a/b/c => a/b/d
  // deactivated : [c]
  // activate : [d]
  // updated : [a,b]

  // 生成一个按照路由钩子顺序的队列
  const queue = [].concat(
    extractLeaveGuards(deactivated), /* 执行离开的钩子 */
    this.router.beforeHooks, /* router.beforeEach */
    extractUpdateHooks(updated), /*  beforeRouteUpdate */
    activated.map(m => m.beforeEnter), /* beforeRouteEnter */
    resolveAsyncComponents(activated) /*解析异步路由组件*/
  )


  runQueue(queue, iterator, () => {
    const postEnterCbs = []
    const isValid = () => this.current === route
    // beforeRouteEnter
    const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid)
    // ⭐️beforeRouteEnter的回调会添加到postEnterCbs数组中,等路由更新后在最后执行

    // router.beforeResolve
    const queue = enterGuards.concat(this.router.resolveHooks)

    runQueue(queue, iterator, () => {
      onComplete(route)
      if (this.router.app) {
        this.router.app.$nextTick(() => {
          postEnterCbs.forEach(cb => { cb() })
          // ⭐️执行postEnterCbs中的回调
        })
      }
    })
  })
}

<router-view>

把根 Vue 实例的 _route 属性是响应式的,在每个 <router-view>render 函数的时候,都会访问 parent.$route,该<router-view> 就订阅了router的变化

export default {
	name: "RouterView",
	functional: true,
	props: {
		name: {
			type: String,
			default: "default",
		},
	},
	render(_, { props, children, parent, data }) {
		data.routerView = true;

		const h = parent.$createElement;
		const name = props.name;

		const route = parent.$route; // 🔥执行时会触发响应式收集为router的依赖,当router更新时重新执行

		const cache = parent._routerViewCache || (parent._routerViewCache = {});

		while (parent && parent._routerRoot !== parent) {
			parent = parent.$parent;
		}

		const component = (cache[name] = matched.components[name]);

		// 给 matched.instances[name] 赋值当前组件的 vm 实例
		data.registerRouteInstance = (vm, val) => {
			const current = matched.instances[name];
			if ((val && current !== vm) || (!val && current === vm)) {
				matched.instances[name] = val;
			}
		};
		(data.hook || (data.hook = {})).prepatch = (_, vnode) => {
			matched.instances[name] = vnode.componentInstance;
		};

		return h(component, data, children);
	},
};

归纳

  1. 在每个组件的beforeCreatedestroyed钩子中混入路由方法
  2. 依据routes中的路由配置,递归生成path/name映射到组件的映射表RouteRecords
  3. 对实例的_route属性绑定响应式
  4. 根据选择的mode不同,生成不同的history模式,与地址栏绑定修改
  5. <router-view>组件的渲染函数会访问_route属性,订阅了路由状态的改变
  6. 地址栏或者router-link改变后修改了_route
  7. matcher计算出从当前url到目标url的改变路径(matched record数组)
  8. 对比新旧的matched解析出activated``updated``deactivated三个队列
  9. 对这三个队列中的组件分别执行相应的执行离开钩子,更新钩子和进入钩子,全局的钩子也会在相应的位置执行
  10. 通知router-view组件重新渲染,router-view会计算自己组件在vue组件树中位置,去matched中找到该渲染的组件
  11. 最后更新路由实例状态

vue-router@3.x实现分析
http://yoursite.com/2022/03/10/[源码]vue2-router/
作者
tatekii
发布于
2022年3月10日
许可协议