• vue-router源码分析(下)


    简介

    路由的概念相信大部分同学并不陌生,我们在用 Vue 开发过实际项目的时候都会用到 Vue-Router 这个官方插件来帮我们解决路由的问题。它的作用就是根据不同的路径映射到不同的视图。本文不再讲述路由的基础使用和API,不清楚的同学可以自行查阅官方文档vue-router3 对应 vue2vue-router4 对应 vue3。今天我们从源码出发以vue-router 3.5.3源码为例,一起来分析下Vue-Router的具体实现。

    由于篇幅原因,vue-router源码分析分上、中、下三篇文章讲解。

    vue-router源码分析(上)

    vue-router源码分析(中)

    vue-router源码分析(下)

    前面我们已经讲了路由的安装和实例化,下面我们来看看初始化流程。

    初始化

    前面我们说到,在安装的时候会混入全局mixin。我们知道全局混入,会影响后续创建的所有Vue实例beforeCreate首次触发是在Vue根实例实例化的时候new Vue({router})时。

    Vue.mixin({
      beforeCreate () {
        if (isDef(this.$options.router)) {
          this._routerRoot = this
          this._router = this.$options.router
          // 初始化
          this._router.init(this)
          Vue.util.defineReactive(this, '_route', this._router.history.current)
        } else {
          this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
        }
        registerInstance(this, this)
      }
      
      ...
    }) 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    由于router仅存在于Vue根实例$options上,所以,整个初始化只会被调用一次。也就是这个if (isDef(this.$options.router))只会执行一次。

    在这里我们我们重点分析下init方法。

    分析init方法

    // src/index.js
    
    ...
    
    init (app: any /* Vue component instance */) {
      process.env.NODE_ENV !== 'production' &&
        assert(
          install.installed,
          `not installed. Make sure to call \`Vue.use(VueRouter)\` ` +
            `before creating root instance.`
        )
    
      this.apps.push(app) // 保存实例
    
      // 绑定destroyed hook,避免内存泄露
      app.$once('hook:destroyed', () => {
        const index = this.apps.indexOf(app)
        if (index > -1) this.apps.splice(index, 1)
        // 需要确保始终有个主应用
        if (this.app === app) this.app = this.apps[0] || null
    
        if (!this.app) this.history.teardown()
      })
    
      // main app已经存在,则不需要重复初始化history 的事件监听
      if (this.app) {
        return
      }
    
      this.app = app
    
      const history = this.history
    
      if (history instanceof HTML5History || history instanceof HashHistory) {
        const handleInitialScroll = routeOrError => {
          const from = history.current
          const expectScroll = this.options.scrollBehavior
          const supportsScroll = supportsPushState && expectScroll
    
          if (supportsScroll && 'fullPath' in routeOrError) {
            handleScroll(this, routeOrError, from, false)
          }
        }
        const setupListeners = routeOrError => {
          history.setupListeners()
          handleInitialScroll(routeOrError)
        }
        history.transitionTo(
          history.getCurrentLocation(),
          setupListeners,
          setupListeners
        )
      }
    
      // 调用父类的listen方法,添加回调; 
      // 回调会在父类的updateRoute方法被调用时触发,重新为app._route赋值
      // 由于app._route被定义为响应式,所以app._route发生变化,依赖app._route的组件(route-view组件)都会被重新渲染
      history.listen(route => {
        this.apps.forEach(app => {
          app._route = route
        })
      })
    } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63

    可以看到,init方法主要做了下面几件事

    1. 检查了VueRouter是否已经安装

    2. 保存了挂载router实例vue实例VueRouter支持多实例嵌套,所以存在this.apps来保存持有router实例vue实例

    3. 注册了一个一次性钩子destroyed,在destroyed时,卸载this.app,避免内存泄露。

    4. 检查了this.app,避免重复事件监听。

    5. 根据history类型,调用transitionTo跳转到初始页面,并调用setupListeners函数初始化路由变化的监听。

    6. 注册updateRoute回调,在route更新时,更新app._route完成页面重新渲染。

    我们重点看下transitionTo相关逻辑

    分析transitionTo方法

    // src/history/base.js
    
    transitionTo (
      location: RawLocation, // 原始location,一个url或者是一个Location
      onComplete?: Function, // 跳转成功回调
      onAbort?: Function // 跳转失败回调
    ) {
      let route
      try {
        // 传入需要跳转的location和当前路由对象,返回to的Route
        route = this.router.match(location, this.current)
      } catch (e) {
        this.errorCbs.forEach(cb => {
          cb(e)
        })
        // Exception should still be thrown
        throw e
      }
      // 保存之前的Route对象
      const prev = this.current
      // 确认跳转
      this.confirmTransition(
        route,
        () => {
          // 更新最新路由并更新所有vue实例_route属性
          this.updateRoute(route)
          // 执行监听回调,后面url的变化会自动触发transitionTo方法
          onComplete && onComplete(route)
          // 变更浏览器url
          this.ensureURL()
          // 执行钩子函数
          this.router.afterHooks.forEach(hook => {
            hook && hook(route, prev)
          })
    
          // 触发ready回调 只触发一次
          if (!this.ready) {
            this.ready = true
            this.readyCbs.forEach(cb => {
              cb(route)
            })
          }
        },
        err => {
          if (onAbort) {
            onAbort(err)
          }
          if (err && !this.ready) {
            if (!isNavigationFailure(err, NavigationFailureType.redirected) || prev !== START) {
              this.ready = true
              // 触发error回调
              this.readyErrorCbs.forEach(cb => {
                cb(err)
              })
            }
          }
        }
      )
    } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59

    上面说到,在初始化时,会调用transitionTo跳转到初始页面。

    为什么要跳转初始页面?因为在初始化时,url可能指向其它页面(比如我们在/about刷新页面),此时需要调用getCurrentLocation方法,从当前url上解析出路由,然后跳转。

    我们来分析下transitionTo方法

    使用match方法匹配路由

    match 方法它的作用是根据传入的 raw 和当前的路径currentRoute计算出一个新的Route对象并返回。这其实就是路由匹配过程。

    调用了this.router.match(location, this.current)方法,match 方法接收 3 个参数,其中rawRawLocation 类型,它可以是一个 url 字符串,也可以是一个 Location 对象。currentRouteRoute 类型,它表示当前的路径。redirectedFrom 和重定向相关,这里先忽略。

    我们先来看看LocationRawLocationRoute的数结构

    export interface Location {
      name?: string
      path?: string
      hash?: string
      query?: Dictionary<string | (string | null)[] | null | undefined>
      params?: Dictionary<string>
      append?: boolean
      replace?: boolean
    } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    RawLocation是联合类型,是字符串或者Location

    我们使用this.$router.push()this.$router.replace()里面的对象就是RawLocation类型。

    export type RawLocation = string | Location 
    
    • 1

    这个对象就是我们this.$route对象。

    export interface Route {
      path: string
      name?: string | null
      hash: string
      query: Dictionary<string | (string | null)[]>
      params: Dictionary<string>
      fullPath: string
      matched: RouteRecord[]
      redirectedFrom?: string
      meta?: RouteMeta
    } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    下面我们来看看match方法。

    在实例化的时候我们分析createMatcher的时候有提到这个match方法,match就是用来完成url和路由的匹配工作。

    function match ( raw: RawLocation,
      currentRoute?: Route,
      redirectedFrom?: Location ): Route {
      // 获取格式化后的location
      const location = normalizeLocation(raw, currentRoute, false, router)
      const { name } = location
      
      // 通过name匹配
      if (name) {
        const record = nameMap[name]
        if (process.env.NODE_ENV !== 'production') {
          warn(record, `Route with name '${name}' does not exist`)
        }
        // 没找到返回新Route对象
        if (!record) return _createRoute(null, location)
        // 获取动态路由参数名
        const paramNames = record.regex.keys
          .filter(key => !key.optional)
          .map(key => key.name)
        
        // params参数
        if (typeof location.params !== 'object') {
          location.params = {}
        }
    
        // 提取当前Route中路由params赋值给location
        if (currentRoute && typeof currentRoute.params === 'object') {
          for (const key in currentRoute.params) {
            if (!(key in location.params) && paramNames.indexOf(key) > -1) {
              location.params[key] = currentRoute.params[key]
            }
          }
        }
        
        // 填充params
        location.path = fillParams(record.path, location.params, `named route "${name}"`)
        // 创建Route
        return _createRoute(record, location, redirectedFrom)
        
      // 通过path匹配
      } else if (location.path) {
        location.params = {}
        for (let i = 0; i < pathList.length; i++) {
          const path = pathList[i]
          const record = pathMap[path]
          if (matchRoute(record.regex, location.path, location.params)) {
            // 创建Route
            return _createRoute(record, location, redirectedFrom)
          }
        }
      }
      // name、path都不匹配创建新Route
      return _createRoute(null, location)
    } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54

    我们来分析下match方法。

    1。 首先它对传入的raw地址,使用normalizeLocation方法进行了格式化。

    1. 然后取出格式化地址中的name

    2. name存在,判断是否能通过namenameMap中找到对应的路由记录RouteRecord

    3. 可以找到,则通过fillParams方法填充params,并使用此路由记录通过_createRoute方法创建一个新的Route对象返回。

    4. 无法找到,则通过_createRoute方法创建一个新Route对象返回。

    5. name不存在,则判断path是否存在,存在,则利用pathList、pathMap调用matchRoute方法判断是否匹配,进而找到匹配的路由记录,然后使用此路由记录通过_createRoute方法创建新Route对象返回。

    6. 如果namepath都不存在,则通过_createRoute方法直接创建一个新Route对象返回

    我们再来分析下上面提到的几个重要方法

    地址格式化 normalizeLocation
    // src/util/location.js
    
    export function normalizeLocation ( raw: RawLocation, // 原始location,一个string,或者是一个已经格式化后的location
      current: ?Route, // 当前路由对象
      append: ?boolean, // 是否是追加模式
      router: ?VueRouter // VueRouter实例 ): Location {
      let next: Location = typeof raw === 'string' ? { path: raw } : raw
      // named target
      if (next._normalized) {
        return next
      } else if (next.name) {
        next = extend({}, raw)
        const params = next.params
        if (params && typeof params === 'object') {
          next.params = extend({}, params)
        }
        return next
      }
    
      // relative params
      if (!next.path && next.params && current) {
        next = extend({}, next)
        next._normalized = true
        const params: any = extend(extend({}, current.params), next.params)
        if (current.name) {
          next.name = current.name
          next.params = params
        } else if (current.matched.length) {
          const rawPath = current.matched[current.matched.length - 1].path
          next.path = fillParams(rawPath, params, `path ${current.path}`)
        } else if (process.env.NODE_ENV !== 'production') {
          warn(false, `relative params navigation requires a current route.`)
        }
        return next
      }
    
      const parsedPath = parsePath(next.path || '')
      const basePath = (current && current.path) || '/'
      const path = parsedPath.path
        ? resolvePath(parsedPath.path, basePath, append || next.append)
        : basePath
    
      const query = resolveQuery(
        parsedPath.query,
        next.query,
        router && router.options.parseQuery
      )
    
      let hash = next.hash || parsedPath.hash
      if (hash && hash.charAt(0) !== '#') {
        hash = `#${hash}`
      }
    
      return {
        _normalized: true,
        path,
        query,
        hash
      }
    } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    1. 首先将string类型的转换为对象形式,方便后面统一处理

    2. 如果发现地址已经做过格式化处理,则直接返回

    3. 再判断是否是命名路由,若是,则拷贝原始地址raw,拷贝params,直接返回

    4. 处理了仅携带参数的相对路由(相对参数)跳转,就是this.$router.push({params:{id:1}})形式

    5. 处理通过path跳转的方式

    6. 经过上面一番处理,无论传入何种地址,都返回一个带有_normalized:true标识的Location类型的对象

    matchRoute
    // 检查path是否能通过regex的匹配,并对params对象正确赋值
    function matchRoute ( regex: RouteRegExp,
      path: string,
      params: Object ): boolean {
      const m = path.match(regex)
    
      if (!m) {
        return false
      } else if (!params) {
        return true
      }
    
      for (let i = 1, len = m.length; i < len; ++i) {
        const key = regex.keys[i - 1]
        if (key) {
          // Fix #1994: using * with props: true generates a param named 0
          params[key.name || 'pathMatch'] = typeof m[i] === 'string' ? decode(m[i]) : m[i]
        }
      }
    
      return true
    } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    通过方法签名,可以知道它返回一个boolean值,这个值代表传入的path是否能通过regex的匹配

    虽然返回一个boolean值,但是其内部还做了件很重要的事,从path上提取动态路由参数值,我们看下完整逻辑

    1. 首先调用path.match(regex)

    2. 不能匹配直接返回false

    3. 可以匹配且无params,返回true

    4. 剩下的就只有一种情况,可以匹配且params存在,此时需要对params进行正确赋值。这里的参数是路由动态参数。即/user/:id + /user/123 -> {id:123}

    fillParams

    fillParams可以看做是matchRoute的逆操作,是一个借助动态路径,使用参数生成 url 的过程;即/user/:id + {id:123} -> /user/123

    export function fillParams ( path: string,
      params: ?Object,
      routeMsg: string ): string {
      params = params || {}
      try {
        const filler =
          regexpCompileCache[path] ||
          (regexpCompileCache[path] = Regexp.compile(path))
    
        // Fix #2505 resolving asterisk routes { name: 'not-found', params: { pathMatch: '/not-found' }}
        // and fix #3106 so that you can work with location descriptor object having params.pathMatch equal to empty string
        if (typeof params.pathMatch === 'string') params[0] = params.pathMatch
    
        return filler(params, { pretty: true })
      } catch (e) {
        if (process.env.NODE_ENV !== 'production') {
          // Fix #3072 no warn if `pathMatch` is string
          warn(typeof params.pathMatch === 'string', `missing param for ${routeMsg}: ${e.message}`)
        }
        return ''
      } finally {
        // delete the 0 if it was added
        delete params[0]
      }
    } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    可以发现通过name匹配是有params赋值操作的,但是通过path匹配是没有的。所以也可以得出,通过path就行路由跳转是不能传递params的原因。

    通过path跳转的才有动态参数的处理过程,所以也可以得出只有通过path跳转才能传递路由动态参数。

    创建路由对象_createRoute

    _createRoute方法实现的作用是找到与地址匹配的路由对象。该方法分普通、重定向、别名三种创建方式。

    //src/create-matcher.js
    
    function _createRoute ( record: ?RouteRecord,
      location: Location,
      redirectedFrom?: Location ): Route {
      if (record && record.redirect) {
        return redirect(record, redirectedFrom || location)
      }
      if (record && record.matchAs) {
        return alias(record, location, record.matchAs)
      }
      return createRoute(record, location, redirectedFrom, router)
    } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    不管哪种创建方式其核心都是调用了createRoute方法

    // src/util/route.js
    
    export function createRoute ( record: ?RouteRecord,
      location: Location,
      redirectedFrom?: ?Location,
      router?: VueRouter ): Route {
      const stringifyQuery = router && router.options.stringifyQuery
    
      // 处理query参数
      let query: any = location.query || {}
      try {
        query = clone(query)
      } catch (e) {}
    
      const route: Route = {
        name: location.name || (record && record.name),
        meta: (record && record.meta) || {},
        path: location.path || '/',
        hash: location.hash || '',
        query,
        params: location.params || {},
        fullPath: getFullPath(location, stringifyQuery), // 完整path
        matched: record ? formatMatch(record) : [] // 获取所有匹配的路由记录
      }
      
      // 如果是从其它路由对重定向过来的,则需要记录重定向之前的地址
      if (redirectedFrom) {
        route.redirectedFrom = getFullPath(redirectedFrom, stringifyQuery)
      }
      // 调用`Object.freeze`冻结新`Route` 对象,因为`Route`对象是`immutable`的
      return Object.freeze(route)
    } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    createRoute方法创建的就是一个Route对象。

    这里我们可以看到 query参数是取得location.query,这也就解释了为什么query参数在刷新的时候不会丢失。因为一直在浏览器路径里。params参数是在location.params上,是临时赋值的一个属性,所以刷新会丢失。

    使用confirmTransition方法路由跳转

    // src/base.js
    
    confirmTransition (route: Route, onComplete: Function, onAbort?: Function) {
      // 路由里面的from
      const current = this.current
      // 路由里面的to
      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
      
      // 相同Route 调用abort方法
      if (
        isSameRoute(route, current) &&
        // in the case the route map has been dynamically appended to
        lastRouteIndex === lastCurrentIndex &&
        route.matched[lastRouteIndex] === current.matched[lastCurrentIndex]
      ) {
        // ensureURL由子类实现,主要根据传参确定是添加还是替换一个记录,这里是替换当前历史记录
        this.ensureURL()
        if (route.hash) {
          handleScroll(this.router, current, route, false)
        }
        return abort(createNavigationDuplicatedError(current, route))
      }
    
      // 对比前后route的RouteRecord,找出需要更新、失活、激活的的路由记录
      const { updated, deactivated, activated } = resolveQueue(
        this.current.matched,
        route.matched
      )
    
      // 生成需要执行的守卫、钩子队列
      const queue: Array<?NavigationGuard> = [].concat(
        // in-component leave guards
        extractLeaveGuards(deactivated), // 提取路由组件中所有beforeRouteLeave守卫
        // global before hooks
        this.router.beforeHooks, // 全局的beforeEach守卫
        // in-component update hooks
        extractUpdateHooks(updated), // 提取路由组件中所有beforeRouteUpdate守卫
        // in-config enter guards
        activated.map(m => m.beforeEnter), // 路由独享的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回调,onComplete中会调用updateRoute方法,内部会触发afterEach钩子
          onComplete(route)
          if (this.router.app) {
            this.router.app.$nextTick(() => {
              handleRouteEntered(route)
            })
          }
        })
      })
    } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118

    拿到将要跳转的route后,调用confirmTransition完成route的解析跳转,并在跳转成功、取消时调用对应回调方法;这是导航解析过程。

    我们来看看这个方法主要干了那几件事情

    1. 首先定义好fromto路由对象。

    2. 定义了取消路由方法abort

    3. 首先处理了重复跳转的问题

    4. 然后通过对比找出需要更新、失活、激活的路由记录

    5. 从上述三种路由记录中抽取出对应钩子、守卫函数

    6. 将钩子及守卫函数放入队列中并执行

    判断重复跳转

    主要利用isSameRoute检测了当前路由对象和目标路由对象是否相同,若相同且二者匹配到路由记录数量相同,则视为重复跳转,此时调用abort方法并传入NavigationDuplicated错误并终止流程

    isSameRoute主要判断了path、name、hash、query、params等关键信息是否相同,若相同则视为相同路由对象。

    在确定是重复跳转后,仍然会调用子类的ensureURL方法来更新url

    // src/util/route.js
    
    // 是否相同route
    export function isSameRoute(a: Route, b: ?Route): boolean {
      if (b === START) {
        return a === b
      } else if (!b) {
        return false
      } else if (a.path && b.path) {
        // path都存在,比较path、hash、query是否相同
        return (
          a.path.replace(trailingSlashRE, '') ===
            b.path.replace(trailingSlashRE, '') &&
          a.hash === b.hash &&
          isObjectEqual(a.query, b.query)
        )
      } else if (a.name && b.name) {
        // name都存在,比较name、hash、query、params是否相同
        return (
          a.name === b.name &&
          a.hash === b.hash &&
          isObjectEqual(a.query, b.query) &&
          isObjectEqual(a.params, b.params)
        )
      } else {
        return false
      }
    } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    对比找出需要更新、失活、激活的路由记录

    判断完重复跳转后,就需要对比fromto路由对象,找出哪些路由记录需要更新,哪些失活、哪些需要激活,用来后续抽取钩子、守卫函数

    // src/history/base.js
    
    // 对比前后route的RouteRecord,找出需要更新、失活、激活的的路由记录
    const { updated, deactivated, activated } = resolveQueue(
      this.current.matched,
      route.matched
    ) 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    传入了当前和目标路由对象的记录列表,从返回值中解构出了updated, deactivated, activated

    抽取钩子、守卫函数、解析异步组件

    首先我们梳理下vue-router有哪些钩子、守卫函数

    • router.beforeEach全局前置守卫
    • router.beforeResolve全局解析守卫(v2.5.0 新增)
    • router.afterEach全局后置钩子
    • RouteConfig.beforeEnter路由独享的守卫
    • vm.beforeRouteEntervue 组件内路由进入守卫
    • vm.beforeRouteUpdatevue 组件内路由更新守卫(v2.2 新增)
    • vm.beforeRouteLeavevue 组件内路由离开守卫

    上面我们已经拿到需要更新、激活、失活的RouteRecord路由记录,我们看下分别要从中抽取出哪些守卫

    • activated中抽取beforeRouteEnter
    • deactivated中抽取beforeRouteLeave
    • updated中抽取beforeRouteUpdate
    守卫队列的执行

    队列的执行是通过runQueue、iterator相互配合来实现的

    总结

    整个导航的解析(确认),其实就是从不同状态的路由记录中抽取出对应的守卫及钩子

    然后组成队列,使用runQueueiterator巧妙的完成守卫的执行

    并在其中处理了异步组件的解析、postEnterCb中实例获取的问题

    整个守卫、钩子的执行流程如下

    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 的回调函数。
  • 相关阅读:
    (数据结构)算法的时间复杂度
    hive葵花宝典:hive函数大全
    How To Install and Configure VNC Server on Ubuntu 20.04
    NLP(7)--Embedding、池化、丢弃层
    【操作系统】24王道考研笔记——第四章 文件管理
    接口测试及接口抓包常用的测试工具
    算法进阶——字符串的排列
    LeetCode每日一题(1849. Splitting a String Into Descending Consecutive Values)
    mysql binlog同步数据
    AI影像修复及图像超分辨率
  • 原文地址:https://blog.csdn.net/weixin_53312997/article/details/125446404