深入理解Vue Router实现

756 阅读4分钟

一.简介

核心实现就是根据路径变化找到对应组件,显示到 router-view 中

二.实现 vue-router

整体目录结构

├─vue-router
│  ├─components # 存放vue-router的两个核心组件
│  │  ├─link.js
│  │  └─view.js
│  ├─history    # 存放浏览器跳转相关逻辑
│  │  ├─base.js
│  │  └─hash.js
│  │  └─h5.js
│  ├─create-matcher.js # 创建匹配器
│  ├─create-route-map.js # 创建路由映射表
│  ├─index.js # 引用时的入口文件
│  ├─install.js # install方法

1. install

Vue.use(Router)默认会调用当前返回 VueRouter 对象的 install 方法,挂载路由实例对象到 vue 上。 所有组件都可以通过 this._routerRoot._router 拿到用户传递进来的路由实例对象。

install.js

export let _Vue;
VueRouter.install = function(_Vue) {
  Vue = _Vue;
  Vue.mixin({
    beforeCreate() {
      if (this.$options.router) {// 如果有router属性说明是根实例
        this._router = this.$options.router;
        this._routerRoot = this;
      } else {// 父组件渲染后会渲染子组件
        this._routerRoot = this.$parent && this.$parent._routerRoot;
      }
    },
  });
 Object.defineProperty(Vue.prototype, "$router", {
    // 方法
    get() {
      return this._routerRoot._router;
    },
  });
  Object.defineProperty(Vue.prototype, "$route", {
    // 属性
    get() {
      return this._routerRoot._route;
    },
  });
  Vue.component("router-link", RouterLink);
  Vue.component("router-view", RouterView);
};

在 Vue-Router 上增加一个 init 方法,主要目的就是初始化功能

Index.js

export default class VueRouter{
    constructor(options){

        this.matcher = createMatcher(options.routes || []);
    }
+    init(app){}
}

2. createMatcher

根据用户传递的 routes 创建匹配关系,createMatcher 需要提供两个方法

  • match:match 方法用来匹配规则
  • addRoutes:用来动态添加路由 动态路由的实现就是将新的路由插入到老的路由映射表 oldPathMap 中
import createRouteMap from './create-route-map'
export default function createMatcher(routes) {
    // 收集所有的路由路径, 收集路径的对应渲染关系
    // pathList = ['/','/about','/about/a','/about/b']
    // pathMap = {'/':'/的记录','/about':'/about记录'...}
    let {pathList,pathMap} = createRouteMap(routes);

    function addRoutes(routes){
        createRouteMap(routes,pathList,pathMap);
    }
    function match(path){
        return pathMap[path]
    }
    return {
        addRoutes,
        match
    }
}

2.1 createRouteMap

createRouteMap 要把路由创建成映射表,路径和记录匹配。还可以添加映射表 映射表pathMap如下图所示

export 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: parent,
  };
  pathMap[path] = record;
  route.children &&
    route.children.forEach((childRoute) => {
      addRouteRecord(childRoute, pathMap, record);
    });
}

3. 路由模式

index.js

class VueRouter {
    constructor(options = {}) {
        const routes = options.routes;
   +    this.mode = options.mode || 'hash';
        this.matcher = createMatcher(options.routes || []);
   +    switch(this.mode) {
            case 'hash': // location.hash  => push
                this.history = new Hash(this)
                break
            case 'history': // pushState => push
                this.history = new HTML5History(this);
                break
        }
    }
    match(location){
        return this.matcher.match(location);
    }
+   init(app) {
        const history = this.history;
        const setUpListener = () =>{
            history.setUpListener();
        }
        history.transitionTo(
            history.getCurrentLocation(), // 各自的获取路径方法
            setUpListener
        );
    }
}

history/base.js

transitionTo 匹配出 path 对应的 record 赋值给 current

function createRoute(record, location) {
  // 创建路由
  const matched = [];
  // 不停的去父级查找
  if (record) {
    while (record) {
      matched.unshift(record);
      record = record.parent;
    } // /about/a => [about,aboutA]
  }
  return {
    ...location,
    matched,
  };
}

export default class History {
  constructor(router) {
    this.router = router;
    // 有一个数据来保存路径的变化
    // 当前没有匹配到记录
    this.current = createRoute(null, {
      path: "/",
    }); // => {path:'/',matched:[]}
  }
  transitionTo(path, cb) {
   let record = this.router.match(path); // 匹配到后
   this.current = createRoute(record, { path });
    cb && cb(); // 默认第一次cb是hashchange
  }
}

