vue-router实现思路

477 阅读1分钟

vue-router解析

vue-router的核心原理就是监听路由的变化,然后修改当前route的值,而router-view组件中会收集route属性,所以当route变化时会渲染相应的组件,然后渲染到router-view中去。

vue-router的使用

import Home from '../views/Home.vue'
import About from '../views/About.vue'

Vue.use(VueRouter)

const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    component: About,
    children: [
      {
        path: 'a',
        component: {
          render: (h) => <h1>this is /about/a</h1>
        }
      },
      {
        path: 'b',
        component: {
          render: (h) => <h1>this is /about/b</h1>
        }
      }
    ]
  }
]

router.beforeEach((to, from, next) => { // 全局钩子 路由钩子 组件钩子
  console.log(from, to, 1);
  setTimeout(() => {
    next();
  }, 3000);
})

router.beforeEach((to, from, next) => { // 全局钩子 路由钩子 组件钩子
  console.log(from, to, 2);
  next();
})

export default router
new Vue({
  router,
  render: h => h(App)
}).$mount('#app')

安装逻辑

在使用vue-router的时候,首先需要使用Vue.use()方法,是按照vue插件的方式来使用,由此说明vue-router暴露了一个install属性给Vue调用。vue-router 安装过程主要完成以下事情:

  • 初始化vue-router
  • 将当前路由定义成响应式数据
  • routerroute挂载到Vue实例上
  • 定义组件router-linkrouter-view
  Vue.mixin({
  beforeCreate() {
    if (this.$options.router) {
      this._router = this.$options.router;
      this._routerRoot = this;
      this._router.init(this);
      Vue.util.defineReactive(this, '_route', this._router.history.current);
    } else {
      this._routerRoot = this.$parent && this.$parent._routerRoot;
    }
  }
})

使用mixin()Vue注入一个beforeCreate钩子,让所有的子组件在创建的时候都会执行beforeCreate逻辑。通过router属性来判断是否是根组件,只有在根组件上才会挂载该属性,router 属性就是vue-router的实例。如果是根组件的话,将router赋值给_router,并且将根组件赋值给_routerRoot,接着初始化vue-router,在初始话完成后使用Vue 自带的方法将路由定义成响应式数据;如果不是根组件的话,将父组件上的_routerRoot赋值给当前组件实例的_routerRoot的属性上,这样所有的组件都包含routerRoot 属性指向根组件,由该属性可以获取vue-router实例,子组件通过这个属性来获取routerroute。 在使用过程中,可以通过$router来获取router实例,以及$route来获取当前路由:

 Object.defineProperty(Vue.prototype, '$router', { // $router返回的是VueRouter对象实例
  get() {
    return this._routerRoot._router;
  }
});
Object.defineProperty(Vue.prototype, '$route', { // $route返回的是路由,当前匹配到的路由
  get() {
    return this._routerRoot._route;
  }
});

当访问$router或这$route的时候都是取的根组件上的属性。

Vue.component('router-link', RouterLink);
Vue.component('router-view', RouterView);

在安装vue-router插件的时候也定义好router-linkrouter-view组件。

router-link组件

router-link组件就是渲染一个a标签,点击标签调用父组件上$routerpush方法来改变当前的路由,由此来更新视图。

export default {
  functional: true,
  props: {
    to: {
      type: String,
      required: true
    }
  },
  render(h, context) {
    const {slots, props, parent} = context;
    const click = () => {
      parent.$router.push(props.to);
    }
    return <a onClick={click}>{slots().default}</a>
  }
}

functional属性用来生命该Vue组件是个函数式组件。由于不是一个类组件,没有this,所以在render()通过context来获取组件属性。

vue-routerVueRouter

构造函数

在构造函数里面将传入配置的路由进行扁平化处理,以及实例化History对象。

