实现一个你脑子中认识的vue-router

吐槽君 分类:javascript

前言

vue中使用到的路由插件是vue-router。它是vue官方路由插件,依赖于Vue。近期学习了下vue-router源码,并自己写了一个小例子,包含动态路由addRoutes,router-view,router-link
image.png

现在让我们看一看vue-router到底是怎样实现的!让我们静下心来好好看看

实现文件目录:
image.png
首先我们第一步引入:
router/index.js文件中:

import VueRoutes from './routes/VueRoutes.js'
Vue.use(VueRoutes);
..........
let routers = new VueRoutes({
  mode: 'hash',
  routes
})
 

我们知道Vue.use(plugin),会执行install方法。

install.js文件中

import routerView from "./view.js"
import routerLink from "./viewLink.js"
export default function install(Vue) {
  // 每个组件混入该生命周期
  Vue.mixin({
    beforeCreate() {
      //  判断是否是根组件options中才有router,根组件才执行,在你new Vue时会传入     
      //  一个let router=new VueRoutes();
      if (this.$options.router) {
        this._rootRouter = this;
        this._router = this.$options.router;
        // 重点:路由初始化
        this._router.init(this)
        // 进行设置_route的响应式,当_route发生变化时会进行页面更新
        Vue.util.defineReactive(this, "_route", this._router.history.current);
      } else {
        this._rootRouter = this.$parent && this.$parent._rootRouter;
      }
    },
  });
  
  Object.defineProperty(Vue.prototype, "$route", {
    get() {
      return this._rootRouter._route;
    }
  })
  Object.defineProperty(Vue.prototype, "$router", {
    get() {
      return this._rootRouter._router;
    }
  })
  //全局组件router-view router-link。具体后面会说
  Vue.component('RouterView', routerView);
  Vue.component('RouterLink',
    routerLink
  );
}
 

我们可以知道install主要做了

  1. 利用mixin之后在每个组件中混入该beforeCreate生命周期
  2. 生命周期中对根组件进行设置私有属性_rootRouter,_router
  3. 每个子组件中获取this.$route,this.$router就会在向上寻找根组件上的_rootRouter._router_rootRouter._route
  4. 注册全局组件router-view,router-link

下面看下路由的构造函数做了什么?

routes目录下

index.js文件中

export default function VueRoutes(options) {
  // 返回一个pathlist表示路由数组,与路由映射表 pathMap
  this.matcher = createMatcher(options || []);
  //路由模式有hash,history等,我们主要以hash来讲
  this.history = new HASHHistory(this);
  //下面会讲,路由初始化
  this.init=function(){
     ...
  }
  //通过跳转url,获取到路由映射表
  this.match=function(location) {
    return this.matcher["match"](location)
  }
  //this.$router.push跳转
  this.push=function(path){
      this.history.push(path);
  }
  //添加动态路由
  this.addRoutes=function(routes){
    this.matcher.addRoutes(routes);
  }
 
  })
 
VueRouters.install = install;
 

构造函数中做了:

  1. this.matcher返回match,addRoutes,匹配路由对象与添加路由,之后通过路由实例就能够添加动态路由,和获取路由的映射关系。
  2. 实例化一个history中,对url监听变化,重新获取跳转新的路由映射,更新_route页面重新渲染
  3. 路由的初始化,其中的关键transitionTo,监听路由的变化后对页面的更新

以上三点我们一一看代码详细说吧

createMatcher.js

(1)

export default function createMatcher(options) {
  // 返回路径的列表, 路径和路由对象的映射
  var {
    pathList,
    pathMap
  } = createRouteMap(options.routes);
  console.log(pathList, pathMap);
    
  // 添加动态路由
  function addRoutes(routes) {
    let obj = createRouteMap(routes, pathList, pathMap);
  }
  // 当路由发生变化的时候,会进行查找该路由 所对应的映射关系,router-view 会用到
  function match(location) {
    let record = pathMap[location];
    let local = {
      path: location
    }
    // 找到当前的记录
    // 需要 到对应的记录,并且根据记录产生一个匹配 的数组
    if (record) {
      return createRoute(record, local)
    }
    // 没有记录则返回为空
    return createRoute(null, local)

  }
  return {
    match,
    addRoutes
  }

}

 

createMatcher中,传入routes,之后计算出pathList,pathMap。我们来看下createRouteMap实现

createRouteMap.js

export default function createRouteMap(routes, pathList, pathMap) {
  // 处理routes中的路由映射
  pathList = pathList ? pathList : [];
  pathMap = pathMap ? pathMap : Object.create(null);

  //  遍历路由添加到pathList,pathMap中
  routes.forEach(val => {
    addRoutesRecord(val, pathList, pathMap);
  })
  return {
    pathList,
    pathMap
  }
}
 

