vue-router源碼分析

上一篇咱們寫了前端路由,這幾天比較閒,抽空研究了vue.js官方路由的vue-router的實現。html

本文主要是以vue-router2.7.0https://github.com/vuejs/vue-router)版本的源代碼進行分析。前端

首先咱們來看一下目錄結構vue

 

這裏,先大概說明一下各個文件的做用,下面會詳細講解html5

components下是兩個組件<router-view> and <router-link> 的實現node

history是路由方式的封裝git

util下主要是各類功能類和功能函數github

create-matchercreate-router-map的做用是生成匹配表vue-router

index是整個插件的入口數組

Install 提供安裝的方法瀏覽器

看源代碼以前,咱們看一下vue-router的使用方法

import Vue from 'vue'
import VueRouter from 'vue-router'
//註冊插件
Vue.use(VueRouter)
// 1. 定義(路由)組件。
// 能夠從其餘文件 import 進來
const Foo = { template: '<div>foo</div>' }
const Bar = { template: '<div>bar</div>' }


// 2. 定義路由
// 每一個路由應該映射一個組件。 其中"component" 能夠是
const routes = [
  { path: '/foo', component: Foo },
  { path: '/bar', component: Bar }
]

// 3. 建立 router 實例,而後傳 `routes` 配置
const router = new VueRouter({
  routes 
})

// 4. 建立和掛載根實例。
// 記得要經過 router 配置參數注入路由,
// 從而讓整個應用都有路由功能
// 使用 router-link 組件來導航.
// 路由出口
// 路由匹配到的組件將渲染在這裏
const app = 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')

插件安裝

利用 Vue.js 提供的插件機制 .use(plugin) 來安裝 VueRouter,而這個插件機制則會調用該 plugin 對象的 install 方法

這裏咱們先分析註冊vueRouter類的兩個文件 src/index.js 和src/install.js

Index文件主要暴露了一個vueRouter

xport default class VueRouter {


  constructor (options: RouterOptions = {}) {
    .....
  }
   //初始化函數
  init (app: any /* Vue component instance */) {
 
    this.apps.push(app)

    // main app already initialized.

    if (this.app) {
      return
    }
    //初次初始化,即首次進入頁面路由指定展現
    this.app = app

    const history = this.history

    if (history instanceof HTML5History) {
      history.transitionTo(history.getCurrentLocation())
    } else if (history instanceof HashHistory) {
      //創建hash監控
      const setupHashListener = () => {
        history.setupListeners()
      }
      history.transitionTo(
        history.getCurrentLocation(),
        setupHashListener,
        setupHashListener
      )
    }
    
    history.listen(route => {
      this.apps.forEach((app) => {
        app._route = route
      })
    })
  }
}

VueRouter.install = install
VueRouter.version = '__VERSION__'

//在外部直接引用vue-router,自動使用插件
if (inBrowser && window.Vue) {
  window.Vue.use(VueRouter)
}

這裏是vue插件的經典寫法,給插件對象增長 install 方法用來安裝插件具體邏輯,同時在最後判斷下若是是在瀏覽器環境且存在 window.Vue 的話就會自動使用插件。

接下來,咱們從installvue如何安裝插件

import View from './components/view'
import Link from './components/link'

export let _Vue

export function install(Vue) {
    if (install.installed) return
    install.installed = true
    //私有化vue,方便引入
    _Vue = Vue

    const isDef = v => v !== undefined

    const registerInstance = (vm, callVal) => {
        let i = vm.$options._parentVnode
        if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
            i(vm, callVal)
        }
    }

    Vue.mixin({
        beforeCreate() {
            //首次進入初始化路由
            if (isDef(this.$options.router)) {
                //根組件指向本身
                this._routerRoot = this
                this._router = this.$options.router
                    //初次進入對頁面進行路由
                this._router.init(this)
                    //監控 router數據變化,這裏爲更新router-view
                Vue.util.defineReactive(this, '_route', this._router.history.current)
            } else {
                //爲每一個組件傳遞根組件,方便訪問router信息
                this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
            }
            registerInstance(this, this)
        },
        destroyed() {
            registerInstance(this)
        }
    })

    /**router訪問的是根組件的router對象,就是傳入的router**/
    Object.defineProperty(Vue.prototype, '$router', {
        get() { return this._routerRoot._router }
    })

    /**route訪問的是根組件的router對象,就是傳入的route**/
    Object.defineProperty(Vue.prototype, '$route', {
            get() { return this._routerRoot._route }
        })
        //註冊router-view和router-link組件
    Vue.component('router-view', View)
    Vue.component('router-link', Link)

    const strats = Vue.config.optionMergeStrategies
        // use the same hook merging strategy for route hooks
    strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created
}

