vue 路由切换时做了什么

1,773 阅读2分钟

以下内容都是基于路由处于hash模式时

使用this.$router.push()切换路由

​ 当调用push方法时,使用的是Router实例的方法,需要传入1-3个参数,分别是跳转的路径,成功函数,失败函数

​ 避免内容太乱太复杂,对下面的函数都进行了一定的删减方便理解!

//index.js
push(location, onComplete, onAbort) {
    this.history.push(location, onComplete, onAbort)
  }

​ 调用this.$router.push()会执行history中的push方法,historyHashHistory继承至History类的实例,History作为基类,拥有公共方法,如transitionTo``confirmTransition这种公共方法,而history.push就交由不同路由模式下的子类来实现

​ 而history.push的作用也非常简单,仅仅只是将current进行结构并重新命名,然后调用HashHistory实例核心方法transitionTo,将路径传入,并将回调函数作为第二个参数,第三个参数为失败函数

//hash.js
function push (location, onComplete , onAbort) {
    const { current: fromRoute } = this
    this.transitionTo(
      location,
      //在下面有对该函数调用时机的解释
      route => {
        pushHash(route.fullPath)
        handleScroll(this.router, route, fromRoute, false)
        onComplete && onComplete(route)
      },
      onAbort
    )
  }

transitionTo

//base.js
function transitionTo (location,onComplete,onAbort) {
    //匹配到的路由,可以通过this.$route拿到
    let route = this.router.match(location, this.current)
    const prev = this.current
    this.confirmTransition(
      route,
      () => {
        //对此回调下面有详解
        this.updateRoute(route)
        onComplete && onComplete(route)
        this.ensureURL()
        this.router.afterHooks.forEach(hook => {
          hook && hook(route, prev)
        })
      },
    )
  }

​ 调用transitionTo,获取到route对象,这个对象有以下参数

键名类型定义
fullPathstring匹配到的完整路径
hashstring
matchedArray重要的参数,记录着从一级路由到要跳转的路由中所有的定义在routes里的内容
metaObject路由中的元数据,如做面包屑导航时,通过meta里定义的title,就可以拿到从一级路由到跳转路由中所有的title信息
namestring
paramsObject动态路由时获取的参数,也可以push跳转时传入,需要注意的是push跳转 params不能与path同时使用
pathstring传入的路径
queryObjectpath后面拼接?a=1 ,query里就有{a=1}

​ 接着调用confirmTransition方法


confirmTransition

confirmTransition方法主要做了4件事,以下将一一说明

function confirmTransition(route, onComplete){
    //此处current是要离开的路由
  	const current = this.current
    ...
}

1. 比对

进入路由与离开路由中matched数组中内容进行比对,拿到updateddeactivatedactivated

如:/foo/nav -> /foo/bar

  • updated: 可复用的配置,定义在routes里的 /foo 配置就是可以复用的

  • deactivated:即将离开的配置,/nav 就是即将离开的配置

  • activated: 需要更新的配置, /bar 就是需要更新的配置

  • //resolveQueue函数非常简单,就是找到两者最大长度,然后遍历从0开始找相同,然后找不同
    const { updated, deactivated, activated } = resolveQueue(
          this.current.matched,
          route.matched
        )
    

2. 获取导航守卫