history/hash.js

import History from './base'
function ensureHash() {
    if (!window.location.hash) {
        window.location.hash = '/';
    }
}
function getHash() {
    return window.location.hash.slice(1);
}
export default class Hash extends History {
    constructor(router) {
        super(router);
        // hash路由初始化的时候 需要增加一个默认hash值 /#/
        ensureHash();
    }
    getCurrentLocation() {
        return getHash();
    }
    setUpListener() {
        window.addEventListener('hashchange', () => {
            // hash值变化 再去切换组件 渲染
            this.transitionTo(getHash());
        })
    }
}

History/h5.js

import History from './base'
export default class HTML5History extends History{
    constructor(router){
        super(router);
    }
    getCurrentLocation(){
        return window.location.pathname;// 获取路径
    }
    setUpListener(){
        window.addEventListener('popstate',()=>{ // 监听前进和后退
            this.transitionTo(window.location.pathname);
        })
    }
    pushState(location){
        history.pushState({},null,location);
    }
}

我们不难发现路径变化时都会更改 current ,我们可以把 current 属性变成响应式的,每次 current 变化刷新视图即可 current 里面的属性在哪使用,就会收集对应的 watcher

install.js

export let Vue;
import RouterLink from "./components/link";
import RouterView from "./components/view";
export default function install(_Vue) {
  Vue = _Vue;
  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;
      }
    },
  });
  Object.defineProperty(Vue.prototype, "$router", {
    get() {
      return this._routerRoot._router;
    },
  });
  Object.defineProperty(Vue.prototype, "$route", {
    get() {
      return this._routerRoot._route;
    },
  });
  Vue.component("router-link", RouterLink);
  Vue.component("router-view", RouterView);
}

改变currentthis.$route却没有改变

所以我们当路径变化时需要执行此回调更新\_route 属性, 在init方法中增加监听函数

index.js

class VueRouter {
  //...
  init(app) {
   //...
+   history.listen((route) => {
+      app._route = route;
    });
  }
}

base.js

export default class History {
 //...
 + listen(cb) {
    this.cb = cb;
  }
  transitionTo(path, cb) {
  //..
 +   this.current = route;
 +   this.cb && this.cb(route);

  }
}

三.实现 Router-Link 及 Router-View

可以将组件标记为 functional,这意味它无状态 (没有响应式数据),也没有实例 (没有 this 上下文)。因为函数式组件只是函数,所以渲染开销也低很多。

1. router-view 组件

routeview 标记 true,表示渲染过。逐层 depth++匹配matched[depth]渲染。

export default {
  functional: true,

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

2. router-link 组件

export default {
  functional: true,
  props: {
    to: {
      type: String,
      required: true,
    },
  },
  render(h, { props, slots, parent }) {
    const click = () => {

      parent.$router.push(props.to);
    };
    return <a onClick={click}>{slots().default}</a>;
  },
};

四.实现 beforeEach

把所有的钩子组成一个数组依次执行

index.js

class VueRouter{
    constructor(options={}){
     this.beforeHooks=[];
    }
    beforeEach(fn){
        this.beforeHooks.push(fn);
    }
}

base.js

function runQueue(queue, iterator,cb) { // 迭代queue
    function step(index){
        if(index >= queue.length){
            cb();
        }else{
            let hook = queue[index];
            iterator(hook,()=>{ // 将本次迭代到的hook 传递给iterator函数中,将下次的权限也一并传入
                step(index+1)
            })
        }
    }
    step(0)
}
export default class History {
    transitionTo(location, onComplete) {
        // 跳转到这个路径
        let route = this.router.match(location);
        if (location === this.current.path && route.matched.length === this.current.matched.length) {
            return
        }
        let queue = [].concat(this.router.beforeHooks);
        const iterator = (hook, next) => {
            hook(route,this.current,()=>{ // 分别对应用户 from,to,next参数
                next();
            });
        }
        runQueue(queue, iterator, () => { // 依次执行队列 ,执行完毕后更新路由
            this.updateRoute(route);
            onComplete && onComplete();
        });
    }
    updateRoute(route) {
        this.current = route;
        this.cb && this.cb(route);
    }
    listen(cb) {
        this.cb = cb;
    }
}

五.小结

默认初始化init时,先进行一次路由跳转 transitionTo,跳转到对应路径 getCurrentLocation 之后设置路由监听事件 setUpListener。路由变化 触发监听回调 transitionTo(path);current改变,_route改变,routeview逐层渲染对应匹配matched的记录。