VueRouter源码解析--手写一个VueRouter

288 阅读5分钟

VueRouter源码解析--手写一个VueRouter

今天来总结记录一下对VueRouter源码的解读。 如何手写一个VueRouter呢? 先上一个思维导图

app.png

第一步 matcher

install方法

新建一个install.jsinstall方法是用来注册VueRouter的,我们使用Vue.use(VueRouter)时,就会去找install方法,进行注册。

let _Vue

const isDef = v => v !== undefined
// 此处使用函数传参方式,传入vue,而不是 import vue,这会使得vuerouter包很大
export default function install(Vue){
    //判断是否已注册
    if(install.installed && _Vue===Vue) return
    install.installed=true
    //将vue赋值给_vue变量
    _Vue=Vue
    Vue.mixin({
       beforeCreate(){
        //判断是否是根实例
        if(isDef(this.$options.router)){
            //把根实例赋值给_routerRoot,---》_routerRoot指向的就是根实例了
            this._routerRoot=this
            //绑定router  -->此处的_router就是VueRouter实例
            this._router=this.$options.router
            //初始化路由
          this._router.init(this)
        }else{
            //如果不是根实例,则去找他的父组件
            this._routerRoot=(this.$parent && this.$parent._routerRoot) || this
        }
       }
    })
}

VueRouter类

