Vue router源码解析(todo)

Vue-router源码解析

status: Draft tags: Vue, 源码解析 Created time: March 7, 2023 10:57 AM emoji: https://vuejs.org/logo.svg

VueRouter类

VueRouter

三种mode

实现导航

一次导航的完整步骤

vue-router一次完整的导航包含以下步骤:

  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 的回调函数,创建好的组件实例会作为回调函数的参数传入。

实现push

前端浏览器跳转页面通常有两种方式,代码跳转和浏览器跳转,代码跳转包含调用this.$router.push() 和点击<router-link>组件,浏览器跳转则是点击浏览器特定按钮,例如前进和后退按钮进行跳转。

无论是代码跳转还是浏览器跳转,其最终都调用了transitionTo函数,下面以HTML5History模式为例。

export class HTML5History extends History {
	setupListeners () {
    if (this.listeners.length > 0) {
      return
    }

    const router = this.router
    const expectScroll = router.options.scrollBehavior
    const supportsScroll = supportsPushState && expectScroll

    if (supportsScroll) {
      this.listeners.push(setupScroll())
    }

    const handleRoutingEvent = () => {
      const current = this.current

      // Avoiding first `popstate` event dispatched in some browsers but first
      // history route not updated since async guard at the same time.
      const location = getLocation(this.base)
      if (this.current === START && location === this._startLocation) {
        return
      }

      this.transitionTo(location, route => {
        if (supportsScroll) {
          handleScroll(router, route, current, true)
        }
      })
    }
    window.addEventListener('popstate', handleRoutingEvent)
    this.listeners.push(() => {
      window.removeEventListener('popstate', handleRoutingEvent)
    })
  }

	push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    const { current: fromRoute } = this
    this.transitionTo(location, route => {
      pushState(cleanPath(this.base + route.fullPath))
      handleScroll(this.router, route, fromRoute, false)
      onComplete && onComplete(route)
    }, onAbort)
  }
}

transitionTo函数

transitionTo位于类History,它是HashHistory、HTML5History 、AbstractHistory的父类。

transitionTo会再调用confirmTransition 函数来确定是否跳转路由,并传递一个onComplete函数作为完成时的回调。

export class History {
	transitionTo (
    location: RawLocation,
    onComplete?: Function,
    onAbort?: Function
  ) {
    let route
    // catch redirect option https://github.com/vuejs/vue-router/issues/3201
    try {
      route = this.router.match(location, this.current)
    } catch (e) {
      this.errorCbs.forEach(cb => {
        cb(e)
      })
      // Exception should still be thrown
      throw e
    }
    const prev = this.current
    this.confirmTransition(
      route,
      () => {
        this.updateRoute(route)
        onComplete && onComplete(route)
        this.ensureURL()
        this.router.afterHooks.forEach(hook => {
          hook && hook(route, prev)
        })

        // fire ready cbs once
        if (!this.ready) {
          this.ready = true
          this.readyCbs.forEach(cb => {
            cb(route)
          })
        }
      },
      err => {
        // ...
      }
    )
  }

confirmTransition (route: Route, onComplete: Function, onAbort?: Function) {
    const current = this.current
    this.pending = route
    const abort = err => {
      // changed after adding errors with
      // https://github.com/vuejs/vue-router/pull/3047 before that change,
      // redirect and aborted navigation would produce an err == null
      if (!isNavigationFailure(err) && isError(err)) {
        if (this.errorCbs.length) {
          this.errorCbs.forEach(cb => {
            cb(err)
          })
        } else {
          if (process.env.NODE_ENV !== 'production') {
            warn(false, 'uncaught error during route navigation:')
          }
          console.error(err)
        }
      }
      onAbort && onAbort(err)
    }
    const lastRouteIndex = route.matched.length - 1
    const lastCurrentIndex = current.matched.length - 1
    if (
      isSameRoute(route, current) &&
      // in the case the route map has been dynamically appended to
      lastRouteIndex === lastCurrentIndex &&
      route.matched[lastRouteIndex] === current.matched[lastCurrentIndex]
    ) {
      this.ensureURL()
      if (route.hash) {
        handleScroll(this.router, current, route, false)
      }
      return abort(createNavigationDuplicatedError(current, route))
    }

    const { updated, deactivated, activated } = resolveQueue(
      this.current.matched,
      route.matched
    )

    const queue: Array<?NavigationGuard> = [].concat(
      // in-component leave guards
      extractLeaveGuards(deactivated),
      // global before hooks
      this.router.beforeHooks,
      // in-component update hooks
      extractUpdateHooks(updated),
      // in-config enter guards
      activated.map(m => m.beforeEnter),
      // async components
      resolveAsyncComponents(activated)
    )

    const iterator = (hook: NavigationGuard, next) => {
      if (this.pending !== route) {
        return abort(createNavigationCancelledError(current, route))
      }
      try {
        hook(route, current, (to: any) => {
          if (to === false) {
            // next(false) -> abort navigation, ensure current URL
            this.ensureURL(true)
            abort(createNavigationAbortedError(current, route))
          } else if (isError(to)) {
            this.ensureURL(true)
            abort(to)
          } else if (
            typeof to === 'string' ||
            (typeof to === 'object' &&
              (typeof to.path === 'string' || typeof to.name === 'string'))
          ) {
            // next('/') or next({ path: '/' }) -> redirect
            abort(createNavigationRedirectedError(current, route))
            if (typeof to === 'object' && to.replace) {
              this.replace(to)
            } else {
              this.push(to)
            }
          } else {
            // confirm transition and pass on the value
            next(to)
          }
        })
      } catch (e) {
        abort(e)
      }
    }

    runQueue(queue, iterator, () => {
      // wait until async components are resolved before
      // extracting in-component enter guards
      const enterGuards = extractEnterGuards(activated)
      const queue = enterGuards.concat(this.router.resolveHooks)
      runQueue(queue, iterator, () => {
        if (this.pending !== route) {
          return abort(createNavigationCancelledError(current, route))
        }
        this.pending = null
        onComplete(route)
        if (this.router.app) {
          this.router.app.$nextTick(() => {
            handleRouteEntered(route)
          })
        }
      })
    })
  }

