源码系列之Vue-router的实现原理

219 阅读4分钟

如果你不知道脚下的路怎么走了,过来看看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;
                }
            }
        })
    }
```

挂载route,route,router

Object.defineProperty方法不做过多解释,网上有很多解释,看过Vue响应式原理的应该明白 ```js // route设定Object.defineProperty(Vue.prototype,route 设定 Object.defineProperty(Vue.prototype,'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>`,
    })

效果出来完成

ecb0abc67f5995d8c973e530295f29e.png

整体是逻辑根据Vue-router源码实现的,为了方便理解我将所有的逻辑都放到了index.html下,与源码文件有很大区别,自己只是简化了以下,但是实现核心都在这里。关于Vue-router实现不是很难,理解起来也很容易

最后

🐯新年马上就要到了,给大家拜个早年,祝屏幕前的你2022 顺风顺水顺财神,脱单脱贫不脱发!🤞