VueRouter笔记整理

137 阅读4分钟

当我们创建Vue-Router实例的时候,我们会选择三种模式。 对应着三种不同的history模式。
其中我们比较常用的是historyhash模式

Vue-Router 构造函数

源码路径:src/router.js

image.png

image.png 对于构造函数来讲其中比较重要的两个地方是:创建matcher实例和创建history实例

Matcher(待补充)

通过createMatche方法创建,构造函数有两个参数:routes和VueRouter实例

routes:我们在定义VueRouter时传入的对象 VueRouter实例则是当前VueRouter实例

Matcher主要用于提供addRoute、addRoutes(现已抛弃)、matchRoute等方法
它是真正存储和管理我们路由的类。

常用方法:比如match方法,这个方法会为我们匹配到一个路由并且创建它。

image.png 那这个函数做了什么事儿呢? 首先我们可以看到它先是通过name去匹配到了我们目标路由。

image.png

History

有三种类型:HTML5History、HashHistory、AbstractHistory
这三个类都继承了 History

image.png

image.png 后边我们主要以HTML5History举例

History的子类是真正为我们提供路由跳转能力的类

接下来我们主要聊聊router的push方法

当我们在调用router.push方法的时候

image.png 我们先来看方法中的参数。
其中location就是咱们平时调用push方法传的参数

image.png onComplete: 路由跳转成功之后的回调
onAbort: 路由跳转中止的回调

其实从第一张图我们可以看到,其实push方法主要调用的是我们history实例的push方法,我们接下来继续看。

image.png 这个是我们HTML5History类中的push方法。
可以看到它其实是调用了父类提供的transitionTo方法

image.png 首先我们看下transitionTo的三个参数:
第一个是location咱们就不用多说了对吧。 第二个是onComplete,它是一个跳转成功之后的回调。

那我们可以看到在history类中push方法里调用transitionTo的时候,传入了一个成功的回调函数。
回调函数中包括一个pushState和一个handleScroll
其中pushState是向浏览器的历史记录中推送记录的,后边我们详解代码实现。
handleScroll是我们在创建VueRouter时传入的回调函数,是在我们页面跳转后,用来控制页面滚动到什么位置的。

接下来我们先回到transitionTo方法

image.png 其实在方法内部它调用了一个核心方法,就是confirmTransition
它同样是接收三个参数route、onComplete、onbrot 但是不同的是这个地方的route已经是我们通过调用匹配到的新对象了

this.$router.match(location,this.current)

完整的confirmTransition函数

confirmTransition (route: Route, onComplete: Function, onAbort?: Function) {
    const current = this.current
    this.pending = route
    const abort = err => {
      // 创建用于抛出异常的函数
      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),
      // 这边是我们注册的前置路由守卫,后置路由守卫会在路由跳转以后调用
      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)
      }
    }
    // 这是类中的迭代函数,使用iterator来迭代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)
          })
        }
      })
    })
  }

其实到这里很多细节并没有补充完毕,但是基本的调用逻辑我们已经清晰了。

RouterView组件

当我们在使用VueRouter的时候经常会配合使用到RouterView组件。
这个组件是用于挂载我们的路由组件的。
其实大部分逻辑在源码中都有非常详细的讲解。
这边我们只聊重点。

image.png

可以看出,这里我们使用的是父组件的$createElement函数。
为什么要这样使用呢?
你可以理解为,我们使用的是哪个组件的$createElement,我们的组件就会渲染在哪个组件下边。
从注释也可以看出来。

// directly use parent context's createElement() function
// so that components rendered by router-view can resolve named slots

之后我们会通过matcher匹配到要进入的路由

image.png

这里我们可以看到,如果我们没有匹配到的话,就会渲染空的元素
如果匹配到的话,就存到cache里边

image.png

然后cache的内容有什么用呢?

image.png

其实主要是为keep-alive的提供缓存的支持,对于已经keep-alive的路由组件,直接拿缓存的组件渲染就可以了。 对于没有经过keep-alive的路由组件,则在匹配到后,直接进行渲染。

番外(补充pushState逻辑)

向浏览器历史记录推送数据的核心逻辑

export function pushState (url?: string, replace?: boolean) {
  saveScrollPosition()
  // try...catch the pushState call to get around Safari
  // DOM Exception 18 where it limits to 100 pushState calls
  const history = window.history
  try {
    if (replace) {
      // preserve existing history state as it could be overriden by the user
      const stateCopy = extend({}, history.state)
      stateCopy.key = getStateKey()
      history.replaceState(stateCopy, '', url)
    } else {
      history.pushState({ key: setStateKey(genStateKey()) }, '', url)
    }
  } catch (e) {
    window.location[replace ? 'replace' : 'assign'](url)
  }
}

以上内容可能有误,有什么问题欢迎大家随时讨论哦!