首頁 > web前端 > Vue.js > 主體

一文聊聊Vue-Router的實作原理

青灯夜游
發布: 2022-12-01 20:24:05
轉載
3441 人瀏覽過

一文聊聊Vue-Router的實作原理

路由的概念相信大部分同學並不陌生,我們在用 Vue 開發實際專案的時候都會用到 Vue-Router 這個官方外掛程式來幫我們解決路線的問題。它的作用就是根據不同的路徑映射到不同的視圖。本文不再講述路由的基礎使用和API,不清楚的同學可以自行查閱官方文件vue-router3 對應 vue2 和 vue-router4 對應 vue3

今天我們主要是談談Vue-Router的實作原理,有興趣的夥伴可以繼續往下看,大佬請停下來。

本文vue-router 版本為3.5.3

#路由

既然我們在分析路由,我們先來說說什麼是路由,什麼是後端路由、什麼是前端路由。

路由就是根據不同的url 位址展示不同的內容或頁面,早期路由的概念是在後端出現的,透過伺服器端渲染後回傳頁面,隨著頁面越來越複雜,伺服器端壓力越來越大。後來ajax非同步刷新的出現使得前端也可以對url進行管理,此時,前端路由就出現了。 【學習影片分享:vue影片教學web前端影片

#我們先說後端路由

##。後端路由

後端路由又可稱為伺服器端路由,因為對於伺服器來說,當接收到客戶端發送的

HTTP請求,就會根據所請求的URL,來找到對應的映射函數,然後執行該函數,並將函數的回傳值傳送給客戶端。

對於最簡單的靜態資源伺服器,可以認為,所有

URL的映射函數就是一個檔案讀取操作。對於動態資源,映射函數可能是一個資料庫讀取操作,也可能是進行一些資料的處理,等等。

然後根據這些讀取的數據,在伺服器端就使用相應的模板來對頁面進行渲染後,再返回渲染完畢的

HTML頁面。早期的jsp就是這種模式。

前端路由

剛剛也介紹了,在前後端沒有分開的時候,服務端都是直接將整個

HTML 返回,用戶每次很小的操作都會造成頁面的整個刷新(再加上之前的網路速度還很慢,所以使用者體驗可想而知)。

在90年代末的時候,微軟首先實現了

ajax(Asynchronous JavaScript And XML) 這個技術,這樣用戶每次的操作就可以不用刷新整個頁面了,用戶體驗就大大提升了。

雖然資料能非同步取得不用每個點擊都去請求整個網頁,但是頁面之間的跳轉還是會載入整個網頁,體驗不是特別好,還有沒有更好的方法呢?

至此非同步互動體驗的更進階版本

SPA單頁應用程式 就出現了。單頁應用程式不只是在頁面互動是無刷新的,連頁面跳轉都是無刷新的。既然頁面的跳轉是無刷新的,也就是不再向後端請求回傳 HTML頁。

頁面跳轉都不從後端取得新的

HTML頁面,那該怎麼做呢?所以就有了現在的前端路由。

可以理解為,前端路由就是將先前服務端根據 url 的不同返回不同的頁面的任務交給前端來做。在這個過程中,js會即時偵測url的變化,從而改變顯示的內容。

前端路由優點是使用者體驗好,使用者操作或頁面跳轉不會刷新頁面,並且能快速展現給使用者。缺點是首屏載入慢,因為需要

js動態渲染展示內容。而且由於內容是js動態渲染的所以不利於SEO

下面我們正式進入

Vue-Router原理分析階段。

分析Vue-Router.install方法

我們先來看看

install.js,這個方法會在Vue.use(VueRouter)的時候被調用。

// install.js

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

export let _Vue

