Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

vue-router源码分析-整体流程 #9

Open
dolymood opened this issue Dec 1, 2016 · 17 comments
Open

vue-router源码分析-整体流程 #9

dolymood opened this issue Dec 1, 2016 · 17 comments

Comments

@dolymood
Copy link
Member

dolymood commented Dec 1, 2016

在现在单页应用这么火爆的年代,路由已经成为了我们开发应用必不可少的利器;而纵观各大框架,都会有对应的强大路由支持。Vue.js 因其性能、通用、易用、体积、学习成本低等特点已经成为了广大前端们的新宠,而其对应的路由 vue-router 也是设计的简单好用,功能强大。本文就从源码来分析下 Vue.js 官方路由 vue-router 的整体流程。

本文主要以 vue-router 的 2.0.3 版本来进行分析。

首先来张整体的图:

vue-router.js流程图

先对整体有个大概的印象,下边就以官方仓库下 examples/basic 基础例子来一点点具体分析整个流程。

目录结构

先来看看整体的目录结构:

vue-router 目录结构图
和流程相关的主要需要关注点的就是 componentshistory 目录以及 create-matcher.jscreate-route-map.jsindex.jsinstall.js。下面就从 basic 应用入口开始来分析 vue-router 的整个流程。

入口

首先看应用入口的代码部分:

import Vue from 'vue'
import VueRouter from 'vue-router'

// 1. 插件
// 安装 <router-view> and <router-link> 组件
// 且给当前应用下所有的组件都注入 $router and $route 对象
Vue.use(VueRouter)

// 2. 定义各个路由下使用的组件,简称路由组件
const Home = { template: '<div>home</div>' }
const Foo = { template: '<div>foo</div>' }
const Bar = { template: '<div>bar</div>' }

// 3. 创建 VueRouter 实例 router
const router = new VueRouter({
  mode: 'history',
  base: __dirname,
  routes: [
    { path: '/', component: Home },
    { path: '/foo', component: Foo },
    { path: '/bar', component: Bar }
  ]
})

// 4. 创建 启动应用
// 一定要确认注入了 router 
// 在 <router-view> 中将会渲染路由组件
new Vue({
  router,
  template: `
    <div id="app">
      <h1>Basic</h1>
      <ul>
        <li><router-link to="/">/</router-link></li>
        <li><router-link to="/foo">/foo</router-link></li>
        <li><router-link to="/bar">/bar</router-link></li>
        <router-link tag="li" to="/bar">/bar</router-link>
      </ul>
      <router-view class="view"></router-view>
    </div>
  `
}).$mount('#app')

作为插件

上边代码中关键的第 1 步,利用 Vue.js 提供的插件机制 .use(plugin) 来安装 VueRouter,而这个插件机制则会调用该 plugin 对象的 install 方法(当然如果该 plugin 没有该方法的话会把 plugin 自身作为函数来调用);下边来看下 vue-router 这个插件具体的实现部分。

VueRouter 对象是在 src/index.js 中暴露出来的,这个对象有一个静态的 install 方法:

/* @flow */
// 导入 install 模块
import { install } from './install'
// ...
import { inBrowser, supportsHistory } from './util/dom'
// ...

export default class VueRouter {
// ...
}

// 赋值 install
VueRouter.install = install

// 自动使用插件
if (inBrowser && window.Vue) {
  window.Vue.use(VueRouter)
}

可以看到这是一个 Vue.js 插件的经典写法,给插件对象增加 install 方法用来安装插件具体逻辑,同时在最后判断下如果是在浏览器环境且存在 window.Vue 的话就会自动使用插件。

install 在这里是一个单独的模块,继续来看同级下的 src/install.js 的主要逻辑:

// router-view router-link 组件
import View from './components/view'
import Link from './components/link'

// export 一个 Vue 引用
export let _Vue

