当前位置:   article > 正文

从源码理解 Vue-Router 实现原理_vue路由源码的实现原理

vue路由源码的实现原理

在使用 vue 开发单页面(SPA)应用时,vue-router 是必不可少的技术;它的本质是监听 URL 的变化然后匹配路由规则显示相应的页面,并且不刷新页面;简单的说就是,更新视图但不重新请求页面。

下面通过源码来看一下 vue-router 的整个实现流程:

一、vue-router 源码目录结构

github地址:https://github.com/vuejs/vue-router

当前版本:
vue 2.6.11
vue-router 3.5.1

在这里插入图片描述

components:这里面是两个组件 router-view 和 router-link
history:这个是路由模式(mode),有三种方式
util:这里是路由的功能函数和类
create-matcher 和 create-router-map 是路由解析和生成配置表
index:VueRouter类,也是整个插件的入口
install:提供插件安装方法

二、Vue.use() 注册插件

vue 在使用路由时需要调用 Vue.use(plugin) 方法进行注册,代码在 vue 源码里面 src/core/global-api/use.js文件里主要作用两个:

1、缓存判断是否已经注册过,避免重复注册
2、使用插件的 install 方法或者直接运行插件来注册
3、这里使用了 flow 的语法,在编译时对 js 变量进行类型检查,缩短调式时间减少类型错误引起的 bug

