浅析vue-router核心原理

746 阅读3分钟

很多大佬都说过,写代码大概有这么几个阶段:知道怎么用 => 知道怎么实现 => 知道为什么这样实现 => 知道怎么实现更好 。 源码分析,则是进阶的一个比较好的方式。今天分享下对 vue-router 的理解,输出倒逼输入吧。

使用方法

先回顾一下vue-router的使用方法,

首先编写vue-router配置文件,

// router/index.js
import Vue from 'vue'
import VueRouter from '../vue-router'
import Home from '../views/home';
import About from '../views/about';

let routes = [
  {
    path: '/',
    component: Home
  },
  {
    path: '/about',
    component: About,
    children: [
      {
        path: 'a',
        component: {
          render: (h) => h('h3', 'about a')
        }
      },
      {
        path: 'b',
        component: {
          render: (h) => h('h3', 'about b')
        }
      }
    ]
  },
]

Vue.use(VueRouter)

export default new VueRouter({
  mode: 'hash',
  routes,
})

在main.js里引入,

//main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router/index'

Vue.config.productionTip = false

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

在App.vue里使用router-view、router-link组件;

<template>
  <div id="app">
    <router-link to="/">首页</router-link>
    <router-link to="/about">关于</router-link>
    <router-view></router-view>
  </div>
</template>

<script>
export default {
  name: 'App',
}
</script>

源码分析

OK,从配置文件开始分析:

Vue.use(VueRouter)

install

利用vue的插件机制,使用Vue.use(VueRouter)时,会调用vue-router的install方法,装载vue-router,install方法如下:

export let Vue
import RouterLink from './components/router-link'
import RouterView from './components/router-view'
const install = function(_Vue) {
  Vue = _Vue
  Vue.component('router-link', RouterLink)
  Vue.component('router-view', RouterView)
  Vue.mixin({
    beforeCreate() {
      if (this.$options.router) {
        this._routerRoot = this; // 将当前根实例 放到_routerRoot
        this._router = this.$options.router;
        this._router.init(this); // 调用实例的init方法 初始化vue-router
        Vue.util.defineReactive(this, '_route', this._router.history.current)
      } else {  // 子组件获取父组件的_routerRoot
        this._routerRoot = this.$parent && this.$parent._routerRoot;
      }
    },
  })
  Object.defineProperty(Vue.prototype, '$route', {
    get() {
      return this._routerRoot._route
    }
  })
  Object.defineProperty(Vue.prototype, '$router', {
    get() {
      return this._routerRoot._router
    }
  })
}

export default install

在install内部定义二个全局组件router-link、router-view;

使用 Vue 混入机制,在 Vue 的生命周期 beforeCreate 钩子函数中混入,用户将router属性注册到new Vue, 在这里,按照先父后子的顺序,让所有组件都可以获取到router属性,然后调用实例的init方法 初始化vue-router;

用 defineReactive 方法定义 _route 属性为响应式,后面再讲它的作用;

为了方便使用,使用Object.defineProperty定义了 $route、$router二个属性,$route 存放的都是属性,$router 是router实例;

VueRouter对象

接下来,看下 VueRouter 做了哪些事:

import createMatcher from './create-matcher'
import HashHistory from './history/hashHistory'
import BrowserHistory from './history/browserHistory'
class VueRouter {
  constructor(options) {
    let routes = options.routes || []
    this.matcher = createMatcher(routes)

    this.mode = options.mode || 'hash'
    switch(this.mode) {
      case 'hash':
        this.history = new HashHistory(this);
        break;
      case 'history':
        this.history = new BrowserHistory(this);
        break;
    }
  }
  
  init(app) {
    const history = this.history;
    let setupHashListener = () => {
      history.setupListener()
    }
    history.transitionTo(history.getCurrentLocation(), setupHashListener)

    history.listen((route) => {
      app._route = route
    })
  }

  push(location) {
    // 执行this.$router.push(), hash变化,执行transitionTo
    window.location.hash = location
  }