// 安装函数
export function install (Vue) {
  if (install.installed) return
  install.installed = true
	
  // 赋值私有 Vue 引用
  _Vue = Vue

  // 注入 $router $route
  Object.defineProperty(Vue.prototype, '$router', {
    get () { return this.$root._router }
  })

  Object.defineProperty(Vue.prototype, '$route', {
    get () { return this.$root._route }
  })
  // beforeCreate mixin
  Vue.mixin({
    beforeCreate () {
      // 判断是否有 router
      if (this.$options.router) {
      	// 赋值 _router
        this._router = this.$options.router
        // 初始化 init
        this._router.init(this)
        // 定义响应式的 _route 对象
        Vue.util.defineReactive(this, '_route', this._router.history.current)
      }
    }
  })

  // 注册组件
  Vue.component('router-view', View)
  Vue.component('router-link', Link)
// ...
}

这里就会有一些疑问了?

  • 为啥要 export 一个 Vue 引用?

插件在打包的时候是肯定不希望把 vue 作为一个依赖包打进去的,但是呢又希望使用 Vue 对象本身的一些方法,此时就可以采用上边类似的做法,在 install 的时候把这个变量赋值 Vue ,这样就可以在其他地方使用 Vue 的一些方法而不必引入 vue 依赖包(前提是保证 install 后才会使用)。

  • 通过给 Vue.prototype 定义 $router$route 属性就可以把他们注入到所有组件中吗?

在 Vue.js 中所有的组件都是被扩展的 Vue 实例,也就意味着所有的组件都可以访问到这个实例原型上定义的属性。

beforeCreate mixin 这个在后边创建 Vue 实例的时候再细说。

实例化 VueRouter

在入口文件中,首先要实例化一个 VueRouter ,然后将其传入 Vue 实例的 options 中。现在继续来看在 src/index.js 中暴露出来的 VueRouter 类:

// ...
import { createMatcher } from './create-matcher'
// ...
export default class VueRouter {
// ...
  constructor (options: RouterOptions = {}) {
    this.app = null
    this.options = options
    this.beforeHooks = []
    this.afterHooks = []
    // 创建 match 匹配函数
    this.match = createMatcher(options.routes || [])
    // 根据 mode 实例化具体的 History
    let mode = options.mode || 'hash'
    this.fallback = mode === 'history' && !supportsHistory
    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)
        break
      default:
        assert(false, `invalid mode: ${mode}`)
    }
  }
// ...
}

里边包含了重要的一步:创建 match 匹配函数。

match 匹配函数

匹配函数是由 src/create-matcher.js 中的 createMatcher 创建的:

/* @flow */

import Regexp from 'path-to-regexp'
// ...
import { createRouteMap } from './create-route-map'
// ...