export function install (Vue) {
  // 不会重复安装
  if (install.installed && _Vue === Vue) return
  install.installed = true

  _Vue = Vue

  const isDef = v => v !== undefined

  // 为router-view组件关联路由组件
  const registerInstance = (vm, callVal) => {
    let i = vm.$options._parentVnode
    // 调用vm.$options._parentVnode.data.registerRouteInstance方法
    // 而这个方法只在router-view组件中存在,router-view组件定义在(../components/view.js @71行)
    // 所以,如果vm的父节点为router-view,则为router-view关联当前vm,即将当前vm做为router-view的路由组件
    if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
      i(vm, callVal)
    }
  }

  Vue.mixin({
    beforeCreate () {
      // 这里只会进来一次,因为只有Vue根实例才会有router属性。
      if (isDef(this.$options.router)) {
        // 所以这里的this就是Vue根实例
        this._routerRoot = this
        this._router = this.$options.router
        this._router.init(this)
        // 将 _route 变成响应式
        Vue.util.defineReactive(this, '_route', this._router.history.current)
      } else {
        // 子组件会进入这里,这里也是把Vue根实例保存带_routerRoot属性上
        this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
      }
      // 为router-view组件关联路由组件
      registerInstance(this, this)
    },
    destroyed () {
      // destroyed hook触发时,取消router-view和路由组件的关联
      registerInstance(this)
    }
  })

  // 在原型上注入$router、$route属性,方便快捷访问
  Object.defineProperty(Vue.prototype, '$router', {
    // 上面说到每个组件的_routerRoot都是Vue根实例,所以都能访问_router
    get () { return this._routerRoot._router }
  })

  // 每个组件访问到的$route,其实最后访问的都是Vue根实例的_route
  Object.defineProperty(Vue.prototype, '$route', {
    get () { return this._routerRoot._route }
  })

  // 注册router-view、router-link两个全局组件
  Vue.component('RouterView', View)
  Vue.component('RouterLink', Link)

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

主要做了以下幾件事:

避免重复安装

为了确保 install 逻辑只执行一次,用了 install.installed 变量做已安装的标志位。

传递Vue引用减少打包体积

用一个全局的 _Vue 来接收参数 Vue,因为作为 Vue 的插件对 Vue 对象是有依赖的,但又不能去单独去 import Vue,因为那样会增加包体积,所以就通过这种方式拿到 Vue 对象。

注册全局混入

Vue-Router 安装最重要的一步就是利用 Vue.mixin,在beforeCreatedestroyed生命周期函数中注入路由逻辑。

Vue.mixin我们知道就是全局 mixin,所以也就相当于每个组件的beforeCreatedestroyed生命周期函数中都会有这些代码,并在每个组件中都会运行。

Vue.mixin({
  beforeCreate () {
    if (isDef(this.$options.router)) {
      this._routerRoot = this
      this._router = this.$options.router
      this._router.init(this)
      Vue.util.defineReactive(this, '_route', this._router.history.current)
    } else {
      this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
    }
    registerInstance(this, this)
  },
  destroyed () {
    registerInstance(this)
  }
})
登入後複製

在这两个钩子中,this是指向当时正在调用钩子的vue实例

这两个钩子中的逻辑,在安装流程中是不会被执行的,只有在组件实例化时执行到钩子时才会被调用

先看混入的 beforeCreate 钩子函数

它先判断了this.$options.router是否存在,我们在new Vue({router})时,router才会被保存到到Vue根实例$options上,而其它Vue实例$options上是没有router的,所以if中的语句只在this === new Vue({router})时,才会被执行,由于Vue根实例只有一个,所以这个逻辑只会被执行一次。

对于根 Vue 实例而言,执行该钩子函数时定义了 this._routerRoot 表示它自身(Vue根实例);this._router 表示 VueRouter 的实例 router,它是在 new Vue 的时候传入的;

另外执行了 this._router.init() 方法初始化 router,这个逻辑在后面讲初始化的时候再介绍。

然后用 defineReactive 方法把 this._route 变成响应式对象,保证_route变化时,router-view会重新渲染,这个我们后面在router-view组件中会细讲。