install作了如下操做

一、對全部vue實例混入beforeCreate操做,設置_routerRoot向上傳遞指向根組件,根組件設置router對象

二、根組件首次進入的時候,初始化路由,將router對象掛載到根組件元素_router上,而且設置劫持數據_route

三、經過給 Vue.prototype 定義 $router$route 屬性把他們注入到全部組件中(主要是爲了方便訪問routerroute

四、註冊兩個組件outer-viewrouter-link

建立 router 實例

在使用的實例中,咱們看到安裝完插件後,會實例一個router對象,把路由配置的數組做爲參數傳入,而且將其傳入vue實例的options中。接下來咱們看VueRouter類的做用。VueRouterindex.js文件中

export default class VueRouter {
  static install: () => void;
  static version: string;

  app: any;
  apps: Array<any>;
  ready: boolean;
  readyCbs: Array<Function>;
  options: RouterOptions;
  mode: string;
  history: HashHistory | HTML5History | AbstractHistory;
  matcher: Matcher;
  fallback: boolean;
  beforeHooks: Array<?NavigationGuard>;
  resolveHooks: Array<?NavigationGuard>;
  afterHooks: Array<?AfterNavigationHook>;

  constructor (options: RouterOptions = {}) {
    this.app = null
    this.apps = []
    this.options = options
    this.beforeHooks = []
    this.resolveHooks = []
    this.afterHooks = []
    this.matcher = createMatcher(options.routes || [], this) //生成匹配表
    
    /*路由模式***/
    let mode = options.mode || 'hash'
    /**兼容低版本不支持history模式*/
    this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false
    if (this.fallback) {
      mode = 'hash'
    }
    /**非瀏覽器***/
    if (!inBrowser) {
      mode = 'abstract'
    }
    this.mode = mode
    
    /**門面模式封裝history***/
    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}`)
        }
    }
  }

這裏能夠看到構造函數中有一步操做

 this.matcher = createMatcher(options.routes || [], this)

將傳入的routes配置數組處理爲 mather屬性的值,順藤摸瓜,咱們去createMatcher建立的文件src/create-matcher.js 文件中看他到底作了什麼操做。

export function createMatcher (
  routes: Array<RouteConfig>,
  router: VueRouter
): Matcher {
  const { pathList, pathMap, nameMap } = createRouteMap(routes) //生成routermap表

  function addRoutes (routes) {
    createRouteMap(routes, pathList, pathMap, nameMap)
  }

  function match (
    raw: RawLocation,
    currentRoute?: Route,
    redirectedFrom?: Location
  ): Route {
    const location = normalizeLocation(raw, currentRoute, false, router)
    const { name } = location

    if (name) {
      const record = nameMap[name]
      if (process.env.NODE_ENV !== 'production') {
        warn(record, `Route with name '${name}' does not exist`)
      }
      if (!record) return _createRoute(null, location)
      const paramNames = record.regex.keys
        .filter(key => !key.optional)
        .map(key => key.name)

      if (typeof location.params !== 'object') {
        location.params = {}
      }

      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]
          }
        }
      }

      if (record) {
        location.path = fillParams(record.path, location.params, `named route "${name}"`)
        return _createRoute(record, location, redirectedFrom)
      }
    } 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)) {
          return _createRoute(record, location, redirectedFrom)
        }
      }
    }
    // no match
    return _createRoute(null, location)
  }

  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
  }
}

在代碼中,咱們看到了createMatcher再一次將routes配置數組傳給了createRouteMap進一步處理

根據代碼分析createMatcher就是根據傳入的routes生成路由map對應表,而且返回match函數以及一個能夠增長路由配置項addRoutes函數,向上傳遞給VueRouter類暴露的接口addRoutes

咱們繼續來看src/create-route-map.js下的createRouteMap如何生成map表的

export function createRouteMap (
  routes: Array<RouteConfig>,
  oldPathList?: Array<string>,
  oldPathMap?: Dictionary<RouteRecord>,
  oldNameMap?: Dictionary<RouteRecord>
): {
  pathList: Array<string>;
  pathMap: Dictionary<RouteRecord>;
  nameMap: Dictionary<RouteRecord>;
} {
  // the path list is used to control path matching priority
  const pathList: Array<string> = oldPathList || []  //路徑列表
  const pathMap: Dictionary<RouteRecord> = oldPathMap || Object.create(null) //path路由map
  const nameMap: Dictionary<RouteRecord> = oldNameMap || Object.create(null) //名字路由map

  routes.forEach(route => {
    addRouteRecord(pathList, pathMap, nameMap, route)
  })
  
  //確保通配符老是在最後
  // ensure wildcard routes are always at the end
  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
  }
}

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 || {}
  const normalizedPath = normalizePath(
    path,
    parent,
    pathToRegexpOptions.strict
  )

  if (typeof route.caseSensitive === 'boolean') {
    pathToRegexpOptions.sensitive = route.caseSensitive
  }
  
  //封裝route記錄
  const record: RouteRecord = {
    path: normalizedPath, //路徑
    regex: compileRouteRegex(normalizedPath, pathToRegexpOptions), //轉化爲匹配數組
    components: route.components || { default: route.component }, //關聯組件
    instances: {}, //實例
    name, //名字
    parent, //父級router
    matchAs,
    redirect: route.redirect, //跳轉
    beforeEnter: route.beforeEnter, //進入前操做
    meta: route.meta || {}, //附加參數
    props: route.props == null  //props屬性
      ? {}
      : route.components
        ? route.props
        : { default: route.props }
  }
  
  //子路由
  if (route.children) {

    //子路由收集
    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
      )
    })
  }
  
  //存儲。按路徑存儲
  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}" }`
      )
    }
  }
}