export function createMatcher (routes: Array<RouteConfig>): Matcher {
  // 创建路由 map
  const { pathMap, nameMap } = createRouteMap(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 {
    if (record && record.redirect) {
      return redirect(record, redirectedFrom || location)
    }
    if (record && record.matchAs) {
      return alias(record, location, record.matchAs)
    }
    return createRoute(record, location, redirectedFrom)
  }
  // 返回
  return match
}
// ...

具体逻辑后续再具体分析,现在只需要理解为根据传入的 routes 配置生成对应的路由 map,然后直接返回了 match 匹配函数。

继续来看 src/create-route-map.js 中的 createRouteMap 函数:

/* @flow */

import { assert, warn } from './util/warn'
import { cleanPath } from './util/path'

// 创建路由 map
export function createRouteMap (routes: Array<RouteConfig>): {
  pathMap: Dictionary<RouteRecord>,
  nameMap: Dictionary<RouteRecord>
} {
  // path 路由 map
  const pathMap: Dictionary<RouteRecord> = Object.create(null)
  // name 路由 map
  const nameMap: Dictionary<RouteRecord> = Object.create(null)
  // 遍历路由配置对象 增加 路由记录
  routes.forEach(route => {
    addRouteRecord(pathMap, nameMap, route)
  })

  return {
    pathMap,
    nameMap
  }
}

// 增加 路由记录 函数
function addRouteRecord (
  pathMap: Dictionary<RouteRecord>,
  nameMap: Dictionary<RouteRecord>,
  route: RouteConfig,
  parent?: RouteRecord,
  matchAs?: string
) {
  // 获取 path 、name
  const { path, name } = route
  assert(path != null, `"path" is required in a route configuration.`)
  // 路由记录 对象
  const record: RouteRecord = {
    path: normalizePath(path, parent),
    components: route.components || { default: route.component },
    instances: {},
    name,
    parent,
    matchAs,
    redirect: route.redirect,
    beforeEnter: route.beforeEnter,
    meta: route.meta || {}
  }
  // 嵌套子路由 则递归增加 记录
  if (route.children) {
// ...
    route.children.forEach(child => {
      addRouteRecord(pathMap, nameMap, child, record)
    })
  }
  // 处理别名 alias 逻辑 增加对应的 记录
  if (route.alias !== undefined) {
    if (Array.isArray(route.alias)) {
      route.alias.forEach(alias => {
        addRouteRecord(pathMap, nameMap, { path: alias }, parent, record.path)
      })
    } else {
      addRouteRecord(pathMap, nameMap, { path: route.alias }, parent, record.path)
    }
  }
  // 更新 path map
  pathMap[record.path] = record
  // 更新 name map
  if (name) {
    if (!nameMap[name]) {
      nameMap[name] = record
    } else {
      warn(false, `Duplicate named routes definition: { name: "${name}", path: "${record.path}" }`)
    }
  }
}

function normalizePath (path: string, parent?: RouteRecord): string {
  path = path.replace(/\/$/, '')
  if (path[0] === '/') return path
  if (parent == null) return path
  return cleanPath(`${parent.path}/${path}`)
}

可以看出主要做的事情就是根据用户路由配置对象生成普通的根据 path 来对应的路由记录以及根据 name 来对应的路由记录的 map,方便后续匹配对应。

实例化 History

这也是很重要的一步,所有的 History 类都是在 src/history/ 目录下,现在呢不需要关心具体的每种 History 的具体实现上差异,只需要知道他们都是继承自 src/history/base.js 中的 History 类的:

/* @flow */

// ...
import { inBrowser } from '../util/dom'
import { runQueue } from '../util/async'
import { START, isSameRoute } from '../util/route'
// 这里从之前分析过的 install.js 中 export _Vue
import { _Vue } from '../install'

export class History {
// ...
  constructor (router: VueRouter, base: ?string) {
    this.router = router
    this.base = normalizeBase(base)
    // start with a route object that stands for "nowhere"
    this.current = START
    this.pending = null
  }
// ...
}

// 得到 base 值
function normalizeBase (base: ?string): string {
  if (!base) {
    if (inBrowser) {
      // respect <base> tag
      const baseEl = document.querySelector('base')
      base = baseEl ? baseEl.getAttribute('href') : '/'
    } else {
      base = '/'
    }
  }
  // make sure there's the starting slash
  if (base.charAt(0) !== '/') {
    base = '/' + base
  }
  // remove trailing slash
  return base.replace(/\/$/, '')
}
// ...

实例化完了 VueRouter,下边就该看看 Vue 实例了。

实例化 Vue

实例化很简单:

new Vue({
  router,
  template: `
    <div id="app">
      <h1>Basic</h1>
      <ul>
        <li><router-link to="/">/</router-link></li>
        <li><router-link to="/foo">/foo</router-link></li>
        <li><router-link to="/bar">/bar</router-link></li>
        <router-link tag="li" to="/bar">/bar</router-link>
      </ul>
      <router-view class="view"></router-view>
    </div>
  `
}).$mount('#app')

options 中传入了 router,以及模板;还记得上边没具体分析的 beforeCreate mixin 吗,此时创建一个 Vue 实例,对应的 beforeCreate 钩子就会被调用:

// ...
  Vue.mixin({
    beforeCreate () {
      // 判断是否有 router
      if (this.$options.router) {
      	// 赋值 _router
        this._router = this.$options.router
        // 初始化 init
        this._router.init(this)
        // 定义响应式的 _route 对象
        Vue.util.defineReactive(this, '_route', this._router.history.current)
      }
    }
  })

具体来说,首先判断实例化时 options 是否包含 router,如果包含也就意味着是一个带有路由配置的实例被创建了,此时才有必要继续初始化路由相关逻辑。然后给当前实例赋值 _router,这样在访问原型上的 $router 的时候就可以得到 router 了。

下边来看里边两个关键:router.init 和 定义响应式的 _route 对象。

router.init

然后来看 routerinit 方法就干了哪些事情,依旧是在 src/index.js 中:

/* @flow */

import { install } from './install'
import { createMatcher } from './create-matcher'
import { HashHistory, getHash } from './history/hash'
import { HTML5History, getLocation } from './history/html5'
import { AbstractHistory } from './history/abstract'
import { inBrowser, supportsHistory } from './util/dom'
import { assert } from './util/warn'

export default class VueRouter {
// ...
  init (app: any /* Vue component instance */) {
// ...
    this.app = app

    const history = this.history

    if (history instanceof HTML5History) {
      history.transitionTo(getLocation(history.base))
    } else if (history instanceof HashHistory) {
      history.transitionTo(getHash(), () => {
        window.addEventListener('hashchange', () => {
          history.onHashChange()
        })
      })
    }

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

可以看到初始化主要就是给 app 赋值,针对于 HTML5HistoryHashHistory 特殊处理,因为在这两种模式下才有可能存在进入时候的不是默认页,需要根据当前浏览器地址栏里的 path 或者 hash 来激活对应的路由,此时就是通过调用 transitionTo 来达到目的;而且此时还有个注意点是针对于 HashHistory 有特殊处理,为什么不直接在初始化 HashHistory 的时候监听 hashchange 事件呢?这个是为了修复vuejs/vue-router#725这个 bug 而这样做的,简要来说就是说如果在 beforeEnter 这样的钩子函数中是异步的话,beforeEnter 钩子就会被触发两次,原因是因为在初始化的时候如果此时的 hash 值不是以 / 开头的话就会补上 #/,这个过程会触发 hashchange 事件,所以会再走一次生命周期钩子,也就意味着会再次调用 beforeEnter 钩子函数。

来看看这个具体的 transitionTo 方法的大概逻辑,在 src/history/base.js 中:

/* @flow */

import type VueRouter from '../index'
import { warn } from '../util/warn'
import { inBrowser } from '../util/dom'
import { runQueue } from '../util/async'
import { START, isSameRoute } from '../util/route'
import { _Vue } from '../install'

export class History {
// ...
  transitionTo (location: RawLocation, cb?: Function) {
  	// 调用 match 得到匹配的 route 对象
    const route = this.router.match(location, this.current)
    // 确认过渡
    this.confirmTransition(route, () => {
      // 更新当前 route 对象
      this.updateRoute(route)
      cb && cb(route)
      // 子类实现的更新url地址
      // 对于 hash 模式的话 就是更新 hash 的值
      // 对于 history 模式的话 就是利用 pushstate / replacestate 来更新
      // 浏览器地址
      this.ensureURL()
    })
  }
  // 确认过渡
  confirmTransition (route: Route, cb: Function) {
    const current = this.current
    // 如果是相同 直接返回
    if (isSameRoute(route, current)) {
      this.ensureURL()
      return
    }
	// 交叉比对当前路由的路由记录和现在的这个路由的路由记录
	// 以便能准确得到父子路由更新的情况下可以确切的知道
	// 哪些组件需要更新 哪些不需要更新
    const {
      deactivated,
      activated
    } = resolveQueue(this.current.matched, route.matched)
    
    // 整个切换周期的队列
    const queue: Array<?NavigationGuard> = [].concat(
      // leave 的钩子
      extractLeaveGuards(deactivated),
      // 全局 router before hooks
      this.router.beforeHooks,
      // 将要更新的路由的 beforeEnter 钩子
      activated.map(m => m.beforeEnter),
      // 异步组件
      resolveAsyncComponents(activated)
    )

    this.pending = route
    每一个队列执行的 iterator 函数
    const iterator = (hook: NavigationGuard, next) => {
      // 确保期间还是当前路由
      if (this.pending !== route) return
      hook(route, current, (to: any) => {
        if (to === false) {
          // next(false) -> abort navigation, ensure current URL
          this.ensureURL(true)
        } else if (typeof to === 'string' || typeof to === 'object') {
          // next('/') or next({ path: '/' }) -> redirect
          this.push(to)
        } else {
          // confirm transition and pass on the value
          next(to)
        }
      })
    }
	// 执行队列
    runQueue(queue, iterator, () => {
      const postEnterCbs = []
      // 组件内的钩子
      const enterGuards = extractEnterGuards(activated, postEnterCbs, () => {
        return this.current === route
      })
      // 在上次的队列执行完成后再执行组件内的钩子
      // 因为需要等异步组件以及是OK的情况下才能执行
      runQueue(enterGuards, iterator, () => {
      	// 确保期间还是当前路由
        if (this.pending === route) {
          this.pending = null
          cb(route)
          this.router.app.$nextTick(() => {
            postEnterCbs.forEach(cb => cb())
          })
        }
      })
    })
  }
  // 更新当前 route 对象
  updateRoute (route: Route) {
    const prev = this.current
    this.current = route
    // 注意 cb 的值 
    // 每次更新都会调用 下边需要用到!
    this.cb && this.cb(route)
    // 执行 after hooks 回调
    this.router.afterHooks.forEach(hook => {
      hook && hook(route, prev)
    })
  }
}
// ...

可以看到整个过程就是执行约定的各种钩子以及处理异步组件问题,这里有一些具体函数具体细节被忽略掉了(后续会具体分析)但是不影响具体理解这个流程。但是需要注意一个概念:路由记录,每一个路由 route 对象都对应有一个 matched 属性,它对应的就是路由记录,他的具体含义在调用 match() 中有处理;通过之前的分析可以知道这个 match 是在 src/create-matcher.js 中的:

// ...
import { createRoute } from './util/route'
import { createRouteMap } from './create-route-map'
// ...
export function createMatcher (routes: Array<RouteConfig>): Matcher {
  const { pathMap, nameMap } = createRouteMap(routes)
  // 关键的 match
  function match (
    raw: RawLocation,
    currentRoute?: Route,
    redirectedFrom?: Location
  ): Route {
    const location = normalizeLocation(raw, currentRoute)
    const { name } = location

    // 命名路由处理
    if (name) {
      // nameMap[name] = 路由记录
      const record = nameMap[name]
      const paramNames = getParams(record.path)
// ...
      if (record) {
        location.path = fillParams(record.path, location.params, `named route "${name}"`)
        // 创建 route
        return _createRoute(record, location, redirectedFrom)
      }
    } else if (location.path) {
      // 普通路由处理
      location.params = {}
      for (const path in pathMap) {
        if (matchRoute(path, location.params, location.path)) {
          // 匹配成功 创建route
          // pathMap[path] = 路由记录
          return _createRoute(pathMap[path], location, redirectedFrom)
        }
      }
    }
    // no match
    return _createRoute(null, location)
  }
// ...
  // 创建路由
  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)
  }

  return match
}
// ...

路由记录在分析 match 匹配函数那里以及分析过了,这里还需要了解下创建路由对象的 createRoute,存在于 src/util/route.js 中:

// ...
export function createRoute (
  record: ?RouteRecord,
  location: Location,
  redirectedFrom?: Location
): Route {
  // 可以看到就是一个被冻结的普通对象
  const route: Route = {
    name: location.name || (record && record.name),
    meta: (record && record.meta) || {},
    path: location.path || '/',
    hash: location.hash || '',
    query: location.query || {},
    params: location.params || {},
    fullPath: getFullPath(location),
    // 根据记录层级的得到所有匹配的 路由记录
    matched: record ? formatMatch(record) : []
  }
  if (redirectedFrom) {
    route.redirectedFrom = getFullPath(redirectedFrom)
  }
  return Object.freeze(route)
}
// ...
function formatMatch (record: ?RouteRecord): Array<RouteRecord> {
  const res = []
  while (record) {
    res.unshift(record)
    record = record.parent
  }
  return res
}
// ...

回到之前看的 init,最后调用了 history.listen 方法:

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

listen 方法很简单就是设置下当前历史对象的 cb 的值, 在之前分析 transitionTo 的时候已经知道在 history 更新完毕的时候调用下这个 cb。然后看这里设置的这个函数的作用就是更新下当前应用实例的 _route 的值,更新这个有什么用呢?请看下段落的分析。

defineReactive 定义 _route

继续回到 beforeCreate 钩子函数中,在最后通过 Vue 的工具方法给当前应用实例定义了一个响应式的 _route 属性,值就是获取的 this._router.history.current,也就是当前 history 实例的当前活动路由对象。给应用实例定义了这么一个响应式的属性值也就意味着如果该属性值发生了变化,就会触发更新机制,继而调用应用实例的 render 重新渲染。还记得上一段结尾留下的疑问,也就是 history 每次更新成功后都会去更新应用实例的 _route 的值,也就意味着一旦 history 发生改变就会触发更新机制调用应用实例的 render 方法进行重新渲染。

router-link 和 router-view 组件

回到实例化应用实例的地方:

new Vue({
  router,
  template: `
    <div id="app">
      <h1>Basic</h1>
      <ul>
        <li><router-link to="/">/</router-link></li>
        <li><router-link to="/foo">/foo</router-link></li>
        <li><router-link to="/bar">/bar</router-link></li>
        <router-link tag="li" to="/bar">/bar</router-link>
      </ul>
      <router-view class="view"></router-view>
    </div>
  `
}).$mount('#app')

可以看到这个实例的 template 中包含了两个自定义组件:router-linkrouter-view

router-view 组件

router-view 组件比较简单,所以这里就先来分析它,他是在源码的 src/components/view.js 中定义的:

export default {
  name: 'router-view',
  functional: true, // 功能组件 纯粹渲染
  props: {
    name: {
      type: String,
      default: 'default' // 默认default 默认命名视图的name
    }
  },
  render (h, { props, children, parent, data }) {
    // 解决嵌套深度问题
    data.routerView = true
	// route 对象
    const route = parent.$route
    // 缓存
    const cache = parent._routerViewCache || (parent._routerViewCache = {})
    let depth = 0
    let inactive = false
    // 当前组件的深度
    while (parent) {
      if (parent.$vnode && parent.$vnode.data.routerView) {
        depth++
      }
      处理 keepalive 逻辑
      if (parent._inactive) {
        inactive = true
      }
      parent = parent.$parent
    }

    data.routerViewDepth = depth
    // 得到相匹配的当前组件层级的 路由记录
    const matched = route.matched[depth]
    if (!matched) {
      return h()
    }
    // 得到要渲染组件
    const name = props.name
    const component = inactive
      ? cache[name]
      : (cache[name] = matched.components[name])

    if (!inactive) {
      // 非 keepalive 模式下 每次都需要设置钩子
      // 进而更新(赋值&销毁)匹配了的实例元素
      const hooks = data.hook || (data.hook = {})
      hooks.init = vnode => {
        matched.instances[name] = vnode.child
      }
      hooks.prepatch = (oldVnode, vnode) => {
        matched.instances[name] = vnode.child
      }
      hooks.destroy = vnode => {
        if (matched.instances[name] === vnode.child) {
          matched.instances[name] = undefined
        }
      }
    }
    // 调用 createElement 函数 渲染匹配的组件
    return h(component, data, children)
  }
}

可以看到逻辑还是比较简单的,拿到匹配的组件进行渲染就可以了。

router-link 组件

再来看看导航链接组件,他在源码的 src/components/link.js 中定义的:

// ...
import { createRoute, isSameRoute, isIncludedRoute } from '../util/route'
// ...
export default {
  name: 'router-link',
  props: {
    // 传入的组件属性们
    to: { // 目标路由的链接
      type: toTypes,
      required: true
    },
    // 创建的html标签
    tag: {
      type: String,
      default: 'a'
    },
    // 完整模式,如果为 true 那么也就意味着
    // 绝对相等的路由才会增加 activeClass
    // 否则是包含关系
    exact: Boolean,
    // 在当前(相对)路径附加路径
    append: Boolean,
    // 如果为 true 则调用 router.replace() 做替换历史操作
    replace: Boolean,
    // 链接激活时使用的 CSS 类名
    activeClass: String
  },
  render (h: Function) {
    // 得到 router 实例以及当前激活的 route 对象
    const router = this.$router
    const current = this.$route
    const to = normalizeLocation(this.to, current, this.append)
    // 根据当前目标链接和当前激活的 route匹配结果
    const resolved = router.match(to, current)
    const fullPath = resolved.redirectedFrom || resolved.fullPath
    const base = router.history.base
    // 创建的 href
    const href = createHref(base, fullPath, router.mode)
    const classes = {}
    // 激活class 优先当前组件上获取 要么就是 router 配置的 linkActiveClass
    // 默认 router-link-active
    const activeClass = this.activeClass || router.options.linkActiveClass || 'router-link-active'
    // 相比较目标
    // 因为有命名路由 所有不一定有path
    const compareTarget = to.path ? createRoute(null, to) : resolved
    // 如果严格模式的话 就判断是否是相同路由(path query params hash)
    // 否则就走包含逻辑(path包含,query包含 hash为空或者相同)
    classes[activeClass] = this.exact
      ? isSameRoute(current, compareTarget)
      : isIncludedRoute(current, compareTarget)
    
    // 事件绑定
    const on = {
      click: (e) => {
        // 忽略带有功能键的点击
        if (e.metaKey || e.ctrlKey || e.shiftKey) return
        // 已阻止的返回
        if (e.defaultPrevented) return
        // 右击
        if (e.button !== 0) return
        // `target="_blank"` 忽略
        const target = e.target.getAttribute('target')
        if (/\b_blank\b/i.test(target)) return
        // 阻止默认行为 防止跳转
        e.preventDefault()
        if (this.replace) {
          // replace 逻辑
          router.replace(to)
        } else {
          // push 逻辑
          router.push(to)
        }
      }
    }
    // 创建元素需要附加的数据们
    const data: any = {
      class: classes
    }

    if (this.tag === 'a') {
      data.on = on
      data.attrs = { href }
    } else {
      // 找到第一个 <a> 给予这个元素事件绑定和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 {
        // 没有 <a> 的话就给当前元素自身绑定时间
        data.on = on
      }
    }
    // 创建元素
    return h(this.tag, data, this.$slots.default)
  }
}