我们再看下else中具体干了啥

主要是为每个组件定义_routerRoot,对于子组件而言,由于组件是树状结构,在遍历组件树的过程中,它们在执行该钩子函数的时候 this._routerRoot 始终指向的离它最近的传入了 router 对象作为配置而实例化的父实例(也就是永远等于根实例)。

所以我们可以得到,在每个vue组件都有 this._routerRoot === vue根实例this._routerRoot._router === router对象

对于 beforeCreatedestroyed 钩子函数,它们都会执行 registerInstance 方法,这个方法的作用我们也是之后会介绍。

添加$route、$router属性

接着给 Vue 原型上定义了 $router$route 2 个属性的 get 方法,这就是为什么我们可以在任何组件实例上都可以访问 this.$router 以及 this.$route

Object.defineProperty(Vue.prototype, '$router', {
get () { return this._routerRoot._router }
})

Object.defineProperty(Vue.prototype, '$route', {
get () { return this._routerRoot._route }
})
登入後複製

我们可以看到,$router其实返回的是this._routerRoot._router,也就是vue根实例上的router,因此我们可以通过this.$router来使用router的各种方法。

$route其实返回的是this._routerRoot._route,其实就是this._router.history.current,也就是目前的路由对象,这个后面会细说。

注册全局组件

通过 Vue.component 方法定义了全局的 <router-link><router-view> 2 个组件,这也是为什么我们在写模板的时候可以直接使用这两个标签,它们的作用我想就不用笔者再说了吧。

钩子函数的合并策略

最后设置路由组件的beforeRouteEnterbeforeRouteLeavebeforeRouteUpdate守卫的合并策略。

总结

那么到此为止,我们分析了 Vue-Router 的安装过程,Vue 编写插件的时候通常要提供静态的 install 方法,我们通过 Vue.use(plugin) 时候,就是在执行 install 方法。Vue-Routerinstall 方法会给每一个组件注入 beforeCreatedestoryed 钩子函数,在beforeCreate 做一些私有属性定义和路由初始化工作。并注册了两个全局组件,然后设置了钩子函数合并策略。在destoryed 做了一些销毁工作。

下面我们再来看看Vue-Router的实例化。

分析init方法

前面我们提到了在 install 的时候会执行 VueRouterinit 方法( this._router.init(this) ),那么接下来我们就来看一下 init 方法做了什么。

init (app: any /* Vue component instance */) {
  // ...

  this.apps.push(app)

  // ...

  // main app previously initialized
  // return as we don&#39;t need to set up new history listener
  if (this.app) {
    return
  }

  this.app = app

  const history = this.history
  
  if (history instanceof HTML5History || history instanceof HashHistory) {
    const handleInitialScroll = routeOrError => {
      const from = history.current
      const expectScroll = this.options.scrollBehavior
      const supportsScroll = supportsPushState && expectScroll

      if (supportsScroll && &#39;fullPath&#39; in routeOrError) {
        handleScroll(this, routeOrError, from, false)
      }
    }
    
    // 1.setupListeners 里会对 hashchange或popstate事件进行监听
    const setupListeners = routeOrError => {
      history.setupListeners()
      handleInitialScroll(routeOrError)
    }
    // 2.初始化导航
    history.transitionTo(
      history.getCurrentLocation(),
      setupListeners,
      setupListeners
    )
  }

  // 3.路由全局监听,维护当前的route 
  // 当路由变化的时候修改app._route的值
  // 由于_route是响应式的,所以修改后相应视图会同步更新
  history.listen(route => {
    this.apps.forEach(app => {
      app._route = route
    })
  })
}
登入後複製

这里主要做了如下几件事情:

设置了路由监听

const setupListeners = routeOrError => {
  history.setupListeners()
  handleInitialScroll(routeOrError)
}
登入後複製