// 初始化use
export function initUse (Vue: GlobalAPI) {
  Vue.use = function (plugin: Function | Object) {
    // 检测插件是否已经被安装
    const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
    if (installedPlugins.indexOf(plugin) > -1) {
      return this
    }
    // additional parameters
    const args = toArray(arguments, 1)
    args.unshift(this)
    // 调用插件的 install 方法或者直接运行插件,以实现插件的 install
    if (typeof plugin.install === 'function') {
      plugin.install.apply(plugin, args)
    } else if (typeof plugin === 'function') {
      plugin.apply(null, args)
    }
    installedPlugins.push(plugin)
    return this
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
三、路由安装 install

注册路由的时候需要调用路由的 install 方法,代码在 vue-router 源码的 src/install.js 文件里,是 vue-router 的安装程序,该方法做了下面四件事:

1、缓存判断是否已经安装过,避免重复安装
2、使用 Vue.mixin 混入 beforeCreate 和 destroyed 钩子函数,这样在 Vue 生命周期阶段就会被调用
3、通过 Vue.prototype 定义 router 和 route 属性,方便所有组件使用
4、全局注册 router-view 和 router-link 组件;router-link 用于触发路由的变化,router-view 用于触发对应路由视图的变化

import View from './components/view'
import Link from './components/link'
export let _Vue
// Vue.use安装插件时候需要暴露的install方法 
export function install (Vue) {
  // 判断是否已安装过,安装过直接 return 出来,没安装执行安装程序 
  if (install.installed && _Vue === Vue) return
  install.installed = true
  // 把Vue赋值给全局变量 
  _Vue = Vue
  // 判断是否已定义 
  const isDef = v => v !== undefined
  //通过registerRouteInstance方法注册router实例 
  const registerInstance = (vm, callVal) => {
    let i = vm.$options._parentVnode
    if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
      i(vm, callVal)
    }
  }
  // 混淆进Vue实例,在boforeCreate与destroyed钩子上混淆 
  Vue.mixin({
    beforeCreate () {
      // 在option上面存在router则代表是根组件 
      if (isDef(this.$options.router)) {
        // 根路由设置为自己
        this._routerRoot = this
        // 保存router
        this._router = this.$options.router
        // VueRouter对象的init方法 
        this._router.init(this)
        // Vue内部方法,为对象defineProperty上在变化时通知的属性,实现响应式
        Vue.util.defineReactive(this, '_route', this._router.history.current)
      } else {
        // 非根组件则直接从父组件中获取,用于 router-view 层级判断
        this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
      }
      // 通过registerRouteInstance方法注册router实例
      registerInstance(this, this)
    },
    destroyed () {
      registerInstance(this)
    }
  })
  //在Vue.prototype挂载属性,可以通过 this.$router、this.$route 来访问 Vue.prototype 上的 _router、_route
  Object.defineProperty(Vue.prototype, '$router', {
    get () { return this._routerRoot._router }
  })
  Object.defineProperty(Vue.prototype, '$route', {
    get () { return this._routerRoot._route }
  })
  // 注册router-view以及router-link组件 
  Vue.component('RouterView', View)
  Vue.component('RouterLink', Link)

  // 该对象保存了两个option合并的规则 
  const strats = Vue.config.optionMergeStrategies
  // use the same hook merging strategy for route hooks
  strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created
}
  • 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
四、VueRouter 实例化

安装路由插件之后,会对 VueRouter 进行实例化然后将其传入 Vue 实例的 options 中,在 vue-router 源码的 src/index.js 文件里;下面是 VueRouter 的构造函数。

  constructor (options: RouterOptions = {}) {
    this.app = null
    this.apps = []
    this.options = options
    this.beforeHooks = []
    this.resolveHooks = []
    this.afterHooks = []
    // 路由匹配对象
    this.matcher = createMatcher(options.routes || [], this)
	// 根据 mode 采取不同的路由方式
    let mode = options.mode || 'hash'
    this.fallback =
      mode === 'history' && !supportsPushState && options.fallback !== false
    if (this.fallback) {
      mode = 'hash'
    }
    if (!inBrowser) {
      mode = 'abstract'
    }
    this.mode = mode

    switch (mode) {
      case 'history':
        this.history = new HTML5History(this, options.base)
        break
      case 'hash':
        this.history = new HashHistory(this, options.base, this.fallback)
        break
      case 'abstract':
        this.history = new AbstractHistory(this, options.base)
        break
      default:
        if (process.env.NODE_ENV !== 'production') {
          assert(false, `invalid mode: ${mode}`)
        }
    }
  }
  • 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

构造函数有两个重要的东西:

1、创建路由匹配对象 matcher(核心)
这里用到的 createMatcher 函数在 src/create-matcher.js 文件里,其作用是创建路由映射表,然后使用闭包的方法让 addRoutes 和 match 函数能够使用路由映射表的几个对象,最后返回一个 Matcher 对象。

//Matcher 的数据结构
export type Matcher = {
  match: (raw: RawLocation, current?: Route, redirectedFrom?: Location) => Route;
  addRoutes: (routes: Array<RouteConfig>) => void;
};
//createMatcher 具体实现
export function createMatcher (
  routes: Array<RouteConfig>,
  router: VueRouter
): Matcher {
  //创建路由映射表
  const { pathList, pathMap, nameMap } = createRouteMap(routes)
  function addRoutes (routes) {...}
  //路由匹配
  function match (
    raw: RawLocation,
    currentRoute?: Route,
    redirectedFrom?: Location
  ): Route {...}

  function redirect (
    record: RouteRecord,
    location: Location
  ): Route {...}

  function alias (
    record: RouteRecord,
    location: Location,
    matchAs: string
  ): Route {...}

  function _createRoute (
    record: ?RouteRecord,
    location: Location,
    redirectedFrom?: Location
  ): Route {...}

  return {
    match,
    addRoutes
  }
}
  • 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

createMatcher()有两个参数routes表示创建VueRouter新对象传入的routes配置信息,router表示VueRouter实例。

createRouteMap() 的作用就是对当前开发者传入的 options.routes 进行路由映射化处理,并得到了三个路由容器 pathList、pathMap、nameMap,方法在 src/create-route-map.js 文件里

export function createRouteMap (
  routes: Array<RouteConfig>,
  oldPathList?: Array<string>,
  oldPathMap?: Dictionary<RouteRecord>,
  oldNameMap?: Dictionary<RouteRecord>
): {
  pathList: Array<string>;
  pathMap: Dictionary<RouteRecord>;
  nameMap: Dictionary<RouteRecord>;
} {
  // 创建映射表
  const pathList: Array<string> = oldPathList || []
  // $flow-disable-line
  const pathMap: Dictionary<RouteRecord> = oldPathMap || Object.create(null)
  // $flow-disable-line
  const nameMap: Dictionary<RouteRecord> = oldNameMap || Object.create(null)
  // 遍历路由配置,为每个配置添加路由记录
  routes.forEach(route => {
    addRouteRecord(pathList, pathMap, nameMap, route)
  })
  // 确保通配符在最后
  for (let i = 0, l = pathList.length; i < l; i++) {
    if (pathList[i] === '*') {
      pathList.push(pathList.splice(i, 1)[0])
      l--
      i--
    }
  }
  return {
    pathList,
    pathMap,
    nameMap
  }
}
  • 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

createRouteMap 函数返回三个属性 —— pathList、pathMap、nameMap。然后通过 addRouteRecord 函数去向这三个属性中增添数据。

下面是 addRouteRecord 方法, 利用递归方式解析嵌套路由。

//添加路由记录
function addRouteRecord (
  pathList: Array<string>,
  pathMap: Dictionary<RouteRecord>,
  nameMap: Dictionary<RouteRecord>,
  route: RouteConfig,
  parent?: RouteRecord,
  matchAs?: string
) {
  //获取路由配置下的属性
  const { path, name } = route
  if (process.env.NODE_ENV !== 'production') {
    assert(path != null, `"path" is required in a route configuration.`)
    assert(
      typeof route.component !== 'string',
      `route config "component" for path: ${String(path || name)} cannot be a ` +
      `string id. Use an actual component instead.`
    )
  }
  const pathToRegexpOptions: PathToRegexpOptions = route.pathToRegexpOptions || {}
  //格式化url 替换 /
  const normalizedPath = normalizePath(
    path,
    parent,
    pathToRegexpOptions.strict
  )
  if (typeof route.caseSensitive === 'boolean') {
    pathToRegexpOptions.sensitive = route.caseSensitive
  }
  //生成记录对象
  const record: RouteRecord = {
    path: normalizedPath,
    regex: compileRouteRegex(normalizedPath, pathToRegexpOptions),
    components: route.components || { default: route.component },
    instances: {},
    name,
    parent,
    matchAs,
    redirect: route.redirect,
    beforeEnter: route.beforeEnter,
    meta: route.meta || {},
    props: route.props == null
      ? {}
      : route.components
        ? route.props
        : { default: route.props }
  }
  if (route.children) {
    // 递归路由配置的 children 属性,添加路由记录
    if (process.env.NODE_ENV !== 'production') {
      if (route.name && !route.redirect && route.children.some(child => /^\/?$/.test(child.path))) {
        warn(
          false,
          `Named Route '${route.name}' has a default child route. ` +
          `When navigating to this named route (:to="{name: '${route.name}'"), ` +
          `the default child route will not be rendered. Remove the name from ` +
          `this route and use the name of the default child route for named ` +
          `links instead.`
        )
      }
    }
    route.children.forEach(child => {
      const childMatchAs = matchAs
        ? cleanPath(`${matchAs}/${child.path}`)
        : undefined
      addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs)
    })
  }
  // 如果路由有别名的话,给别名也添加路由记录
  if (route.alias !== undefined) {
    const aliases = Array.isArray(route.alias)
      ? route.alias
      : [route.alias]

    aliases.forEach(alias => {
      const aliasRoute = {
        path: alias,
        children: route.children
      }
      addRouteRecord(
        pathList,
        pathMap,
        nameMap,
        aliasRoute,
        parent,
        record.path || '/' // matchAs
      )
    })
  }
  //更新映射表,使用键值对对解析好的路由进行记录,这样配置相同的path只有第一个会起作用,后面的都会忽略
  if (!pathMap[record.path]) {
    pathList.push(record.path)
    pathMap[record.path] = record
  }
  //命名路由添加记录
  if (name) {
    if (!nameMap[name]) {
      nameMap[name] = record
    } else if (process.env.NODE_ENV !== 'production' && !matchAs) {
      warn(
        false,
        `Duplicate named routes definition: ` +
        `{ name: "${name}", path: "${record.path}" }`
      )
    }
  }
}
  • 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

