如果你不知道脚下的路怎么走了,过来看看Vue-router的实现原理吧
大家好👋我是某行人😁,作为2022年第一篇技术文章,首先作为一名Vue的忠实粉丝,了解原理是必不可少的,那Vue-router作为Vue框架的一个插件也充当着一个重要的角色,我们也需要了解其中的实现原理了,目前行情表示,对于工作几年的码农只会用vue的已经不怎么值钱了,为什么这么说,因为刚毕业的实习生都会熟练运用Vue,有可能比老技术人员使用的还要熟练,所以说作为一名公司元老级别的技术人员,原理性的东西必须要理解,这样自己在公司才能稳住脚,俗话说的好:要想稳住脚,头发必不保😂,好了!废话不多说,现在就开始。
首先环境搭建 创建myRouter文件夹,依次在里面创建下面的几个文件
- 创建一个index.html
- 引入Vue,自己去cdn去Copy链接
- 测试一下,并在浏览器上测试
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app"></div>
</body>
<script>
const app = new Vue({
el:'#app',
template:` <div>
<h1>app</h1><router-link to="/home"><p>/home</p></router-link><router-view></router-view>
</div>`,
})
</script>
</html>
创建History
```js
// 创建路由 注意此函数在其他地方会得到复用
function createRoute(record,location){
// path matched
// 我们在Vue打印router中的matched属性的时候会发现他是一个存放着的是路由数组
let res = []
if(record){
while(record){
// 先渲染父级后渲染自己组件
res.unshift(record); // 递归父级因为需要 一层一层的去渲染
record = record.parent
}
}
return {
...location,
matched:res
}
}
class History{
constructor(router){
this.router = router;
// 默认路由中应该保存一个当前路径,后续会更改这个路径
this.current = createRoute(null,{
path:'/',
})
}
// 过度路径 location 代表跳转路径 complete当前跳转成功后执行的方法
transitionTo(location,onComplete){
//
let route = this.router.match(location) // 我要用当前路径,找出对应的记录
// route 就是当前路径要匹配的路径
if(this.current.path===location && route.matched.length === this.current.matched.length){
return;
}
// 更新路由
this.updateRoute(route);
// 下面主要监听路由的前进与回退
onComplete && onComplete();
}
// 路由更新
updateRoute(route){
this.current = route;
// 执行回调 我先把逻辑写出来 这里不需要理解此函数功能,到了下面VueRouter实例中就会明白
this.cb && this.cb(this.current);
}
listen(cb){
this.cb = cb; //这里是在init下定义的回调函数
}
}
```
HashHistory实例对象
其实HashHistory与History功能是一样的就是路由一个带#一个不带,我们直接继承History就可以了
// 这里我们先创建两个工具函数帮助我们主力Hash路由
// 取hash戳
function gethash(){
return window.location.hash.slice(1);
}
// 如果路径没有路由
function ensureSlash(){
if(window.location.hash){
return
}
window.location.hash = '/';
}
// 继承路由对象window.history
class HashHistory extends History{
constructor(router){
super(router);
// 默认路由 /
ensureSlash(); // 判断跟路由是否存在
this.setupListener(); // 这里初始化 就需要做监听 不然 不会做监听
}
// 获取hash路由
getCurrentLocation(){
return gethash();
}
// 设置监听
setupListener(){
window.addEventListener('hashchange',()=>{
this.transitionTo(gethash());
})
}
}
创建路由映射表与递归处理器
// 递归处理
// 主要处理每个路由的子集路由
function addRouteRecord(route,pathList,pathMap,parent){
let path = parent?`${parent.path}/${route.path}`:route.path;
let recod = {
path,
component:route.component,
parent
}
if(!pathMap[path]){
pathList.push(path),
pathMap[path] = recod
}
if(route.children){
route.children.forEach(child=>{
addRouteRecord(child,pathList,pathMap,recod)
})
}
}
// 路由映射表
function createRoutermap(routes,oldPathList,oldPathMap){
// 这里我们直接使用path坐标标识
let pathList = oldPathList || [];
let pathMap = oldPathMap || Object.create(null);
routes.forEach(route => {
addRouteRecord(route,pathList,pathMap)
});
return{
pathList,
pathMap,
}
}
创建匹配器
function createMatcher(router){
// 核心作用将 tree路由转化为扁平化路由,创建路由映射表
// [/,/home] -> {/:记录,/home:记录}
let {pathList,pathMap} = createRoutermap(router);
console.log(pathList,pathMap)
function addRoutes(routes){
createRoutermap(routes)
}
function match(location){
let record = pathMap[location.replace(/\/$/,'')];
let local = {path:location};
// 如果记录存在 开始创建路由
if(record){
return createRoute(record,local); // 注意这里的createRoute
}
return createRoute(null,local)
}
return {
match,
addRoutes
}
}
创建VueRouter实例对象
```js
// router 实例
class VueRouter{
constructor(options){
this.matcher = createMatcher(options.routes || []);
// 创建路由系统 ,根据模式创建
this.mode = options.mode || 'hash';
this.history = new HashHistory(this);// 注意这里的this
}
init(app){
const history = this.history;
const setupHash = ()=>{
history.setupListener();// 监听路由变化
}
history.transitionTo(history.getCurrentLocation(),setupHash);
// 这里对比我们在创建HashHistory的时候有个listen函数就会理解listen中回调是拿来的了
history.listen((route)=>{
//app 指的是根实例 _route 我们的响应式声明 我们会install函数中进行声明
app._route = route;
})
}
// 用来匹配路径
match(location){
// {path:'about/a'}
// 这里不光需要找到 a 的记录 还需要找到 a 的父级页面的记录
// 将about与a 存放到一个数组内
console.log(location,'location')
return this.matcher.match(location);
}
}
```
创建VueRouter.install函数
```js
function install(Vue){
// 混入mixin,这里与我们Vue3中的一些Hooks钩子插件类似,因为Vue-router本身就是一个基于Vue的插件
Vue.mixin({
beforeCreate(){
// 深度赋值
// 因为除了main其他的相当于子组件,没有router属性,所以需要通过父级去获取
// 根实例
if(this.$options.router){
this._routerRoot = this;
this._router = this.$options.router
// 初始化
this._router.init(this) // 注意这的this
// 将里面的变量变为响应式 _route
Vue.util.defineReactive(this,'_route',this._router.history.current);
}else{
this._routerRoot = this.$parent && this.$parent._routerRoot;
}
}
})
}
```
挂载router
Object.defineProperty方法不做过多解释,网上有很多解释,看过Vue响应式原理的应该明白
```js // route',{ // 这里的this指向根组件 get(){
return this._routerRoot._route;
}
})
// $router 设定
Object.defineProperty(Vue.prototype,'$router',{ // 这里的this指向根组件
get(){
return this._routerRoot._router;
}
})
```
router-link
```js
const link = {
functional:true,
props:{
to:String
},
// render 会有两个参数 一个是 h 函数主要作用是将虚拟dom变为真实dom,第二个参数就是content->{parent,data,slots,props}
render(h,{parent,data,slots,props}){
let mode = parent.$root._router.mode;
let to = mode === "hash"?"#"+props.to:props.to;
console.log(slots().default,props)
return h('a',{attrs:{href:to}},slots().default)
}
}
```
router-view
const view = {
functional:true,
render(h,{parent,data}){
let route = parent.$route;
let matched = route.matched;
data.routerView = true; // 这里可以理解为 当前组件式routerView组件
let dept = 0; // 深度
// 循环父组件
// 当父组件 渲染完页面后发现内容区域还存在router-view组件,那需要深层便利父组件,增加dept深度
while(parent){
// 如果父组件存在虚拟节点 并且存在routerView属性,深度整加
if(parent.$vnode && parent.$vnode.data.routerView){
dept++;
}
parent = parent.$parent;
}
let record = matched[dept];
// 判断记录是否存在 如果不存在 就渲染空 存在 就渲染组件实例
if(!record){
return h();
}
let component = record.component;
return h(component,data);
}
}
全局组件
Vue.component('router-link',link)
Vue.component('router-view',view)
创建路由&挂载VueRouter实例
const root = {
name:'root',
template:`<h1>root<router-link to="/home"><p>/home</p></router-link></h1>`
}
const home = {
name:'root',
template:`<div>
<router-link to="/home/b"><p>/home/b</p></router-link>
<router-link to="/home/a"><p>/home/a</p></router-link>
<h1>home <router-view></router-view></h1>
</div>`
}
const homea = {
name:'root',
template:`<h1>homea</h1>`
}
const homeb = {
name:'root',
template:`<h1>homeb</h1>`
}
// 挂载
VueRouter.install = install;
Vue.use(VueRouter);
let router = new VueRouter(
{routes:[
{path:'/',component:root},
{path:'/home',component:home,
children:[
{path:'a',component:homea},
{path:'b',component:homeb}
]
}
]
}
)
引入使用
const app = new Vue({
el:'#app',
router,
template:` <div>
<h1>app</h1><router-link to="/home"><p>/home</p></router-link><router-view></router-view>
</div>`,
})
效果出来完成
整体是逻辑根据Vue-router源码实现的,为了方便理解我将所有的逻辑都放到了index.html下,与源码文件有很大区别,自己只是简化了以下,但是实现核心都在这里。关于Vue-router实现不是很难,理解起来也很容易
最后
🐯新年马上就要到了,给大家拜个早年,祝屏幕前的你2022 顺风顺水顺财神,脱单脱贫不脱发!🤞