这里会根据当前路由模式监听hashchangepopstate事件,当事件触发的时候,会进行路由的跳转。(后面说到路由模式的时候会细说)

初始化导航

history.transitionTo(
  history.getCurrentLocation(),
  setupListeners,
  setupListeners
)
登入後複製

进入系统会进行初始化路由匹配,渲染对应的组件。因为第一次进入系统,并不会触发hashchange或者popstate事件,所以第一次需要自己手动匹配路径然后进行跳转。

路由全局监听

history.listen(route => {
  this.apps.forEach(app => {
    app._route = route
  })
})
登入後複製

当路由变化的时候修改app._route的值。由于_route是响应式的,所以修改后相应视图会同步更新。

总结

这里主要是做了一些初始化工作。根据当前路由模式监听对应的路由事件。初始化导航,根据当前的url渲染初始页面。最后切换路由的时候修改_route,由于_route是响应式的,所以修改后相应视图会同步更新。

分析VueRouter实例化

实例化就是我们new VueRouter({routes})的过程,我们来重点分析下VueRouter的构造函数。

constructor (options: RouterOptions = {}) {
  // ...
  
  // 参数初始化
  this.app = null
  this.apps = []
  this.options = options
  this.beforeHooks = []
  this.resolveHooks = []
  this.afterHooks = []
  // 创建matcher
  this.matcher = createMatcher(options.routes || [], this)

  // 设置默认模式和做不支持 H5 history 的降级处理
  let mode = options.mode || &#39;hash&#39;
  this.fallback =
    mode === &#39;history&#39; && !supportsPushState && options.fallback !== false
  if (this.fallback) {
    mode = &#39;hash&#39;
  }
  if (!inBrowser) {
    mode = &#39;abstract&#39;
  }
  this.mode = mode

  // 根据不同的 mode 实例化不同的 History 对象
  switch (mode) {
    case &#39;history&#39;:
      this.history = new HTML5History(this, options.base)
      break
    case &#39;hash&#39;:
      this.history = new HashHistory(this, options.base, this.fallback)
      break
    case &#39;abstract&#39;:
      this.history = new AbstractHistory(this, options.base)
      break
    default:
      if (process.env.NODE_ENV !== &#39;production&#39;) {
        assert(false, `invalid mode: ${mode}`)
      }
  }
}
登入後複製

这里主要做了如下几件事情:

初始化参数

我们看到在最开始有些参数的初始化,这些参数到底是什么呢?

this.app 用来保存根 Vue 实例。

this.apps 用来保存持有 $options.router 属性的 Vue 实例。

this.options 保存传入的路由配置,也就是前面说的RouterOptions

this.beforeHooksthis.resolveHooksthis.afterHooks 表示一些钩子函数。

this.fallback 表示在浏览器不支持 historyapi的情况下,根据传入的 fallback 配置参数,决定是否回退到hash模式。

this.mode 表示路由创建的模式。

创建matcher

matcher,匹配器。简单理解就是可以通过url找到我们对应的组件。这一块内容较多,这里笔者就不再详细分析了。

确定路由模式

路由模式平时都会只说两种,其实在vue-router总共实现了 hashhistoryabstract 3 种模式。

VueRouter会根据options.modeoptions.fallbacksupportsPushStateinBrowser来确定最终的路由模式。

如果没有设置mode就默认是hash模式。

确定fallback值,只有在用户设置了mode:history并且当前环境不支持pushState且用户没有主动声明不需要回退(没设置fallback值位undefined),此时this.fallback才为true,当fallbacktrue时会使用hash模式。(简单理解就是如果不支持history模式并且只要没设置fallbackfalse,就会启用hash模式)

如果最后发现处于非浏览器环境,则会强制使用abstract模式。

实例化路由模式