  match(location) {
    return this.matcher.match(location)
  }
}

export default VueRouter

1)调用createMatcher,创建匹配器, 它的作用是添加路由匹配,以及动态路由添加;

2)然后根据用户传入的mode,创建不同的历史管理实例history;

3)重要的是init方法,执行history的transitionTo方法,对跳转路径进行匹配,跳转完毕后,还需要监听路由的变化,执行setupHashListener,就是执行了window.addEventListener('hashchange', ()=> {this.transitionTo();}) (参看后面的 setupListener 方法实现);

4)初始化时还需要调用更新 _route 的方法;

createMatcher

先说第一点 createMatcher,因为传入的routes 并不方便使用,转换成map更方便,例如:{path: 'a', children: [{path: 'b'}]}, 被 createMatcher 转换成 {'a': {path: 'a', component:A}, 'a/b': {path: 'a/b',compoent:B, parent: 'a'}},更加方便匹配;

match方法通过用户输入的路径,来获取对应的匹配记录,然后通过 createRoute 创建新路由记录;

addRoutes方法则将用户添加的路由,与存在的进行合并;

export default function createMatcher(routes) {
  let {pathList, pathMap} = createRouteMap(routes)
  
  function match(location) {
    let record = pathMap[location];
    return createRoute(record, {
      path: location
    })
  }
  
  function addRoutes(routes) {
    createRouteMap(routes, pathList, pathMap)
  }
  return {
    match,
    addRoutes
  }
}

History

再说第二点,先看下父类History,

export default class History {
  constructor(router) {
    this.router = router;
    this.current = createRoute(null, {
      path: '/'
    })
  }
  transitionTo(location, complete) {
    let current = this.router.match(location)
    // 判断是否重复路径
    if(location == this.current.path && this.current.matched.length === current.matched.length) {
      return
    }
    this.current = current
    // 希望current变化 , 更新_route, 视图就可以更新
    this.cb && this.cb(current)
    complete && complete()
  }
  listen(cb) {
    this.cb = cb
  }
}

子类HashHistory:

const ensureSlash = () => {
  if(window.location.hash) {
    return
  }else {
    window.location.hash = '/'
  }
}
function getHash(){
  return window.location.hash.slice(1);
}
export default class HashHistory extends History {
  constructor(router) {
    super(router)
    this.router = router;
    // 使用hash模式,默认如果没有#,应该跳转到 #/
    ensureSlash()
  }
  getCurrentLocation() {
    return getHash()
  }
  setupListener() {
    window.addEventListener('hashchange', ()=> {
      this.transitionTo(getHash());
    })
  }
}

子类的 setupListener 会在 init 中被调用,用来监听路由的变化;

在History类里定义了current属性,对应路径匹配的记录,transitionTo方法根据当前location,获取对应的路径匹配记录,然后在router-view中渲染页面。

渲染页面

渲染这一步具体是如何做的呢?

结合2、3、4点一起分析, 我们把根 Vue 实例的_route属性定义成响应式的,

Vue.util.defineReactive(this, '_route', this._router.history.current)

然后在每个 router-view 组件执行 render 函数的时候,都会访问 parent.$route,触发了它的 getter,

export default {
  name: 'router-view',
  functional: true,
  render(h, {parent, data}) {
    // 访问parent.$route
    let route = parent.$route
    let depth = 0
    data.routerView = true // 标识路由属性
    while(parent) {
      if(parent.$vnode && parent.$vnode.data.routerView) {
        depth++
      }
      parent = parent.$parent
    }
    let record = route.matched[depth]
    if(!record) {
      return h()
    }
    return h(record.component, data)
  }
}

然后再执行完 transitionTo 后,修改 current 的时候, app._route 也被修改,于是触发了setter,因此会通知 router-view 的渲染 watcher 更新,重新渲染组件。

以上,分析了 vue-router 的核心代码的关键思路,有问题敬请指证,一起探讨。