首先在vue-router文件夹下新建一个index.jsVueRouter的参数就是那些我们在new时传入的,moderoutesbase等。 在constructor设定mode,这里默认为hash;建立路由匹配器,将用户传入的routes转化为好维护的结构(/about,/about/a

export default class VueRouter{
    constructor(options){
        this.mode= options.mode || 'hash'
        
        //建立路由匹配器
        //将用户传入的路由转化为好维护的结构 /about,/about/a
        // 里面有match方法,用来匹配路由  {'/':记录},{'/about':记录}
        //有addRoutes ,用来动态添加路由
      this.matcher = createMatcher(options.routes || [])

    }
  init (app) {   //此处app是指 根实例
  }
}
VueRouter.install=install

create-matcher方法

create-matcher主要工作就是创建路由映射表,并提供了addRoutes方法和match方法(用来根据路径匹配记录)

export function createMatcher(routes){
    //扁平化数组,创建路由映射表
    const { pathList, pathMap }= createRouteMap(routes)
    function addRoutes(){
        createRouteMap(routes,pathList,pathMap)
    }
  function match (location) {}   
}

createRouteMap方法

该方法主要为了生成pathListpathMappathList['/','/about']pathMap{'/',{path:{/about,component:xxx,parent:xxx},path:{/about/a,component:xxx,parent:xxx}}}。当执行addRoutes时,也是往原先的pathListpathMap增加对象。

export function createRouteMap(routes,oldPathList,oldPathMap){
    let pathList=oldPathList||[]
    let pathMap=oldPathMap||Object.create(null)
    routes.forEach(route => {
        addRouteRecord(pathList,pathMap,route)     
    });
    return {
        pathList,
        pathMap
    }
}
function addRouteRecord (pathList, pathMap, route, parent) {
  //递归加上父路径
    const record={
        path:parent?`${parent.path}/${route.path}`:route.path,
      component: route.component,
        parent
    }
  //判断pathMap里面有没有path这项
    if(!pathMap[record.path]){
        pathList.push(record.path)  //['/','/about',]
        pathMap[record.path]=record  //{'/':记录,‘/about’:记录}
    }
    //判断children ,用递归给children路由添加到list和map中
    if(route.children){
        route.children.forEach(children=>{
            addRouteRecord(pathList,pathMap,children,record) 
        })
        
    }

}

至此用户传入的routes就已经转化为对应的路由映射关系,总结一下第一部分我们完成了啥。

  1. 完成install注册,在_routerRoot上绑定了router实例。
  2. 建立了路由匹配器,后续我们只需要通过路径在pathMap里获取路由record就行了。
  3. 完成了addRoutes功能。

第二步 history

history and HashHistory

现在我们开始来思考mode,一般的模式有hashhistoryabstract。而这些模式会有一些公共的方法,也会有一些自己的方法。所以这里我们抽象一个History类做为基类,一个HashHistory类作为子类去extends History类。

  • HashHistory会有一个getCurrentLocation方法用来获取当前hash值。
  • History会有一个transitionTo用来去跳转到某个地址。
  • VueRouter类中的init方法中,我们则会初始化得去获取当前路径并跳转

History类

export default class History { 
  constructor(router) { 
    this.router = router
  }
  transitionTo (location, onComplete) {}
}

HashHistory类

export default class HashHistory extends History{ 
  constructor(router) {
    super(router)
  }
  getCurrentLocation () { 
    return getHash()

  }
}
function getHash () {
  return window.location.hash.slice(1)
 }

VueRouter类

export default class VueRouter{
    constructor(options){
        this.mode= options.mode || 'hash'
        
        //建立路由匹配器
        //将用户传入的路由转化为好维护的结构 /about,/about/a
        // 里面有match方法,用来匹配路由  {'/':记录},{'/about':记录}
        //有addRoutes ,用来动态添加路由
      this.matcher = createMatcher(options.routes || [])
      this.history=new HashHistory(this)

    }
  init (app) {   //此处app是指 根实例
    const history = this.history
    const setupHashListener=() => { 
      history.setupListener()
    }
    history.transitionTo(   //初始化跳转成功后,要做监听浏览器前后操作
      history.getCurrentLocation(),
      setupHashListener
    )
  }

如何实现transitionTo

现在我们发现核心地方就在transitionTo方法了。那这个方法需要做完成什么呢?

  • 根据locationmatcher里得match方法,结合pathMap获取这条路由record
  • 而如果是多级路由,如/about/a,我们需要先渲染/about,再渲染/about/a,所以针对多级路由,我们需要他对应多条record
  • 创建一个createRoute方法,用来递归生成该路径下的所有record
  • 定义一个current,当路由跳转时,更改current

createMatcher 方法

 function match (location) {
      //根据路径去匹配record
      //这里要考虑一个问题,对于 /about/a这种,我们要先渲染 about,再渲染about/a,
      const record = pathMap[location]
      const local={
          path: location
        }
    if (record) {
        return createRoute(record, local)
      }
      return createRoute(null, local)

    }

history类

export function createRoute (record, location) { 
  let res = []
  while (record) { 
    res.unshift(record)
    record=record.parent
  }
  return {
    ...location,
    matched:res
  }

}
 transitionTo (location, onComplete) {
    //根据路径去匹配记录
    let route = this.router.match(location)
    // {
    //   matched: [
    //   {path: "/about", parent: undefined, component: xxx},
    //     { path: "/about/a", component: xxx, parent: xxx }
    //   ],
    //   path:"/about/a"
    // }
    //判断跳转的路径和原来的是否一样,不一样,就要去更新current
    //判断length是因为 首次跳转时,/ 对应的matched为空,而之后就有值了
    if (route.path === this.current.path && route.matched.length === this.current.matched.length) { 
      return
    }
    this.updateRoute(route)
    onComplete && onComplete()

  }
   //更新路由
  updateRoute (route) {
    this.current = route
  }

至此,我们已经实现了哪些功能呢? 在我们注册并实例化VueRouter后

  1. 自动根据传入的routes生成pathListpathMap这些路由映射表
  2. init方法中获取到当前路径并跳转
  3. 跳转方法中根据当前路径去pathMap中匹配record
  4. 针对多级组件(多级路由)情况对record做一个转化,多级路由matched就有几个record
  5. 得到被转化的route后,更新当前current

第三步 数据响应式

  • router实例上绑定响应式数据_route,值为historycurrent
  • 绑定 $route$router
  • history监听,当路由变化时将route赋值给_route,实现视图更新。

install方法

 Vue.util.defineReactive(this,'_route',this._router.history.current)
 Object.defineProperty(Vue.prototype, '$route', {
    get () { 
      return this._routerRoot._route
    }
  })

  Object.defineProperty(Vue.prototype, '$router', {
    get () { 
      return this._routerRoot._router
    }
  })

VueRouter init方法

 //监听路由变化,将回调函数存储,当路由更新时再执行
    history.listen((route) => { 
      app._route=route  //_route 是响应式的,到时候他一变,视图就变
    })

history类

//更新路由
  updateRoute (route) {
    this.current = route
    this.cb&&this.cb(route)
  }
  listen (cb) { 
    this.cb=cb
  }