根据mode属性值来实例化不同的对象。VueRouter的三种路由模式,主要由下面的四个核心类实现

  • History

    • 基础类
    • 位于src/history/base.js
  • HTML5History

    • 用于支持pushState的浏览器
    • src/history/html5.js
  • HashHistory

    • 用于不支持pushState的浏览器
    • src/history/hash.js
  • AbstractHistory

    • 用于非浏览器环境(服务端渲染)
    • src/history/abstract.js

HTML5HistoryHashHistoryAbstractHistory三者都是继承于基础类History

这里我们详细分析下HTML5HistoryHashHistory类。

HTML5History类

当我们使用history模式的时候会实例化HTML5History类

// src/history/html5.js

...

export class HTML5History extends History {
  _startLocation: string

  constructor (router: Router, base: ?string) {
    // 调用父类构造函数初始化
    super(router, base)

    this._startLocation = getLocation(this.base)
  }

  // 设置监听,主要是监听popstate方法来自动触发transitionTo
  setupListeners () {
    if (this.listeners.length > 0) {
      return
    }

    const router = this.router
    const expectScroll = router.options.scrollBehavior
    const supportsScroll = supportsPushState && expectScroll
    
    // 若支持scroll,初始化scroll相关逻辑
    if (supportsScroll) {
      this.listeners.push(setupScroll())
    }

    const handleRoutingEvent = () => {
      const current = this.current

      // 某些浏览器,会在打开页面时触发一次popstate 
      // 此时如果初始路由是异步路由,就会出现`popstate`先触发,初始路由后解析完成,进而导致route未更新 
      // 所以需要避免
      const location = getLocation(this.base)
      if (this.current === START && location === this._startLocation) {
        return
      }
      
      // 路由地址发生变化,则跳转,如需滚动则在跳转后处理滚动
      this.transitionTo(location, route => {
        if (supportsScroll) {
          handleScroll(router, route, current, true)
        }
      })
    }
    
    // 监听popstate事件
    window.addEventListener(&#39;popstate&#39;, handleRoutingEvent)
    this.listeners.push(() => {
      window.removeEventListener(&#39;popstate&#39;, handleRoutingEvent)
    })
  }

  // 可以看到 history模式go方法其实是调用的window.history.go(n)
  go (n: number) {
    window.history.go(n)
  }

  // push方法会主动调用transitionTo进行跳转
  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方法会主动调用transitionTo进行跳转
  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)
  }

  ensureURL (push?: boolean) {
    if (getLocation(this.base) !== this.current.fullPath) {
      const current = cleanPath(this.base + this.current.fullPath)
      push ? pushState(current) : replaceState(current)
    }
  }

  getCurrentLocation (): string {
    return getLocation(this.base)
  }
}