通过updateddeactivatedactivated,创建导航守卫队列,队列中有以下守卫

  • 通过deactivated 得到的beforeRouteLeave守卫数组,如果``deactivated 长度大于1,那么beforeRouteLeave`数组顺序是从子到父

  • 拿到全局调用的beforeEach的回调函数数组

  • 通过updated得到的beforeRouteUpdate守卫数组

  • 拿到activated路由配置中的beforeEnter方法

  • activated中所有路由组件进行解析,确保所有的异步组件解析完成后在执行接下来的守卫

  • //拍平为一维数组
    const queue = [].concat(
          //beforeRouteLeave数组
          extractLeaveGuards(deactivated),
          //beforeEach数组
          this.router.beforeHooks,
          //beforeRouteUpdate数组
          extractUpdateHooks(updated),
          //beforeEnter数组
          activated.map(m => m.beforeEnter),
          // 解析异步组件         
          resolveAsyncComponents(activated)
        )
    

3. 创建迭代器函数

该函数接收两个参数,一个是当前的守卫hook,一个是回调函数next

  • 主要功能:对hook进行调用,传入要跳转路由,要离开的路由,以及一个回调函数

  • hook使用如:beforeEach

    • to就是要进入的路由

    • from就是要离开的路由

    • next是一个回调函数,必须在此处调用next,否则将会暂停路由切换,具体原因下面将会分析

    • router.beforeEach((to, from, next) => {
        console.log('beforeEach全局前置守卫', to, from)
        next()
      })
      
  • 迭代器具体实现

    • const iterator = (hook, next) => {
          //hook就是导航
        	//回调函数中to就是守卫中next(...)传入的参数
          hook(route, current, (to) => {
              //如果to符合以下预期,将尝试跳转
              if (
                  typeof to === 'string' ||
                  (typeof to === 'object' &&
                   (typeof to.path === 'string' || typeof to.name === 'string'))
              ) {
                  if (typeof to === 'object' && to.replace) {
                      this.replace(to)
                  } else {
                      this.push(to)
                  }
              } else {
                  //否则对iterator的next回调调用,注意:此处传入to参数无任何意义
                  next(to)
              }
          })
      }
      

4. 执行队列

使用递归方式创建类似generator函数将队列执行

​ 先将该函数贴下面,大致知道通过递归使用即可,下面将一一说明使用

function runQueue (queue, fn, cb) {
    //创建递归函数
  const step = index => {
      //当index大于queue,说明守卫队列全部被执行完毕
    if (index >= queue.length) {
        //此处cb()就是执行步骤2或步骤4
      cb()
    } else {
       //判断是否有守卫,可能存在是undefined
      if (queue[index]) {
          //fn是iterator,queue[index]是守卫,回调函数是iterator中的next
        fn(queue[index], () => {
          step(index + 1)
        })
      } else {
         //如果是undefined,就直接进行下一个守卫的调用
        step(index + 1)
      }
    }
  }
  step(0)
}
**调用runQueue时大致分类4个步骤
  • 步骤1:将queue中的守卫 逐一调用

  • 步骤2:守卫 全部调用完毕后,执行runQueue时传入的回调函数调用

  • 步骤3:重复步骤1,不过queue中的守卫是beforeRouteEnterbeforeResolve

  • 步骤4:重复步骤2,此时回调函数调用,除afterEach外所有导航守卫都被调用完毕

  • //执行步骤1
    runQueue(queue, iterator,
           //执行步骤2
          () => {
        	//提取`beforeRouteEnter` 
          const enterGuards = extractEnterGuards(activated)
          //beforeRouteEnter和beforeResolve 拼接
          const queue = enterGuards.concat(this.router.resolveHooks)
          //执行步骤3
          runQueue(queue, iterator,
            //执行步骤4
            () => {
                onComplete(route)
                if (this.router.app) {
                  this.router.app.$nextTick(() => {
                    handleRouteEntered(route)
                  })
                }
          })
        })
    

    大致执行流程如下,在守卫中必须调用next(),否则就会暂停路由导航,这也是runQueue主要做的事情,保证守卫依次执行并且必须由用户手动确定何时执行

步骤4结束后就执行onComplete函数,这个函数就是调用confirmTransition时的第二个参数,是一个回调函数,主要做了以下事情

 () => {
     this.updateRoute(route)
     onComplete && onComplete(route)
     this.ensureURL()
     this.router.afterHooks.forEach(hook => {
         hook && hook(route, prev)
     })
 }
  • 更新路由,并对_route执行赋值,触发set方法进行依赖收集,并在下次异步任务时进行更新视图
  • 调用hashHistory实例push时传入的回调函数onComplete,这个onComplete和上面的是不同的回调函数,注意区分
    • 这个onComplete回调函数主要做了如下三件事
      • 执行pushHash(route.fullPath)来更新浏览器会话的历史堆栈
      • 处理滚动
      • 执行用户传入的成功函数
  • 确保路由是当前的路由
  • 调用全局afterEach钩子

上面回调结束后会继续往下执行该判断,this.router.app是根Vue实例,在下次DOM更新后触发handleRouteEntered方法

if (this.router.app) {
    this.router.app.$nextTick(() => {
    	handleRouteEntered(route)
    })
}

该方法主要是获取BeforeRouteEnter中传入的回调,因为在BeforeRouteEnter中无法获取this,此时实例还没有被创建

beforeRouteEnter 守卫 不能 访问 this,因为守卫在导航确认前被调用,因此即将登场的新组件还没被创建。

不过,你可以通过传一个回调给 next来访问组件实例。在导航被确认的时候执行回调,并且把组件实例作为回调方法的参数。

beforeRouteEnter (to, from, next) {
  next(vm => {
    // 通过 `vm` 访问组件实例
  })
}

此时一次由用户调用this.$router.push()流程基本走完

完整的导航解析流程

  1. 导航被触发。
  2. 在失活的组件里调用 beforeRouteLeave 守卫。
  3. 调用全局的 beforeEach 守卫。
  4. 在重用的组件里调用 beforeRouteUpdate 守卫 (2.2+)。
  5. 在路由配置里调用 beforeEnter
  6. 解析异步路由组件。
  7. 在被激活的组件里调用 beforeRouteEnter
  8. 调用全局的 beforeResolve 守卫 (2.5+)。
  9. 导航被确认。
  10. 调用全局的 afterEach 钩子。
  11. 触发 DOM 更新。
  12. 调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入。