把routes传入之后继续进行遍历之后,添加到pathList,pathMap

看下addRoutesRecord的实现:

function addRoutesRecord(routes, pathList, pathMap, parent) {
  let path
  //parent是当遍历的是chilren中的路由才有的parent
  if (parent) {
    path = `${parent.path}/${routes.path}`;
  } else {
    path = routes.path;
  }
  let record = {
    path: path,
    component: routes.component,
    parent
  }
  if (!pathMap[path]) {
    pathList.push(path);
    pathMap[path] = record;
  }

  //   如果有children 就继续遍历,生成扁平化放进数组
  if (routes.children) {
    routes.children.forEach(val => {
    //routes传进去,表示当前路由的父路由
      addRoutesRecord(val, pathList, pathMap, routes)
    })
  }

}
 

routes有children情况下,会继续执行addRoutesRecord,继续添加到pathList,pathaMap中。变成一维数组吧。转化请看下面,还会有parent,之后会在路由变化,router-view重新生成render中用到。

const routes = [{
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    component: () => import( /* webpackChunkName: "about" */ '../views/About.vue'),
    children: [{
        path: 'aaa',
        component: aaa
      },
      {
        path: 'bbb',
        component: bbb
      }
    ]
  }
  
 

变成

image.png

HashHistory.js

(2)
接下来我们看下

this.history = new HashHistory(this);

因为小例子所以我们只关心hash路由模式,它会继承于HashHistory,HashHistory会继承于BaseHistory基类。因为之前考虑history,hash,abstract都有共同的属性与方法。

看下HashHistory

export default class HASHHistory extends BaseHistory {
  constructor(app) {
      super(app);
      
  }
  getCurrentLocation() {
    return getHash();
  }
  setlister() {
    // 设置监听hash的变化
    window.addEventListener("hashchange", () => {
      this.transitionTo(getHash());
      // console.log();
    })
  }
}

 

监听了路由的变化,会执行this.tansitionTo(url),重要看下基类中的transitionTo实现

BaseHistory类中:

  // 跳转的核心逻辑location 
  this.transitionTo=function(location, oncomplete) {
    let routes=this.router.match(location);
     //是否与之前的           
     if(this.current.path==location&&this.current.matched.length==routes.matched.length)}    {
      return;
    }
    getCurrent(this,routes)
    // 执行监听路由的变化
    oncomplete && oncomplete();
    // 执行页面的更新
    this.cb&&this.cb(routes);
  }
   this.listen=function(cb){
    this.cb=cb;
  }
 }
 

判断跳转的url,获取到它的路由映射关系,执行cb更新路由实例的_route,更新页面。

(3)看下init,在vueRoutes构造函数中

this.init=function(app){
    let setlister = function (_that) {
      _that.history.setlister();
    }
    // 默认会执行一次
    this.history.transitionTo(this.history.getCurrentLocation(), setlister(this));
    // 视图更新,已经对实例的_route进行拦截,发生变化会进行对页面的更新
    this.history.listen(route=>{
      app._route=route;
    })
 }
 

在初始化过程中,默认会执行一次transitionTo,之后transitionTo中匹配到类似像{path:“/”,matched:[...]};之后赋值于app._route,更新页面。我们知道路由的变化会导致router-view重新生成render,渲染到页面。

router-view是一个全局组件,它的实现是一个函数式渲染

export default {
  functional: true,
  render(h, {
    parent,
    data
  }) {
  //获取到当前的路由对象
    let route = parent.$route;
    let match = route.matched;
    data.routerViews = true;
    let depth = 0
    while (parent) {
      if (parent.$vnode && parent.$vnode.data.routerViews) {
        depth++;
      }
      parent = parent.$parent;
    }
    let record = match[depth];
    if (!record) {
      return h()
    }
    let component = record.component;
    return h(component, data)
  }
}

 

此时matcher比如长这样:
image.png
知道当前路由的映射,路径path与组件component。假如嵌套路由时候,这时会有两个router-view,第一个router-view执行后设置一个状态,data.routerViews = true;之后在次只要根据判断(parent.$vnodeparent.$vnode.data.routerViews)为true则证明现在是第二个router-view。

总结,上面只是实现了简单的动态路由,router-view,router-link,还有像导航钩子等都没实现,有兴趣的小伙伴,看看这个哈,也只是草草的写文章 记录了一下,须要查看完整的例子,小伙伴们可以点击gihtub查看,以上只是自己的一点理解,希望有错误的可以一起交流一下哈,力争共同进步。

回复

我来回复
  • 暂无回复内容