function findAnchor (children) {
  if (children) {
    let child
    for (let i = 0; i < children.length; i++) {
      child = children[i]
      if (child.tag === 'a') {
        return child
      }
      if (child.children && (child = findAnchor(child.children))) {
        return child
      }
    }
  }
}

function createHref (base, fullPath, mode) {
  var path = mode === 'hash' ? '/#' + fullPath : fullPath
  return base ? cleanPath(base + path) : path
}

可以看出 router-link 组件就是在其点击的时候根据设置的 to 的值去调用 routerpush 或者 replace 来更新路由的,同时呢,会检查自身是否和当前路由匹配(严格匹配和包含匹配)来决定自身的 activeClass 是否添加。

小结

整个流程的代码到这里已经分析的差不多了,再来回顾下:

vue-router.js流程图

相信整体看完后和最开始的时候看到这张图的感觉是不一样的,且对于 vue-router 的整体的流程了解的比较清楚了。当然由于篇幅有限,这里还有很多细节的地方没有细细分析,后续会根据模块来进行具体的分析。

@benhaben
Copy link

流程图看不见

@BlingSu
Copy link

BlingSu commented Apr 9, 2017

看不太懂0.0

@chenEdgar
Copy link

消失的分析图。。

@dolymood
Copy link
Member Author