export function getLocation (base: string): string {
  let path = window.location.pathname
  const pathLowerCase = path.toLowerCase()
  const baseLowerCase = base.toLowerCase()
  // base="/a" shouldn&#39;t turn path="/app" into "/a/pp"
  // https://github.com/vuejs/vue-router/issues/3555
  // so we ensure the trailing slash in the base
  if (base && ((pathLowerCase === baseLowerCase) ||
    (pathLowerCase.indexOf(cleanPath(baseLowerCase + &#39;/&#39;)) === 0))) {
    path = path.slice(base.length)
  }
  return (path || &#39;/&#39;) + window.location.search + window.location.hash
}
登入後複製

可以看到HTML5History类主要干了如下几件事。

  • 继承于History类,并调用父类构造函数初始化。

  • 实现了setupListeners方法,在该方法中检查了是否需要支持滚动行为,如果支持,则初始化滚动相关逻辑,监听了popstate事件,并在popstate触发时自动调用transitionTo方法。

  • 实现了go、push、replace等方法,我们可以看到,history模式其实就是使用的history api

// 可以看到 history模式go方法其实是调用的window.history.go(n)
go (n: number) {
  window.history.go(n)
}

// push、replace调用的是util/push-state.js,里面实现了push和replace方法
// 实现原理也是使用的history api,并且在不支持history api的情况下使用location api

export function pushState (url?: string, replace?: boolean) {
  ...
  const history = window.history
  try {
    if (replace) {
      const stateCopy = extend({}, history.state)
      stateCopy.key = getStateKey()
      // 调用的 history.replaceState
      history.replaceState(stateCopy, &#39;&#39;, url)
    } else {
      // 调用的 history.pushState
      history.pushState({ key: setStateKey(genStateKey()) }, &#39;&#39;, url)
    }
  } catch (e) {
    window.location[replace ? &#39;replace&#39; : &#39;assign&#39;](url)
  }
}

export function replaceState (url?: string) {
  pushState(url, true)
}
登入後複製

总结

所以history模式的原理就是在js中路由的跳转(也就是使用pushreplace方法)都是通过history apihistory.pushStatehistory.replaceState两个方法完成,通过这两个方法我们知道了路由的变化,然后根据路由映射关系来实现页面内容的更新。

对于直接点击浏览器的前进后退按钮或者js调用 this.$router.go()this.$router.forward()this.$router.back()、或者原生js方法history.back()history.go()history.forward()的,都会触发popstate事件,通过监听这个事件我们就可以知道路由发生了哪些变化然后来实现更新页面内容。

注意history.pushStatehistory.replaceState这两个方法并不会触发popstate事件。在这两个方法里面他是有手动调用transitionTo方法的。

接下来我们再来看看HashHistory类

HashHistory类

当我们使用hash模式的时候会实例化HashHistory类

//src/history/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 () {
    if (this.listeners.length > 0) {
      return
    }

    const router = this.router
    const expectScroll = router.options.scrollBehavior
    const supportsScroll = supportsPushState && expectScroll

    if (supportsScroll) {
      this.listeners.push(setupScroll())
    }

    const handleRoutingEvent = () => {
      const current = this.current
      if (!ensureSlash()) {
        return
      }
      this.transitionTo(getHash(), route => {
        if (supportsScroll) {
          handleScroll(this.router, route, current, true)
        }
        if (!supportsPushState) {
          replaceHash(route.fullPath)
        }
      })
    }
    // 事件优先使用 popstate
    // 判断supportsPushState就是通过return window.history && typeof window.history.pushState === &#39;function&#39;
    const eventType = supportsPushState ? &#39;popstate&#39; : &#39;hashchange&#39;
    window.addEventListener(
      eventType,
      handleRoutingEvent
    )
    this.listeners.push(() => {
      window.removeEventListener(eventType, handleRoutingEvent)
    })
  }
  
  // 其实也是优先使用history的pushState方法来实现,不支持再使用location修改hash值
  push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    const { current: fromRoute } = this
    this.transitionTo(
      location,
      route => {
        pushHash(route.fullPath)
        handleScroll(this.router, route, fromRoute, false)
        onComplete && onComplete(route)
      },
      onAbort
    )
  }

  // 其实也是优先使用history的replaceState方法来实现,不支持再使用location修改replace方法
  replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    const { current: fromRoute } = this
    this.transitionTo(
      location,
      route => {
        replaceHash(route.fullPath)
        handleScroll(this.router, route, fromRoute, false)
        onComplete && onComplete(route)
      },
      onAbort
    )
  }

  // 也是使用的history go方法
  go (n: number) {
    window.history.go(n)
  }

  ensureURL (push?: boolean) {
    const current = this.current.fullPath
    if (getHash() !== current) {
      push ? pushHash(current) : replaceHash(current)
    }
  }

  getCurrentLocation () {
    return getHash()
  }
}

function checkFallback (base) {
  const location = getLocation(base)
  if (!/^\/#/.test(location)) {
    window.location.replace(cleanPath(base + &#39;/#&#39; + location))
    return true
  }
}

function ensureSlash (): boolean {
  const path = getHash()
  if (path.charAt(0) === &#39;/&#39;) {
    return true
  }
  replaceHash(&#39;/&#39; + path)
  return false
}

// 获取 # 后面的内容
export function getHash (): string {
  // We can&#39;t use window.location.hash here because it&#39;s not
  // consistent across browsers - Firefox will pre-decode it!
  let href = window.location.href
  const index = href.indexOf(&#39;#&#39;)
  // empty path
  if (index < 0) return &#39;&#39;

  href = href.slice(index + 1)

  return href
}

function getUrl (path) {
  const href = window.location.href
  const i = href.indexOf(&#39;#&#39;)
  const base = i >= 0 ? href.slice(0, i) : href
  return `${base}#${path}`
}

function pushHash (path) {
  if (supportsPushState) {
    pushState(getUrl(path))
  } else {
    window.location.hash = path
  }
}

function replaceHash (path) {
  if (supportsPushState) {
    replaceState(getUrl(path))
  } else {
    window.location.replace(getUrl(path))
  }
}
登入後複製

可以看到HashHistory类主要干了如下几件事。

  • 继承于History类,并调用父类构造函数初始化。这里比HTML5History多了回退操作,所以,需要将history模式的url替换成hash模式,即添加上#,这个逻辑是由checkFallback实现的

  • 实现了setupListeners方法,在该方法中检查了是否需要支持滚动行为,如果支持,则初始化滚动相关逻辑。 监听了popstate事件或hashchange事件,并在相应事件触发时,调用transitionTo方法实现跳转。

通过const eventType = supportsPushState ? &#39;popstate&#39; : &#39;hashchange&#39;我们可以发现就算是hash模式优先使用的还是popstate事件。

  • 实现了go、push、replace等方法。

我们可以看到,hash模式实现的push、replace方法其实也是优先使用history里面的方法,也就是history api

// 可以看到 hash 模式go方法其实是调用的window.history.go(n)
go (n: number) {
  window.history.go(n)
}

// 在支持新的history api情况下优先使用history.pushState实现
// 否则使用location api
function pushHash (path) {
  if (supportsPushState) {
    pushState(getUrl(path))
  } else {
    window.location.hash = path
  }
}

// 在支持新的history api情况下优先使用history.replaceState实现
// 否则使用location api
function replaceHash (path) {
  if (supportsPushState) {
    replaceState(getUrl(path))
  } else {
    window.location.replace(getUrl(path))
  }
}
登入後複製

总结

在浏览器链接里面我们改变hash值是不会重新向后台发送请求的,也就不会刷新页面。并且每次 hash 值的变化,还会触发hashchange 这个事件。

所以hash模式的原理就是通过监听hashchange事件,通过这个事件我们就可以知道 hash 值发生了哪些变化然后根据路由映射关系来实现页面内容的更新。(这里hash值的变化不管是通过js修改的还是直接点击浏览器的前进后退按钮都会触发hashchange事件)

对于hash模式,如果是在浏览器支持history api情况下,hash模式的实现其实是和history模式一样的。只有在不支持history api情况下才会监听hashchange事件。这个我们可以在源码中看出来。

一文聊聊Vue-Router的實作原理

总结

总的来说就是使用 Vue.util.defineReactive 将实例的 _route 设置为响应式对象。在push, replace方法里会主动更新属性 _route。而 go,back,forward,或者通过点击浏览器前进后退的按钮则会在 hashchange 或者 popstate 的回调中更新 _route_route 的更新会触发 RoterView 的重新渲染。

对于第一次进入系统,并不会触发hashchange或者popstate事件,所以第一次需要自己手动匹配路径然后通过transitionTo方法进行跳转,然后渲染对应的视图。

(学习视频分享:web前端开发编程基础视频

以上是一文聊聊Vue-Router的實作原理的詳細內容。更多資訊請關注PHP中文網其他相關文章!

相關標籤:
來源:juejin.cn
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板