class VueRouter {
  constructor(options = {}) {
    const routes = options.routes;
    this.mode = options.mode || 'hash';
    
    this.matcher = createMatcher(options.routes || []);
    
    switch (this.mode) {
      case "history": // pushState
        this.history = new BrowserHistory(this);
        break;
      case 'hash': // location.hash
        this.history = new HashHistory(this);
        break;
    }
    this.beforeHooks = []; //用于存放beforeEach的钩子函数
  }
}

根据实例化vue-router时传入的mode参数来生成不同的History实例来进行路由操作。

处理路由配置

调用createRouteMap()方法将routes扁平化处理,并提供match()addRoutes()方法用于匹配路由和添加路由。

function createMatcher(routes) {
  
  // 路径和记录匹配
  let {pathMap} = createRouteMap(routes); // 创建映射表
  
  function match(path) {
    // 在映射中找到路径
    return pathMap[path];
  }
  
  function addRoutes(routes) {
    createRouteMap(routes, pathMap); // 新增的路由,将新增的路由添加到pathMap中
  }
  
  
  return {
    addRoutes,
    match
  }
}

创建路由映射

function createRouteMap(routes, oldPathMap) {
  // 如果有oldPathMap,需要将routes格式话后添加到oldPathMap中,如果没传则生成
  let pathMap = oldPathMap || {};
  
  routes.forEach((route) => {
    addRouteRecord(route, pathMap);
  })
  
  return {
    pathMap
  };
}

function addRouteRecord(route, pathMap, parent) {
  let path = parent ? parent.path + '/' + route.path : route.path;
  let record = { // 最终路径,会匹配到这个记录,里面可以有自定义属性等
    path,
    component: route.component,
    props: route.props || {},
    parent
  }
  pathMap[path] = record;
  route.children && route.children.forEach((childRoute) => {
    addRouteRecord(childRoute, pathMap, record);
  })
}

递归调用addRouteRecord()方法,将vue-router中的routes属性处理成key为路由路径,value 为路由所对应的组件信息的形式,扁平处理就是将原来由属性嵌套来表现父子路由的形式改成了直接有key来表示父子路由,形成下面的格式:

{
  "/a": {
    path,
      component,
      props,
      parent
  },
  "/a/b":{
    path,
      component,
      props,
      parent
  }
}

初始化

在创建根组件的时候,会触发beforeCreate钩子,然后调用init()方法进行vue-router的初始化。vue-router的初始化就是设置监听事件,监听路由的变化,并且默认跳转到url带入的路径。

class VueRouter {
  init(app) {
    const history = this.history;
    const setUpListener = () => {
      history.setUpListener();
    }
    
    history.transitionTo(
      history.getCurrentLocation(), 
      setUpListener() // 设置监听事件
    );
    
    history.listen((route) => {
      // 监听 如果current变化了,就重新给_route赋值
      app._route = route;
    })
  }
}

transitionTo()路由跳转的核心方法,接受两个参数,跳转的路径以及一个回调函数。调用方法transitionTo()来跳转到当前的路由并调用history.listen()方法来设置路由更改后的回调方法用来改变vue实例上的_route属性

路由控制

路由控制由History类来管理,hashhistory两种模式分别为HashHistoryBrowserHistory继承自基类BaseHistory。两个类不同之处主要是在于: + 对事件处理的不同。hash使用hashChange事件来监听,history模式则使用popstate来监听路由改动; + 路由跳转的方式不同。hash是直接修改location.hash值,history是使用history.pushState()方法来改变history。 ### BaseHistory类 基类BaseHistory就是处理hashhistory相同的部分就是实现路由跳转渲染相应的组件。

export default class BaseHistory {
  constructor(router) {
    this.router = router;

    // 用于存放路由的变化
    // this.current = {
    //   matched: []
    // }
    // 当前没有匹配到路由
    this.current = createRoute(null, {
      path: "/",
    }); // => {path: '/', matched: []}
  }

  listen(cb) {
    this.cb = cb;
  }

