手动实现Vue Router(重点是实现路由钩子)

997 阅读3分钟

这里的实现方法大致和源码相同,但结构组成和源码有所出入,比如说匹配路由的方法源码是放在History类上,我几乎都放在VueRouter类上了,因为写的时候忘记看结构了。还有一些正则的方法没有像源码一样处理得很好(源码那我实在看不太懂,只能用自己的理解写一下,history模式同理,我还没全写好) 仓库地址

钩子函数是在哪里执行呢

在说明这个问题前,我们要知道VueRouter在初始化的时候,已经将routes遍历,每一个route都在其原有的信息上加入了更多信息(比如route是没有parent这个属性点,也没有路径的正则匹配的),创建成含有这些信息的record对象

  1. 在初始化一来是完成下面四步骤,第二就是监听hashChange,load,popState事件
    1. 创建成route对应的record
    2. 将目前所有存在的路径加入pathList数组
    3. 将route.name和对应的record创建映射表nameMap
    4. 将route.path和对应的record创建映射表pathMap
  2. 当window.location发生变化,或者用户点击了router-link的时候,会触发router.push这个方法
  3. router.push方法调用了transitionTo,transitionTo方法
    1. 首先通过match函数把当前location匹配的record得到,然后创建一个currentRoute对象 1.这个currentRoute的matched是一个数组,数组里面按顺序排列着(父在前)符合这个location的route以及他的父级route
    2. 把目前的toRoute赋值给fromRoute(旧的路由对象),再把currentRoute赋值给toRoute
    3. 提供了当ConfirmTransitionTo完成/失败后需要执行的onComplete/onAbort方法
  4. ConfirmTransitionTo方法主要完成以下步骤,然后调用上诉的onComplete/onAbort方法
    1. 首先比较toRoute和fromRoute的matched里面,如果顺序和地址都一样的话,说明这个route里面的component不需要重新渲染
let maxLength = Math.max(this.fromRoute.matched.length,this.toRoute.matched.length)
    var i
    for(i =0;i<maxLength;i++){
    //比如父子孙,当i=1是,说明toRoute/fromRoute的父是可以复用,属于updatedRoutes
    //toRoute的子孙组件是activatedRoutes
    //fromRoute的子孙组件是deviatedRoutes
        if(this.fromRoute.matched[i]!==this.toRoute.matched[i]){
            break
        }
    }
    let activatedRoutes  = this.toRoute.matched&&this.toRoute.matched.slice(i) //
    let activatedRoutes  = this.fromRoute.matched&&this.fromRoute.matched.slice(i)
    let updatedRoutes = this.toRoute.matched&&this.toRoute.matched.slice(0,i)
   

上面获得了activatedRoutes,activatedRoutes,updatedRoutes这三个数组,就可以开始说重点了

实现路由钩子函数里的方法

var cbs = []
 //下面这个方法
 //就是将除开beforeRouterEnter、beforeResolve和afterEach钩子函数链接成数组,里面的函数逐一执行,执行完后再执行cb
 // runQueue(queue,iterator, cb)
 // cb这个方法将beforeRouterEnter和beforeResolve钩子函数链接成数组,
 //然后一次执行新连接成的数组里面的函数
 //再调用transition传过来的onComplete方法,complete方法会依次执行afterHooks和全局数组cbs里面的方法
 //这样就将所有的钩子函数全都执行完
 
let queue = [].concat([
         ...extractLeaveRoutesFn(transitionRoutes.deviatedRoutes),//在失活的组件里调用离开守卫
         ...extractUpdateRoutesFn(transitionRoutes.updatedRoutes),
         ...this.beforeHooks,///调用全局的beforeEach守卫,
         ...transitionRoutes.activatedRoutes.map(activatedRoute=>activatedRoute.beforeEnter)
            .filter(fn=>fn),//调用路由组件里面beforeEnter
         resolveAsyncComponents(transitionRoutes.activatedRoutes) //解析异步组件
    ])
 runQueue(queue,iterator, () =>{
        //上面的hook执行完了
  var queue = extractEnterGuards(transitionRoutes.activatedRoutes,cb).concat(this.resolveHooks)
        runQueue(queue,iterator, ()=> {
            this.pending = null;
            onComplete(this.toRoute);//这里用来pushstate实现跳转
        })
    })   