2、根据 mode 采取不同的路由方式

1、vue-router 一共有三种路由模式(mode):hash、history、abstract,其中 abstract 是在非浏览器环境下使用的路由模式,如 weex
2、默认是 hash 模式,如果传入的是 history 模式,但是当前环境不支持则会降级为 hash 模式
3、如果当前环境是非浏览器环境,则强制使用 abstract 模式
4、模式匹配成功则会进行对应的初始化操作

五、路由初始化

当根组件调用 beforeCreate 钩子函数的时候会执行路由初始化代码,代码在 src/index 文件下面,是路由实例提供的一个方法。

 /* 初始化 */
  init (app: any /* Vue component instance */) {
    /* 未安装就调用init会抛出异常 */
    process.env.NODE_ENV !== 'production' && assert(
      install.installed,
      `not installed. Make sure to call \`Vue.use(VueRouter)\` ` +
      `before creating root instance.`
    )
    /* 将当前vm实例保存在app中 */
    this.apps.push(app)
    // main app already initialized.
    /* 已存在说明已经被init过了,直接返回 */
    if (this.app) {
      return
    }
    /* this.app保存当前vm实例 */
    this.app = app
    const history = this.history
    if (history instanceof HTML5History) {
      history.transitionTo(history.getCurrentLocation())
    } else if (history instanceof HashHistory) {
      const setupHashListener = () => {
        history.setupListeners()
      }
      history.transitionTo(
        history.getCurrentLocation(),
        setupHashListener,
        setupHashListener
      )
    }
    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

路由初始化会进行路由跳转,改变 URL 后渲染对应的组件。路由跳转的核心的 history 的 transitionTo 方法。

六、路由切换

在 src/history/base 文件下 History 实例提供的一个方法。

  transitionTo (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    // 根据跳转的 location 得到新的route
    const route = this.router.match(location, this.current)
    //确认切换路由
    this.confirmTransition(route, () => {
      //更新 route
      this.updateRoute(route)
      //添加 hashChange 监听
      onComplete && onComplete(route)
      //更新 URL
      this.ensureURL()
	  //只执行一次 ready 回掉
      if (!this.ready) {
        this.ready = true
        this.readyCbs.forEach(cb => { cb(route) })
      }
    }, err => {
      if (onAbort) {
        onAbort(err)
      }
      if (err && !this.ready) {
        this.ready = true
        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

在 transitionTo 方法中先调用match方法得到新的路由对象,然后调用 confirmTransition 方法是处理导航守卫的逻辑。

  confirmTransition (route: Route, onComplete: Function, onAbort?: Function) {
    const current = this.current
    //中断跳转路由函数
    const abort = err => {
      if (isError(err)) {
        if (this.errorCbs.length) {
          this.errorCbs.forEach(cb => { cb(err) })
        } else {
          warn(false, 'uncaught error during route navigation:')
          console.error(err)
        }
      }
      onAbort && onAbort(err)
    }
    //判断路由是否相同,相同不跳转
    if (
      isSameRoute(route, current) &&
      // in the case the route map has been dynamically appended to
      route.matched.length === current.matched.length
    ) {
      this.ensureURL()
      return abort()
    }
	//对比路由,解析出可复用的组件、失活的组件、需要渲染的组件
    const {
      updated,
      deactivated,
      activated
    } = resolveQueue(this.current.matched, route.matched)
	//导航守卫数组
    const queue: Array<?NavigationGuard> = [].concat(
      //失活的组件钩子
      extractLeaveGuards(deactivated),
      //全局的 beforeEach 钩子
      this.router.beforeHooks,
      //在当前路由改变,但是该组件被复用时调用
      extractUpdateHooks(updated),
      //需要渲染组件 enter 守卫钩子
      activated.map(m => m.beforeEnter),
      //解析异步路由组件
      resolveAsyncComponents(activated)
    )
	//保存路由
    this.pending = route
    //迭代器,用于执行 queue 里面的导航守卫钩子
    const iterator = (hook: NavigationGuard, next) => {
   	  //路由不相等就不跳转
      if (this.pending !== route) {
        return abort()
      }
      try {
        //执行钩子
        hook(route, current, (to: any) => {
          if (to === false || isError(to)) {
            // next(false) -> abort navigation, ensure current URL
            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: '/' }) -> 重定向
            abort()
            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, () => {
      const postEnterCbs = []
      const isValid = () => this.current === route
      //上一次队列执行完成之后再执行组件内的钩子
      const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid)
      const queue = enterGuards.concat(this.router.resolveHooks)
      runQueue(queue, iterator, () => {
        if (this.pending !== route) {
          return abort()
        }
        this.pending = null
        onComplete(route)
        if (this.router.app) {
          this.router.app.$nextTick(() => {
            postEnterCbs.forEach(cb => { cb() })
          })
        }
      })
    })
  }
  • 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

通过 next 进行导航守卫的回调迭代,所以如果在代码中使用了路由钩子函数,那么就必须在最后调用 next(),否则回调不执行,导航将无法继续

七、路由同步(以hash模式为例)

在路由切换的时候,vue-router 会调用 push、go 等方法实现视图与地址 url 的同步。

1、主动触发
点击事件跳转页面,触发 push 或 replace 方法,然后调用 transitionTo 方法里面的 updateRoute 方法来更新 _route,从而触发 router-view 的变化。

  // src/history/hash.js
  function pushHash (path) {
	  if (supportsPushState) {
	    pushState(getUrl(path))
	  } else {
	    window.location.hash = path
	  }
  }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

push 方法先检测当前浏览器是否支持 html5的 History API,如果支持则调用此 API进行 href的修改,否则直接对window.location.hash进行赋值

2、改变地址栏 url,然后视图同步
在路由初始化的时候会添加事件 setupHashListener 来监听 hashchange 或 popstate;当路由变化时,会触发对应的 push 或 replace 方法,然后调用 transitionTo 方法里面的 updateRoute 方法来更新 _route,从而触发 router-view 的变化。

  // src/history/hash.js
  setupListeners () {
    const router = this.router
    const expectScroll = router.options.scrollBehavior
    const supportsScroll = supportsPushState && expectScroll
    if (supportsScroll) {
      setupScroll()
    }
    window.addEventListener(supportsPushState ? 'popstate' : 'hashchange', () => {
      const current = this.current
      if (!ensureSlash()) {
        return
      }
      this.transitionTo(getHash(), route => {
        if (supportsScroll) {
          handleScroll(this.router, route, current, true)
        }
        if (!supportsPushState) {
          replaceHash(route.fullPath)
        }
      })
    })
  }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
八、路由组件

在 src/components/ 文件夹下

1、router-view:组件挂载
该组件是无状态 (没有 data ) 和无实例 (没有 this 上下文)的(功能组件)函数式组件。其通过路由匹配获取到对应的组件实例,通过 h函数动态生成组件,如果当前路由没有匹配到任何组件,则渲染一个注释节点。

export default {
  name: 'RouterView',
  /* 
    https://cn.vuejs.org/v2/api/#functional
    使组件无状态 (没有 data ) 和无实例 (没有 this 上下文)。他们用一个简单的 render 函数返回虚拟节点使他们更容易渲染。
  */
  functional: true,
  props: {
    name: {
      type: String,
      default: 'default'
    }
  },
  render (_, { props, children, parent, data }) {
    // 标记位,标记是route-view组件
    data.routerView = true
    // 直接使用父组件的createElement函数 
    const h = parent.$createElement
    // props的name,默认'default' 
    const name = props.name
    // option中的VueRouter对象 
    const route = parent.$route
    // 在parent上建立一个缓存对象 
    const cache = parent._routerViewCache || (parent._routerViewCache = {})
    // 记录组件深度 
    let depth = 0
    // 标记是否是待用(非alive状态)
    let inactive = false
    // _routerRoot中中存放了根组件的实例,这边循环向上级访问,直到访问到根组件,得到depth深度 
    while (parent && parent._routerRoot !== parent) {
      if (parent.$vnode && parent.$vnode.data.routerView) {
        depth++
      }
      // 如果_inactive为true,代表是在keep-alive中且是待用(非alive状态)
      if (parent._inactive) {
        inactive = true
      }
      parent = parent.$parent
    }
    // 存放route-view组件的深度 
    data.routerViewDepth = depth
    // 如果inactive为true说明在keep-alive组件中,直接从缓存中取 
    if (inactive) {
      return h(cache[name], data, children)
    }
    const matched = route.matched[depth]
    // 如果没有匹配到的路由,则渲染一个空节点 
    if (!matched) {
      cache[name] = null
      return h()
    }
    // 从成功匹配到的路由中取出组件 
    const component = cache[name] = matched.components[name]
    // 注册实例的registration钩子,这个函数将在实例被注入的加入到组件的生命钩子(beforeCreate与destroyed)中被调用 
    data.registerRouteInstance = (vm, val) => {  
      // 第二个值不存在的时候为注销 
      // 获取组件实例 
      const current = matched.instances[name]
      if (
        (val && current !== vm) ||
        (!val && current === vm)
      ) {
        //这里有两种情况,一种是val存在,则用val替换当前组件实例,另一种则是val不存在,则直接将val赋给instances 
        matched.instances[name] = val
      }
    }
    // also register instance in prepatch hook
    // in case the same component instance is reused across different routes
    ;(data.hook || (data.hook = {})).prepatch = (_, vnode) => {
      matched.instances[name] = vnode.componentInstance
    }
    // resolve props
    let propsToPass = data.props = resolveProps(route, matched.props && matched.props[name])
    if (propsToPass) {
      // clone to prevent mutation
      propsToPass = data.props = extend({}, propsToPass)
      // pass non-declared props as attrs
      const attrs = data.attrs = data.attrs || {}
      for (const key in propsToPass) {
        if (!component.props || !(key in component.props)) {
          attrs[key] = propsToPass[key]
          delete propsToPass[key]
        }
      }
    }
    return h(component, data, children)
  }
}
  • 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

主要作用就是拿到匹配的组件进行渲染。

2、router-link:路由跳转
router-link在执行 render函数的时候,会根据当前的路由状态,给渲染出来的active元素添加 class,所以你可以借助此给active路由元素设置样式等

export default {
  name: 'RouterLink',
  props: {
    to: {
      type: toTypes,
      required: true
    },
    tag: {
      type: String,
      default: 'a'
    },
    exact: Boolean,
    append: Boolean,
    replace: Boolean,
    activeClass: String,
    exactActiveClass: String,
    event: {
      type: eventTypes,
      default: 'click'
    }
  },
  render (h: Function) {
    const router = this.$router
    const current = this.$route
    const { location, route, href } = router.resolve(this.to, current, this.append)
    const classes = {}
    const globalActiveClass = router.options.linkActiveClass
    const globalExactActiveClass = router.options.linkExactActiveClass
    // Support global empty active class
    const activeClassFallback = globalActiveClass == null
            ? 'router-link-active'
            : globalActiveClass
    const exactActiveClassFallback = globalExactActiveClass == null
            ? 'router-link-exact-active'
            : globalExactActiveClass
    const activeClass = this.activeClass == null
            ? activeClassFallback
            : this.activeClass
    const exactActiveClass = this.exactActiveClass == null
            ? exactActiveClassFallback
            : this.exactActiveClass
    const compareTarget = location.path
      ? createRoute(null, location, null, router)
      : route
    classes[exactActiveClass] = isSameRoute(current, compareTarget)
    classes[activeClass] = this.exact
      ? classes[exactActiveClass]
      : isIncludedRoute(current, compareTarget)
    // 当触发这些路由切换事件时,会调用相应的方法来切换路由刷新视图:
    const handler = e => {
      if (guardEvent(e)) {
        if (this.replace) {
          router.replace(location)
        } else {
          router.push(location)
        }
      }
    }
    const on = { click: guardEvent }
    if (Array.isArray(this.event)) {
      this.event.forEach(e => { on[e] = handler })
    } else {
      on[this.event] = handler
    }
    const data: any = {
      class: classes
    }
	// 渲染出 <a> 标签,然后添加 href 属性和点击事件
    if (this.tag === 'a') {
      data.on = on
      data.attrs = { href }
    } else {
      // find the first <a> child and apply listener and href
      const a = findAnchor(this.$slots.default)
      if (a) {
        // in case the <a> is a static node
        a.isStatic = false
        const extend = _Vue.util.extend
        const aData = a.data = extend({}, a.data)
        aData.on = on
        const aAttrs = a.data.attrs = extend({}, a.data.attrs)
        aAttrs.href = href
      } else {
        // doesn't have <a> child, apply listener to self
        data.on = on
      }
    }
    return h(this.tag, data, this.$slots.default)
  }
}
  • 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
最后、路由实现的过程简化版

页面渲染
1、Vue.use(Router) 注册
2、注册时调用 install 方法混入生命周期,定义 router 和 route 属性,注册 router-view 和 router-link 组件
3、生成 router 实例,根据配置数组(传入的routes)生成路由配置记录表,根据不同模式生成监控路由变化的History对象
4、生成 vue 实例,将 router 实例挂载到 vue 实例上面,挂载的时候 router 会执行最开始混入的生命周期函数
5、初始化结束,显示默认页面
路由点击更新
1、 router-link 绑定 click 方法,触发 history.push 或 history.replace ,从而触发 history.transitionTo 方法
2、ransitionTo 用于处理路由转换,其中包含了 updateRoute 用于更新 _route
3、在 beforeCreate 中有劫持 _route 的方法,当 _route 变化后,触发 router-view 的变化
地址变化路由更新
1、HashHistory 和 HTML5History 会分别监控 hashchange 和 popstate 来对路由变化作对用的处理
2、HashHistory 和 HTML5History 捕获到变化后会对应执行 push 或 replace 方法,从而调用 transitionTo
3、然后更新 _route 触发 router-view 的变化

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/酷酷是懒虫/article/detail/995999
推荐阅读
相关标签
  

闽ICP备14008679号