  transitionTo(path, cb) {
    let record = this.router.matcher.match(path);
    let route = createRoute(record, {
      path,
    });
    if (
      path === this.current.path &&
      route.matched.length === this.current.matched.length
    ) { // 
      return null;
    }

    let queue = this.router.beforeHooks;
    const iterator = (hook, next) => {
      // 获取对应的钩子函数
      hook(route, this.current, next);
    };
    // 实现钩子函数,在跳转前执行钩子函数
    runQueue(queue, iterator, () => {
      this.updateRoute(route, cb);
      cb && cb();
      // 后置的钩子
    });
  }
  updateRoute(route, cb) {
    this.current = route; // 不仅仅要改变this.current还要改变route
    this.cb && this.cb(route);
    console.log("this.current:", this.current);
  }
}

transitionTo()方法里面有三个重要的逻辑:

  • 创建路由信息;根据路径拿到当前路由的组件信息,然后递归查找所有的父级路由的组件信息,其实也是将信息扁平化处理,这样子方便拿取数据,不扁平化处理写个递归方法来获取也是可以的。
function createRoute(record, location) {
  // 创建路由
  const matched = [];
  if (record) {
    while (record) {
      matched.unshift(record); // 往数组前面推入。因为先渲染父组件,在渲染子组件,所以父组件应该在子组件前面
      record = record.parent;
    }
  }
  return {
    ...location,
    matched,
  };
}
  • 判断当前路由路径匹配到到的组件信息是否与现在使用的组件信息是否一致,在router-link组件里面调用push()方法的时候调用了transitionTo()方法,另一方面在监听路由改变的时候也调用transitionTo()方法,所以重复调用了,避免重复渲染组件,所以这里判断一下
  • 执行队列的逻辑,runQueue()方法用来实现钩子函数队列的执行,就是一个迭代执行。
/**
 *
 * @param {*} queue // [fn, fn, fn, ...]
 * @param {*} iterator
 * @param {*} cb
 * 
 */
function runQueue(queue, iterator, cb) {
  function step(index) {
    if (index >= queue.length) return cb();
    let hook = queue[index];
    iterator(hook, () => step(index + 1));
  }
  step(0);
}

runQueue()接受一个执行队列一个迭代器和回调方法。调用step()方法依次获取任务队列里面的任务然后将其作为参数传给迭代器,同时也传一个回调方法调用step()方法来处理下一个索引的任务,直到执行完所有的任务后调用回调方法。transitionTo()方法在执行完钩子函数后,调用updateRoute()来更新当前的路由。更新路由就是更改this.current的值,将新的值通过实例上的cb方法来更新vue上的_route属性,然后触发Vue的依赖收集(在安装vue-router的时候将这个属性定义成了响应式的)从而触发更新视图。

router-view组件

router-view组件就是一个容器,用来显示路由对应的组件。

export default {
  functional: true,
  render(h, {parent, data}) {
    const route = parent.$route;
    let depth = 0;
    while (parent) {
      if (parent.$vnode && parent.$vnode.data.roterView) {
        depth++;
      }
      parent = parent.$parent;
    }
    let record = route.matched[depth];
    if (!record) {
      return h();
    }
    data.roterView = true;
    return h(record.component, data);
  }
}

const route = parent.$route;这条语句是核心点,router-view组件去父组件上获取$route属性,实际上去就是去获取根组件上的_route_route属性指向HashHistory或者BaseHistorythis.current属性,在vue-router安装的时候的时候将_route转成了响应式数据,所以Vue内部将_routerouter-view组件进行了依赖收集,在路由变化后调用了transitionTo()方法后会调用回调方法来修改_route属性,_route的改变就会触发视图的更新。下面的while循环去找当前的router-view的在所有router-view结构树中的层级(根据data.roterView来判断父组件是不是router-view组件),根据层级来渲染对应层级路由的组件。