  updateRoute (route: Route) {
    this.current = route
    this.cb && this.cb(route)
  }
}

// runQueue函数的定义位于util/async文件下
export function runQueue (queue: Array<?NavigationGuard>, fn: Function, cb: Function) {
  const step = index => {
    if (index >= queue.length) {
      cb()
    } else {
      if (queue[index]) {
        fn(queue[index], () => {
          step(index + 1)
        })
      } else {
        step(index + 1)
      }
    }
  }
  step(0)
}

confirmTransition

confirmTransition 函数将所有要执行的函数存储到队列中,以确保执行顺序的正确。最初执行:

const queue: Array<?NavigationGuard> = [].concat(
      // 组件内beforeRouteLeave守卫
      extractLeaveGuards(deactivated),
      // 全局 beforeEach守卫
      this.router.beforeHooks,
      // 组件内update守卫(组件重用时调用)
      extractUpdateHooks(updated),
      // router配置文件中的beforeEnter守卫
      activated.map(m => m.beforeEnter),
      // 解析异步组件
      resolveAsyncComponents(activated)
    }

runQueue 函数会按顺序将这些守卫函数传入iterator 函数中执行,当用户在导航守卫中调用next()调转其他路由时,又会重新调用push跳转到目标路由。

hook(route, current, (to: any) => {
  if (to === false) {
    // next(false) -> abort navigation, ensure current URL
    this.ensureURL(true)
    abort(createNavigationAbortedError(current, route))
  } else if (isError(to)) {
    this.ensureURL(true)
    abort(to)
  } else if (
    typeof to === 'string' ||
    (typeof to === 'object' &&
      (typeof to.path === 'string' || typeof to.name === 'string'))
  ) {
    // next('/') or next({ path: '/' }) -> redirect
    abort(createNavigationRedirectedError(current, route))
    if (typeof to === 'object' && to.replace) {
      this.replace(to)
    } else {
      this.push(to)
    }
  } else {
    // confirm transition and pass on the value
    next(to)
  }
})

在confirmTransition函数中调用runQueue时,又传入了一个回调函数,以便queue执行完毕后执行。

runQueue(queue, iterator, () => {
    // wait until async components are resolved before
    // extracting in-component enter guards
    const enterGuards = extractEnterGuards(activated)
    const queue = enterGuards.concat(this.router.resolveHooks)
    runQueue(queue, iterator, () => {
      if (this.pending !== route) {
        return abort(createNavigationCancelledError(current, route))
      }
      this.pending = null
      onComplete(route)
      if (this.router.app) {
        this.router.app.$nextTick(() => {
          handleRouteEntered(route)
        })
      }
    })
  })

在这个函数中又会以相同的方式执行

  • 激活路由的beforeRouteEnter守卫

  • 全局beforeResolve守卫

最后又调用了

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

这里的onComplete来自transitionTo函数:

this.confirmTransition(
  route,
  () => {
    this.updateRoute(route)
    onComplete && onComplete(route)
    this.ensureURL()
    this.router.afterHooks.forEach(hook => {
      hook && hook(route, prev)
    })

    // fire ready cbs once
    if (!this.ready) {
      this.ready = true
      this.readyCbs.forEach(cb => {
        cb(route)
      })
    }
  },

// updateRoute
 updateRoute (route: Route) {
    this.current = route
    this.cb && this.cb(route)
  }
// onComplete 又来自最开始的HTML5History
export class HTML5History extends History {
	push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    const { current: fromRoute } = this
    this.transitionTo(location, route => {
      pushState(cleanPath(this.base + route.fullPath))
      handleScroll(this.router, route, fromRoute, false)
// 最后这里的onComplete 来自用户所传的onComplete 函数,即this.$router.push(routeObj,onCompelete)
      onComplete && onComplete(route)
    }, onAbort)
  }
}

updateRoute函数执行的过程中会调用this.cb(route) ,这里的cb函数来自前面VueRouter类的init方法中:

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

由于app._route是响应式的,改变app._route时就会导致页面重新渲染,也就是触发了DOM更新。

也就是说在这一过程中做了:

  • 更新路由

  • 执行用户onComplete函数

  • 确定路由(ensureURL)

  • 执行全局beforeEach守卫

  • 触发 DOM 更新。

为什么app._route = route先执行,但是DOM却在后面才触发更新呢?这是因为Vue组件渲染是异步的,由一个异步调度系统控制。

最后,调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入。

这里使用nextTick的原因也同样是因为要延迟到DOM更新后再执行。

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

// handleRouteEntered 位于util/route.js文件下
export function handleRouteEntered (route: Route) {
  for (let i = 0; i < route.matched.length; i++) {
    const record = route.matched[i]
    for (const name in record.instances) {
      const instance = record.instances[name]
      const cbs = record.enteredCbs[name]
      if (!instance || !cbs) continue
      delete record.enteredCbs[name]
      for (let i = 0; i < cbs.length; i++) {
        if (!instance._isBeingDestroyed) cbs[i](instance)
      }
    }
  }
}

至此,导航解析核心流程执行完毕:

  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 的回调函数,创建好的组件实例会作为回调函数的参数传入。

监听浏览器事件

组件