image
再传一次图片

@william-xue
Copy link

有需要看 才看的懂 勉强看很累

@hua1995116
Copy link

请问下,你这个图是用什么软件做的。

@Yuanfang-fe
Copy link

正在研究动态路由添加,看了这篇分析理解的更清楚了 👍

@NehoDeveloper
Copy link

路由配置在new VueRouter()前提供的,有没有可以创建实例后再动态给路由配置添加

@dolymood
Copy link
Member Author

@NehoDeveloper vue-router 2.2.0+ 的版本提供了 router.addRoutes(routes) 方法可以动态添加配置,可以参考文档 https://router.vuejs.org/zh-cn/api/router-instance.html

@lirong098
Copy link

lirong098 commented Jan 4, 2018

使用vue-router 两个页面使用同一组件不同参数来控制,(非keep-alive模式下)打开这俩个页面不是根据组件实例出俩个对象而是一直在操作一个对象,俩个页面的数据没有隔离,而是相互影响。(俩页面切换可以通过监听路由来做相应的处理,可是这样的话用户在一个页面操作的一半的数据,去看另一个页面,再回来的时候 数据就没有了,我想如果vue-router渲染同一组件不同页面时都是实例出不同的对象来渲染就解决了),以上的分析是我看了上面的分析得出的,如果有错请告知,如果没有错,请问解决此问题您有什么方案。谢了 @dolymood