全局卫视beforeEach,afterEach,beforeResolve

一般调用方式如下

router.beforeEach((to, from, next) => {
    //do somthing
    next()
})
router.beforeResolve((to, from, next) => {
    //do somthing
    next()
})
router.afterEach((to, from, next) => {
    //do somthing
})

在VueRouter的类的原型上定义了这三个方法,而在VueRouter的构造函数上已经定义好了beforeHooks,resolveHooks,afterHooks数组

//registerHooks方法把fn加到对应的数组里面
VueRouter.prototype.beforeEach = function(fn){
    registerHooks(this.beforeHooks,fn)
}

VueRouter.prototype.beforeResolve = function(fn){
    registerHooks(this.resolveHooks,fn)
}
VueRouter.prototype.afterEach = function(fn){
    registerHooks(this.afterHooks,fn)
}
路由独享的守卫

下面的beforeEnter是route里面的一个属性,也就是说对应的record上面会有beforeEnter方法,

const router = new VueRouter({
  routes: [
    {
      path: '/foo',
      component: Foo,
      beforeEnter: (to, from, next) => {
        // ...
      }
    }
  ]
组件内的导航钩子
const Foo = {
  template: `...`,
  beforeRouteEnter (to, from, next) {
  },
  beforeRouteUpdate (to, from, next) {
  },
  beforeRouteLeave (to, from, next) {
  }

处理组件内的导航钩子比较特殊

  1. 首先说说beforeRouteLeave(extractUpdateRoutesFn方法是同理的,只是参数是复用的路由updatedRoutes而已)
extractLeaveRoutesFn(transitionRoutes.deviatedRoutes)
function extractLeaveRoutesFn(deviatedRoutes) {
	//参数是deviatedRoutes数组
    var fns = []
    deviatedRoutes&&deviatedRoutes.forEach(deviatedRoute=>{
    	//获得component对象,component对象里面就有组件内的导航钩子(如果有设置的话)
        let components = deviatedRoute.components
        for(let [index,item] of Object.entries(components)){
            if(typeof  item !=='function'){
            	//如果component不是function的话,需要通过Vue.extend把它转换成VueComponent类
                let def = Vue.extend(item);
                //获得这个路由里的实例对象,这个在Vue.mixin里面的registerInstance定义了
                let instance = deviatedRoute.instances[index]
                //获得VueComponent类的beforeRouteLeave函数
                var f = def.options['beforeRouteLeave']
                if(f){
                	//把f的this绑定在实例上
                    fns.push(f.bind(instance))
                }

            }
        }
    })
    return fns
}

  1. extractEnterGuards提取组件内的beforeRouteEnter方法
extractEnterGuards(transitionRoutes.activatedRoutes,cb)
var cbs = []
function extractEnterGuards(activatedRoutes, postEnterCbs) {
    var fns = []
    activatedRoutes&&activatedRoutes.forEach(activatedRoute=>{
        let components = activatedRoute.components
        for(let [index,item] of Object.entries(components)){
            if(typeof  item !=='function'){
                let def = Vue.extend(item);
                let instance = deviatedRoute.instances[index]
                var f = def.options['beforeRouteEnter']
                if(f){
                    var f1 = function(to,from,next){
                        f(to,from,function (cb) {
                        	//这里的cb就是next方法传入的函数,
                            //放进全局数组cb里面,onComplete方法需要依次执行里面的方法
                            if(typeof cb === "function"){
                                cbs.push(cb)
                            }
                        })
                        next()

                    }
                    fns.push(f1)
                }
            }
        }
    })
    return fns
}