這裏能夠看到遍歷routes根據path和名name將每個路由項處理爲一個routerRecord,而且分類保存到pathMap nameMap,方便後續的匹配操做。

接下來,咱們繼續往下看vueRouter的構造函數。根據不一樣的模式生成history對象,那history對象究竟是什麼呢?咱們接下去看History類。全部的History類都繼承自基類base.js

export class History {
  router: Router; //router對象
  base: string;  //基準路徑
  current: Route;  //當前的路由
  pending: ?Route;
  cb: (r: Route) => void;  //回調
  ready: boolean; 
  readyCbs: Array<Function>;
  readyErrorCbs: Array<Function>;
  errorCbs: Array<Function>;

  // 子類實現
  +go: (n: number) => void;
  +push: (loc: RawLocation) => void;
  +replace: (loc: RawLocation) => void;
  +ensureURL: (push?: boolean) => void;
  +getCurrentLocation: () => string;

  constructor (router: Router, base: ?string) {
    this.router = router
    this.base = normalizeBase(base) //返回基準路徑
    this.current = START //route 設置當前route
    this.pending = null
    this.ready = false
    this.readyCbs = []
    this.readyErrorCbs = []
    this.errorCbs = []
  }

  listen (cb: Function) {
    this.cb = cb
  }

  onReady (cb: Function, errorCb: ?Function) {
   //*****
  }

  onError (errorCb: Function) {
    //*****
  }
  
  //路由轉化操做
  transitionTo (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    const route = this.router.match(location, this.current) //找到匹配路由
    this.confirmTransition(route, () => { //確認是否轉化
      this.updateRoute(route) //更新route
      onComplete && onComplete(route)
      this.ensureURL()

      // fire ready cbs once
      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) })
      }
    })
  }
  //確認是否轉化路由
  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) &&
      route.matched.length === current.matched.length
    ) {
      this.ensureURL()
      return abort()
    }
    //下面是各種鉤子函數的處理
    //*********************
    })
  }
 
  //更新路由
  updateRoute (route: Route) {
    const prev = this.current //跳轉前路由
    this.current = route //裝備跳轉路由
    this.cb && this.cb(route) //回調函數,這一步很重要,這個回調函數在index文件中註冊,會更新被劫持的數據 _router
    this.router.afterHooks.forEach(hook => {
      hook && hook(route, prev)
    })
  }
}

history/base.js實現了基本history的操做,history/hash.jshistory/html5.jshistory/abstract.js繼承了base,只是根據不一樣的模式封裝了一下幾個函數的基本操做

  +go: (n: number) => void;
  +push: (loc: RawLocation) => void;
  +replace: (loc: RawLocation) => void;
  +ensureURL: (push?: boolean) => void;
  +getCurrentLocation: () => string;

能夠看到有幾個重要的操做函數,transitionTo對於路由更新的控制以及更新路由,updateRoute調用了咱們在vue-router中註冊的函數

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

這一步很重要,更新_route的值,還記得咱們在install中作的操做

Vue.util.defineReactive(this, '_route', this._router.history.current)

劫持了_route對象,所裏這裏路由更新_route,致使了視圖更新。

接下來,咱們來看如何調用transitionTo,達到更新目的,transitionTo的調用都是在代碼幾個子類的實現裏面。

//hash.js
//設置路由,監控路由改變
export class HashHistory extends History {
  constructor (router: Router, base: ?string, fallback: boolean) {
    super(router, base)
    // check history fallback deeplinking
    if (fallback && checkFallback(this.base)) {
      return
    }
    ensureSlash()
  }
  setupListeners () {
    window.addEventListener('hashchange', () => {
      if (!ensureSlash()) {
        return
      }
      this.transitionTo(getHash(), route => {
        replaceHash(route.fullPath)
      })
    })
  }
 //push方法
  push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    this.transitionTo(location, route => {
      pushHash(route.fullPath)
      onComplete && onComplete(route)
    }, onAbort)
  }
  //replace方法
  replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    this.transitionTo(location, route => {
      replaceHash(route.fullPath)
      onComplete && onComplete(route)
    }, onAbort)
  }
 }

  //html5.js實現

export class HTML5History extends History {
  constructor (router: Router, base: ?string) {
    super(router, base)

    const expectScroll = router.options.scrollBehavior //指回滾方式

    if (expectScroll) {
      setupScroll()
    }
    //監控popstate事件
    window.addEventListener('popstate', e => {
      const current = this.current
      this.transitionTo(getLocation(this.base), route => {
        if (expectScroll) {
          handleScroll(router, route, current, true)
        }
      })
    })
  }

  //push
   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)
  }

  replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    const { current: fromRoute } = this
    this.transitionTo(location, route => {
      replaceState(cleanPath(this.base + route.fullPath)) //保存當前的位置信息,用於返回時候復位
      handleScroll(this.router, route, fromRoute, false)
      onComplete && onComplete(route)
    }, onAbort)
  }
}

  //abstract.js實現,這裏經過棧的數據結構來模擬路由路徑
 export class AbstractHistory extends History {
  index: number;
  stack: Array<Route>;

  constructor (router: Router, base: ?string) {
    super(router, base)
    this.stack = []
    this.index = -1
  }

  push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    this.transitionTo(location, route => {
      this.stack = this.stack.slice(0, this.index + 1).concat(route)
      this.index++
      onComplete && onComplete(route)
    }, onAbort)
  }

  replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    this.transitionTo(location, route => {
      this.stack = this.stack.slice(0, this.index).concat(route)
      onComplete && onComplete(route)
    }, onAbort)
  }
}

這裏,3種方式都提供了replacepush接口來更新路由同時hash模式監控 hashchangeH5模式監控 popstate

這裏H5模式多了一步保存當前的位置信息,用於返回時候復位的操做

除了在子類調用以外,在 vueRouter類中init也有調用

if (history instanceof HTML5History) {
      history.transitionTo(history.getCurrentLocation())
    } else if (history instanceof HashHistory) {
      //創建hash監控
      const setupHashListener = () => {
        history.setupListeners()
      }
      history.transitionTo(
        history.getCurrentLocation(),
        setupHashListener,
        setupHashListener
      )
    }
}

由於在這兩種模式下才有可能存在進入時候的不是默認頁,須要根據當前瀏覽器地址欄裏的 path 或者 hash 來激活對應的路由,此時就是經過調用 transitionTo 來達到目的

接着繼續追蹤replacepush的調用,這兩個方法的觸發經過咱們定義的router-link組件

render (h: Function) {
    const router = this.$router //路由對象
    const current = this.$route  //當前路由
    //解析 to的路徑對應路由項
    const { location, route, href } = router.resolve(this.to, current, this.append)
    
    //設置一些默認元素class
    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
    // 若是嚴格模式的話 就判斷是不是相同路由(path query params hash)
    // 不然就走包含邏輯(path包含,query包含 hash爲空或者相同)
    classes[exactActiveClass] = isSameRoute(current, compareTarget)
    classes[activeClass] = this.exact
      ? classes[exactActiveClass]
      : isIncludedRoute(current, compareTarget)
   
    //事件處理函數
    const handler = e => {
      if (guardEvent(e)) {
        if (this.replace) {//路由replace觸發改變router-view
          router.replace(location)
        } else {//路由push觸發改變router-view
          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
    }

    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)
  }
}

function guardEvent (e) {
  // 忽略帶有功能鍵的點擊
  if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) return
  // 已阻止的返回
  if (e.defaultPrevented) return
  // 右擊
  if (e.button !== undefined && e.button !== 0) return
  // `target="_blank"` 忽略
  if (e.currentTarget && e.currentTarget.getAttribute) {
    const target = e.currentTarget.getAttribute('target')
    if (/\b_blank\b/i.test(target)) return
  }
   // 阻止默認行爲 防止跳轉
  if (e.preventDefault) {
    e.preventDefault()
  }
  return true
}

//找到第一個A標籤
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
      }
    }
  }
}

能夠看到router-link綁定了click 方法調用replacepush 達到更新路由目的。

最後,咱們來看router-view如何是如何更新的

export default {
  name: 'router-view',
  functional: true, // 功能組件 純粹渲染
  props: {
    name: {
      type: String,
      default: 'default'
    }
  },
  render (_, { props, children, parent, data }) {
    //標記爲routerview
    data.routerView = true

    // directly use parent context's createElement() function
    //直接使用父組件上下文的createElement()函數
    // so that components rendered by router-view can resolve named slots
    const h = parent.$createElement
    const name = props.name
    const route = parent.$route
    const cache = parent._routerViewCache || (parent._routerViewCache = {}) //緩存

    // determine current view depth, also check to see if the tree
    // has been toggled inactive but kept-alive.
    let depth = 0
    let inactive = false 
    //解決router-view 嵌套問題
    while (parent && parent._routerRoot !== parent) {
      if (parent.$vnode && parent.$vnode.data.routerView) {
        depth++
      }
      //是不是keep-alive
      if (parent._inactive) {
        inactive = true
      }
      parent = parent.$parent
    }
    //當前view-router的嵌套深度
    data.routerViewDepth = depth

    // render previous view if the tree is inactive and kept-alive
    if (inactive) {
      return h(cache[name], data, children)
    }
    // 獲得相匹配的當前組件層級的 路由記錄
    const matched = route.matched[depth]
    // render empty node if no matched route
    if (!matched) {
      cache[name] = null
      return h()
    }
    
    //緩存組件
    const component = cache[name] = matched.components[name]

    // attach instance registration hook
    // this will be called in the instance's injected lifecycle hooks
    data.registerRouteInstance = (vm, val) => {
      // val could be undefined for unregistration
      const current = matched.instances[name]
      if (
        (val && current !== vm) ||
        (!val && current === vm)
      ) {
        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
    data.props = resolveProps(route, matched.props && matched.props[name])

    return h(component, data, children)
  }
}

能夠看到邏輯仍是比較簡單的,根據route拿到匹配的組件進行渲染就能夠了。裏面比較複雜的是對於組件的緩存處理。

這裏,整個流程就徹底走完了。可能還有些懵,咱們下面就在總結一下整個流程。

一、安裝插件

   完成了router-linkrouter-view 兩個組件的註冊,router-link用於觸發路由的變化,router-view做爲功能組件,用於觸發對應路由視圖的變化

  混入beforeCreate生命週期處理,初始化_routerRoot_router_route等數據

  全局設置VUE靜態訪問$router$route,方便後期訪問

二、根據路由配置生成router實例

  根據配置數組生成路由配置記錄表

  生成監控路由變化的hsitory對象

三、router實例傳入根VUE實例

  根據beforeCreate混入,爲根vue對象設置了劫持字段_route,用戶觸發router-view的變化

  調用init()函數,完成首次路由的渲染,首次渲染的調用路徑是 調用history.transitionTo方法,根據routermatch函數,生成一個新的route對象,接着經過confirmTransition對比一下新生成的route和當前的route對象是否改變,改變   的話觸發updateRoute,更新hsitory.current屬性,觸發根組件的_route的變化,從而致使組件的調用render函數,更新router-view

  另一種更新路由的方式是主動觸發,router-link綁定了click方法,觸發history.push或者history.replace,從而觸發history.transitionTo

  同時會監控hashchangepopstate來對路由變化做對用的處理 

相關文章
相關標籤/搜索