@Thomas-wei
Copy link

这些业务逻辑代码里用到的一些泛型我看都是在types文件夹里,但是这些文件并没有被引用,那是如何使用这些接口的,求大神指教

@zjxzjx54
Copy link

请教下,在入口文件已经有了Vue.use(VueRouter)这,为什么还要在index文件要重复写这样的代码?
if (inBrowser && window.Vue) {
window.Vue.use(VueRouter)
}

@qianlongo
Copy link

@zjxzjx54 入口文件的Vue.use(VueRouter)是使用者自己添加的
if (inBrowser && window.Vue) {
window.Vue.use(VueRouter)
}
是如果在浏览器中,并且全局有Vue,会尝试自动注册插件,同样的插件只会注册一次
所以不是重复写代码

@xiaozhu5566
Copy link

呜呜,看不懂。我还会回来的

@DylanChen08
Copy link

DylanChen08 commented Mar 1, 2021 via email

@307590317
Copy link

使用vue-router 两个页面使用同一组件不同参数来控制,(非keep-alive模式下)打开这俩个页面不是根据组件实例出俩个对象而是一直在操作一个对象,俩个页面的数据没有隔离,而是相互影响。(俩页面切换可以通过监听路由来做相应的处理,可是这样的话用户在一个页面操作的一半的数据,去看另一个页面,再回来的时候 数据就没有了,我想如果vue-router渲染同一组件不同页面时都是实例出不同的对象来渲染就解决了),以上的分析是我看了上面的分析得出的,如果有错请告知,如果没有错,请问解决此问题您有什么方案。谢了 @dolymood

你这个你自己都已经给出了解决方案了,组件化就是为了复用,data返回函数就是为了不操作同一个对象。你导入一次组件,页面里写两个不就好了,根据参数不同展示两个其中的一个。你要非要用一个组件来展示,那就做数据缓存,把数据存到vuex里,切换的时候根据参数去取就行了,不过这样就有点本末倒置了。

@Lihr1999
Copy link

你好,有个地方不太懂,请问可以帮忙解答一下嘛?
在beforeCreate 钩子函数中,对_route 属性进行了响应式绑定,那么点击routerlink改变路由地址的时候,它是如何重新调用render函数重新渲染的?
数据响应式我记得是解析模板的时候通过new Watcher绑定回调函数更新DOM,然后在set方法去调用,在路由这块重新调用render就想不通了。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests