VueRouter 原始碼深度解析

yck發表於1970-01-01
VueRouter 原始碼深度解析

路由原理

在解析原始碼前,先來了解下前端路由的實現原理。 前端路由實現起來其實很簡單,本質就是監聽 URL 的變化,然後匹配路由規則,顯示相應的頁面,並且無須重新整理。目前單頁面使用的路由就只有兩種實現方式

  • hash 模式
  • history 模式

www.test.com/#/ 就是 Hash URL,當 # 後面的雜湊值發生變化時,不會向伺服器請求資料,可以通過 hashchange 事件來監聽到 URL 的變化,從而進行跳轉頁面。

VueRouter 原始碼深度解析

History 模式是 HTML5 新推出的功能,比之 Hash URL 更加美觀

VueRouter 原始碼深度解析

VueRouter 原始碼解析

重要函式思維導圖

以下思維導圖羅列了原始碼中重要的一些函式

VueRouter 原始碼深度解析

路由註冊

在開始之前,推薦大家 clone 一份原始碼對照著看。因為篇幅較長,函式間的跳轉也很多。

使用路由之前,需要呼叫 Vue.use(VueRouter),這是因為讓外掛可以使用 Vue

export function initUse (Vue: GlobalAPI) {
  Vue.use = function (plugin: Function | Object) {
    // 判斷重複安裝外掛
    const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
    if (installedPlugins.indexOf(plugin) > -1) {
      return this
    }
    const args = toArray(arguments, 1)
    // 插入 Vue
    args.unshift(this)
    // 一般外掛都會有一個 install 函式
    // 通過該函式讓外掛可以使用 Vue
    if (typeof plugin.install === 'function') {
      plugin.install.apply(plugin, args)
    } else if (typeof plugin === 'function') {
      plugin.apply(null, args)
    }
    installedPlugins.push(plugin)
    return this
  }
}
複製程式碼

接下來看下 install 函式的部分實現

export function install (Vue) {
  // 確保 install 呼叫一次
  if (install.installed && _Vue === Vue) return
  install.installed = true
  // 把 Vue 賦值給全域性變數
  _Vue = Vue
  const registerInstance = (vm, callVal) => {
    let i = vm.$options._parentVnode
    if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
      i(vm, callVal)
    }
  }
  // 給每個元件的鉤子函式混入實現
  // 可以發現在 `beforeCreate` 鉤子執行時
  // 會初始化路由
  Vue.mixin({
    beforeCreate () {
      // 判斷元件是否存在 router 物件,該物件只在根元件上有
      if (isDef(this.$options.router)) {
        // 根路由設定為自己
        this._routerRoot = this
        this._router = this.$options.router
        // 初始化路由
        this._router.init(this)
        // 很重要,為 _route 屬性實現雙向繫結
        // 觸發元件渲染
        Vue.util.defineReactive(this, '_route', this._router.history.current)
      } else {
        // 用於 router-view 層級判斷
        this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
      }
      registerInstance(this, this)
    },
    destroyed () {
      registerInstance(this)
    }
  })
  // 全域性註冊元件 router-link 和 router-view
  Vue.component('RouterView', View)
  Vue.component('RouterLink', Link)
}
複製程式碼

對於路由註冊來說,核心就是呼叫 Vue.use(VueRouter),使得 VueRouter 可以使用 Vue。然後通過 Vue 來呼叫 VueRouter 的 install 函式。在該函式中,核心就是給元件混入鉤子函式和全域性註冊兩個路由元件。

VueRouter 例項化

在安裝外掛後,對 VueRouter 進行例項化。

const Home = { template: '<div>home</div>' }
const Foo = { template: '<div>foo</div>' }
const Bar = { template: '<div>bar</div>' }

// 3. Create the router
const router = new VueRouter({
  mode: 'hash',
  base: __dirname,
  routes: [
    { path: '/', component: Home }, // all paths are defined without the hash.
    { path: '/foo', component: Foo },
    { path: '/bar', component: Bar }
  ]
})
複製程式碼

來看一下 VueRouter 的建構函式

constructor(options: RouterOptions = {}) {
    // ...
    // 路由匹配物件
    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}`)
        }
    }
  }
複製程式碼

在例項化 VueRouter 的過程中,核心是建立一個路由匹配物件,並且根據 mode 來採取不同的路由方式。

建立路由匹配物件

export function createMatcher (
  routes: Array<RouteConfig>,
  router: VueRouter
): Matcher {
    // 建立路由對映表
  const { pathList, pathMap, nameMap } = createRouteMap(routes)
    
  function addRoutes (routes) {
    createRouteMap(routes, pathList, pathMap, nameMap)
  }
  // 路由匹配
  function match (
    raw: RawLocation,
    currentRoute?: Route,
    redirectedFrom?: Location
  ): Route {
    //...
  }

  return {
    match,
    addRoutes
  }
}
複製程式碼

createMatcher 函式的作用就是建立路由對映表,然後通過閉包的方式讓 addRoutesmatch 函式能夠使用路由對映表的幾個物件,最後返回一個 Matcher 物件。

接下來看 createMatcher 函式時如何建立對映表的

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 || []
  const pathMap: Dictionary<RouteRecord> = oldPathMap || Object.create(null)
  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
  }
}
// 新增路由記錄
function addRouteRecord (
  pathList: Array<string>,
  pathMap: Dictionary<RouteRecord>,
  nameMap: Dictionary<RouteRecord>,
  route: RouteConfig,
  parent?: RouteRecord,
  matchAs?: string
) {
  // 獲得路由配置下的屬性
  const { path, name } = route
  const pathToRegexpOptions: PathToRegexpOptions = route.pathToRegexpOptions || {}
  // 格式化 url,替換 / 
  const normalizedPath = normalizePath(
    path,
    parent,
    pathToRegexpOptions.strict
  )
  // 生成記錄物件
  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 屬性,新增路由記錄
    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}" }`
      )
    }
  }
}
複製程式碼

以上就是建立路由匹配物件的全過程,通過使用者配置的路由規則來建立對應的路由對映表。

路由初始化

當根元件呼叫 beforeCreate 鉤子函式時,會執行以下程式碼

beforeCreate () {
// 只有根元件有 router 屬性,所以根元件初始化時會初始化路由
  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)
}
複製程式碼

接下來看下路由初始化會做些什麼

init(app: any /* Vue component instance */) {
    // 儲存元件例項
    this.apps.push(app)
    // 如果根元件已經有了就返回
    if (this.app) {
      return
    }
    this.app = app
    // 賦值路由模式
    const history = this.history
    // 判斷路由模式,以雜湊模式為例
    if (history instanceof HTML5History) {
      history.transitionTo(history.getCurrentLocation())
    } else if (history instanceof HashHistory) {
      // 新增 hashchange 監聽
      const setupHashListener = () => {
        history.setupListeners()
      }
      // 路由跳轉
      history.transitionTo(
        history.getCurrentLocation(),
        setupHashListener,
        setupHashListener
      )
    }
    // 該回撥會在 transitionTo 中呼叫
    // 對元件的 _route 屬性進行賦值,觸發元件渲染
    history.listen(route => {
      this.apps.forEach(app => {
        app._route = route
      })
    })
  }
複製程式碼

在路由初始化時,核心就是進行路由的跳轉,改變 URL 然後渲染對應的元件。接下來來看一下路由是如何進行跳轉的。

路由跳轉

transitionTo (location: RawLocation, onComplete?: Function, onAbort?: Function) {
  // 獲取匹配的路由資訊
  const route = this.router.match(location, this.current)
  // 確認切換路由
  this.confirmTransition(route, () => {
    // 以下為切換路由成功或失敗的回撥
    // 更新路由資訊,對元件的 _route 屬性進行賦值,觸發元件渲染
    // 呼叫 afterHooks 中的鉤子函式
    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) })
    }
  })
}
複製程式碼

在路由跳轉中,需要先獲取匹配的路由資訊,所以先來看下如何獲取匹配的路由資訊

function match (
  raw: RawLocation,
  currentRoute?: Route,
  redirectedFrom?: Location
): Route {
  // 序列化 url
  // 比如對於該 url 來說 /abc?foo=bar&baz=qux#hello
  // 會序列化路徑為 /abc
  // 雜湊為 #hello
  // 引數為 foo: 'bar', baz: 'qux'
  const location = normalizeLocation(raw, currentRoute, false, router)
  const { name } = location
  // 如果是命名路由,就判斷記錄中是否有該命名路由配置
  if (name) {
    const record = nameMap[name]
    // 沒找到表示沒有匹配的路由
    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)
      }
    }
  }
  // 沒有匹配的路由
  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, router)
}

export function createRoute (
  record: ?RouteRecord,
  location: Location,
  redirectedFrom?: ?Location,
  router?: VueRouter
): Route {
  const stringifyQuery = router && router.options.stringifyQuery
  // 克隆引數
  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),
    matched: record ? formatMatch(record) : []
  }
  if (redirectedFrom) {
    route.redirectedFrom = getFullPath(redirectedFrom, stringifyQuery)
  }
  // 讓路由物件不可修改
  return Object.freeze(route)
}
// 獲得包含當前路由的所有巢狀路徑片段的路由記錄
// 包含從根路由到當前路由的匹配記錄,從上至下
function formatMatch(record: ?RouteRecord): Array<RouteRecord> {
  const res = []
  while (record) {
    res.unshift(record)
    record = record.parent
  }
  return res
}
複製程式碼

至此匹配路由已經完成,我們回到 transitionTo 函式中,接下來執行 confirmTransition

transitionTo (location: RawLocation, onComplete?: Function, onAbort?: Function) {
  // 確認切換路由
  this.confirmTransition(route, () => {}
}
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()
  }
  // 通過對比路由解析出可複用的元件,需要渲染的元件,失活的元件
  const { updated, deactivated, activated } = resolveQueue(
    this.current.matched,
    route.matched
  )
  
  function resolveQueue(
      current: Array<RouteRecord>,
      next: Array<RouteRecord>
    ): {
      updated: Array<RouteRecord>,
      activated: Array<RouteRecord>,
      deactivated: Array<RouteRecord>
    } {
      let i
      const max = Math.max(current.length, next.length)
      for (i = 0; i < max; i++) {
        // 當前路由路徑和跳轉路由路徑不同時跳出遍歷
        if (current[i] !== next[i]) {
          break
        }
      }
      return {
        // 可複用的元件對應路由
        updated: next.slice(0, i),
        // 需要渲染的元件對應路由
        activated: next.slice(i),
        // 失活的元件對應路由
        deactivated: current.slice(i)
      }
  }
  // 導航守衛陣列
  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) => {
        // 只有執行了鉤子函式中的 next,才會繼續執行下一個鉤子函式
        // 否則會暫停跳轉
        // 以下邏輯是在判斷 next() 中的傳參
        if (to === false || isError(to)) {
          // next(false) 
          this.ensureURL(true)
          abort(to)
        } else if (
          typeof to === 'string' ||
          (typeof to === 'object' &&
            (typeof to.path === 'string' || typeof to.name === 'string'))
        ) {
        // next('/') 或者 next({ path: '/' }) -> 重定向
          abort()
          if (typeof to === 'object' && to.replace) {
            this.replace(to)
          } else {
            this.push(to)
          }
        } else {
        // 這裡執行 next
        // 也就是執行下面函式 runQueue 中的 step(index + 1)
          next(to)
        }
      })
    } catch (e) {
      abort(e)
    }
  }
  // 經典的同步執行非同步函式
  runQueue(queue, iterator, () => {
    const postEnterCbs = []
    const isValid = () => this.current === route
    // 當所有非同步元件載入完成後,會執行這裡的回撥,也就是 runQueue 中的 cb()
    // 接下來執行 需要渲染元件的導航守衛鉤子
    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()
          })
        })
      }
    })
  })
}
export function runQueue (queue: Array<?NavigationGuard>, fn: Function, cb: Function) {
  const step = index => {
  // 佇列中的函式都執行完畢,就執行回撥函式
    if (index >= queue.length) {
      cb()
    } else {
      if (queue[index]) {
      // 執行迭代器,使用者在鉤子函式中執行 next() 回撥
      // 回撥中判斷傳參,沒有問題就執行 next(),也就是 fn 函式中的第二個引數
        fn(queue[index], () => {
          step(index + 1)
        })
      } else {
        step(index + 1)
      }
    }
  }
  // 取出佇列中第一個鉤子函式
  step(0)
}
複製程式碼

接下來介紹導航守衛

const queue: Array<?NavigationGuard> = [].concat(
    // 失活的元件鉤子
    extractLeaveGuards(deactivated),
    // 全域性 beforeEach 鉤子
    this.router.beforeHooks,
    // 在當前路由改變,但是該元件被複用時呼叫
    extractUpdateHooks(updated),
    // 需要渲染元件 enter 守衛鉤子
    activated.map(m => m.beforeEnter),
    // 解析非同步路由元件
    resolveAsyncComponents(activated)
)
複製程式碼

第一步是先執行失活元件的鉤子函式

function extractLeaveGuards(deactivated: Array<RouteRecord>): Array<?Function> {
// 傳入需要執行的鉤子函式名
  return extractGuards(deactivated, 'beforeRouteLeave', bindGuard, true)
}
function extractGuards(
  records: Array<RouteRecord>,
  name: string,
  bind: Function,
  reverse?: boolean
): Array<?Function> {
  const guards = flatMapComponents(records, (def, instance, match, key) => {
   // 找出元件中對應的鉤子函式
    const guard = extractGuard(def, name)
    if (guard) {
    // 給每個鉤子函式新增上下文物件為元件自身
      return Array.isArray(guard)
        ? guard.map(guard => bind(guard, instance, match, key))
        : bind(guard, instance, match, key)
    }
  })
  // 陣列降維,並且判斷是否需要翻轉陣列
  // 因為某些鉤子函式需要從子執行到父
  return flatten(reverse ? guards.reverse() : guards)
}
export function flatMapComponents (
  matched: Array<RouteRecord>,
  fn: Function
): Array<?Function> {
// 陣列降維
  return flatten(matched.map(m => {
  // 將元件中的物件傳入回撥函式中,獲得鉤子函式陣列
    return Object.keys(m.components).map(key => fn(
      m.components[key],
      m.instances[key],
      m, key
    ))
  }))
}
複製程式碼

第二步執行全域性 beforeEach 鉤子函式

beforeEach(fn: Function): Function {
    return registerHook(this.beforeHooks, fn)
}
function registerHook(list: Array<any>, fn: Function): Function {
  list.push(fn)
  return () => {
    const i = list.indexOf(fn)
    if (i > -1) list.splice(i, 1)
  }
}
複製程式碼

在 VueRouter 類中有以上程式碼,每當給 VueRouter 例項新增 beforeEach 函式時就會將函式 push 進 beforeHooks 中。

第三步執行 beforeRouteUpdate 鉤子函式,呼叫方式和第一步相同,只是傳入的函式名不同,在該函式中可以訪問到 this 物件。

第四步執行 beforeEnter 鉤子函式,該函式是路由獨享的鉤子函式。

第五步是解析非同步元件。

export function resolveAsyncComponents (matched: Array<RouteRecord>): Function {
  return (to, from, next) => {
    let hasAsync = false
    let pending = 0
    let error = null
    // 該函式作用之前已經介紹過了
    flatMapComponents(matched, (def, _, match, key) => {
    // 判斷是否是非同步元件
      if (typeof def === 'function' && def.cid === undefined) {
        hasAsync = true
        pending++
        // 成功回撥
        // once 函式確保非同步元件只載入一次
        const resolve = once(resolvedDef => {
          if (isESModule(resolvedDef)) {
            resolvedDef = resolvedDef.default
          }
          // 判斷是否是建構函式
          // 不是的話通過 Vue 來生成元件建構函式
          def.resolved = typeof resolvedDef === 'function'
            ? resolvedDef
            : _Vue.extend(resolvedDef)
        // 賦值元件
        // 如果元件全部解析完畢,繼續下一步
          match.components[key] = resolvedDef
          pending--
          if (pending <= 0) {
            next()
          }
        })
        // 失敗回撥
        const reject = once(reason => {
          const msg = `Failed to resolve async component ${key}: ${reason}`
          process.env.NODE_ENV !== 'production' && warn(false, msg)
          if (!error) {
            error = isError(reason)
              ? reason
              : new Error(msg)
            next(error)
          }
        })
        let res
        try {
        // 執行非同步元件函式
          res = def(resolve, reject)
        } catch (e) {
          reject(e)
        }
        if (res) {
        // 下載完成執行回撥
          if (typeof res.then === 'function') {
            res.then(resolve, reject)
          } else {
            const comp = res.component
            if (comp && typeof comp.then === 'function') {
              comp.then(resolve, reject)
            }
          }
        }
      }
    })
    // 不是非同步元件直接下一步
    if (!hasAsync) next()
  }
}
複製程式碼

以上就是第一個 runQueue 中的邏輯,第五步完成後會執行第一個 runQueue 中回撥函式

// 該回撥用於儲存 `beforeRouteEnter` 鉤子中的回撥函式
const postEnterCbs = []
const isValid = () => this.current === route
// beforeRouteEnter 導航守衛鉤子
const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid)
// beforeResolve 導航守衛鉤子
const queue = enterGuards.concat(this.router.resolveHooks)
runQueue(queue, iterator, () => {
  if (this.pending !== route) {
    return abort()
  }
  this.pending = null
  // 這裡會執行 afterEach 導航守衛鉤子
  onComplete(route)
  if (this.router.app) {
    this.router.app.$nextTick(() => {
      postEnterCbs.forEach(cb => {
        cb()
      })
    })
  }
})
複製程式碼

第六步是執行 beforeRouteEnter 導航守衛鉤子,beforeRouteEnter 鉤子不能訪問 this 物件,因為鉤子在導航確認前被呼叫,需要渲染的元件還沒被建立。但是該鉤子函式是唯一一個支援在回撥中獲取 this 物件的函式,回撥會在路由確認執行。

beforeRouteEnter (to, from, next) {
  next(vm => {
    // 通過 `vm` 訪問元件例項
  })
}
複製程式碼

下面來看看是如何支援在回撥中拿到 this 物件的

function extractEnterGuards(
  activated: Array<RouteRecord>,
  cbs: Array<Function>,
  isValid: () => boolean
): Array<?Function> {
// 這裡和之前呼叫導航守衛基本一致
  return extractGuards(
    activated,
    'beforeRouteEnter',
    (guard, _, match, key) => {
      return bindEnterGuard(guard, match, key, cbs, isValid)
    }
  )
}
function bindEnterGuard(
  guard: NavigationGuard,
  match: RouteRecord,
  key: string,
  cbs: Array<Function>,
  isValid: () => boolean
): NavigationGuard {
  return function routeEnterGuard(to, from, next) {
    return guard(to, from, cb => {
    // 判斷 cb 是否是函式
    // 是的話就 push 進 postEnterCbs
      next(cb)
      if (typeof cb === 'function') {
        cbs.push(() => {
          // 迴圈直到拿到元件例項
          poll(cb, match.instances, key, isValid)
        })
      }
    })
  }
}
// 該函式是為了解決 issus #750
// 當 router-view 外面包裹了 mode 為 out-in 的 transition 元件 
// 會在元件初次導航到時獲得不到元件例項物件
function poll(
  cb: any, // somehow flow cannot infer this is a function
  instances: Object,
  key: string,
  isValid: () => boolean
) {
  if (
    instances[key] &&
    !instances[key]._isBeingDestroyed // do not reuse being destroyed instance
  ) {
    cb(instances[key])
  } else if (isValid()) {
  // setTimeout 16ms 作用和 nextTick 基本相同
    setTimeout(() => {
      poll(cb, instances, key, isValid)
    }, 16)
  }
}
複製程式碼

第七步是執行 beforeResolve 導航守衛鉤子,如果註冊了全域性 beforeResolve 鉤子就會在這裡執行。

第八步就是導航確認,呼叫 afterEach 導航守衛鉤子了。

以上都執行完成後,會觸發元件的渲染

history.listen(route => {
      this.apps.forEach(app => {
        app._route = route
      })
})
複製程式碼

以上回撥會在 updateRoute 中呼叫

updateRoute(route: Route) {
    const prev = this.current
    this.current = route
    this.cb && this.cb(route)
    this.router.afterHooks.forEach(hook => {
      hook && hook(route, prev)
    })
}
複製程式碼

至此,路由跳轉已經全部分析完畢。核心就是判斷需要跳轉的路由是否存在於記錄中,然後執行各種導航守衛函式,最後完成 URL 的改變和元件的渲染。

求職

最近本人在尋找工作機會,如果有杭州的不錯崗位的話,歡迎聯絡我 zx597813039@gmail.com

公眾號

VueRouter 原始碼深度解析

最後

如果你有不清楚的地方或者認為我有寫錯的地方,歡迎評論區交流。

相關文章

相關文章