Vue Router location 监听机制
Vue Router location 监听机制
Section titled “Vue Router location 监听机制”vue-router@3.6.5
- Hash 模式:supportsPushState ? 'popstate' : 'hashchange'
- History 模式:'popstate'
Vue Router 会根据不同的路由模式选择对应的监听方式,具体是由 new VueRouter({ mode: 'hash | history' }) 中的 mode 来决定。
export class HashHistory extends History {  // ...
  setupListeners () {    // ...
    // 对于 hash mode,会查看当前环境是否支持 supportsPushState,来决定监听什么事件    // 也就是说并不是简单的 hash mode 就用 haschange event    const eventType = supportsPushState ? 'popstate' : 'hashchange'    window.addEventListener(      eventType,      handleRoutingEvent    )    // ...  }
  // ...}
// src/history/html5.jsexport class HTML5History extends History {  // ...
  setupListeners () {    // ...
    // 对于 history mode,直接监听 popstate    window.addEventListener('popstate', handleRoutingEvent)    this.listeners.push(() => {      window.removeEventListener('popstate', handleRoutingEvent)    })  }
  // ...}
// src/util/push-state.jsexport const supportsPushState = inBrowser && (function () {  const ua = window.navigator.userAgent
  // 检查是否需要降级处理  if (    (ua.indexOf('Android 2.') !== -1 || ua.indexOf('Android 4.0') !== -1) &&    ua.indexOf('Mobile Safari') !== -1 &&    ua.indexOf('Chrome') === -1 &&    ua.indexOf('Windows Phone') === -1  ) {    return false  }
  return window.history && typeof window.history.pushState === 'function'})()// hashchange 事件history.pushState(null, '', '#foo')   // 不触发window.location.hash = 'foo'          // 触发
// popstate 事件history.pushState(null, '', 'foo')    // 不触发history.back()                        // 触发history.forward()                     // 触发history.go(2)                         // 触发HashHistory 和 HTML5History 都继承 History(src/history/base.js)
这俩不存在先后顺序const router = new VueRouter(options) -->Vue.use(VueRouter) --> install(Vue)
然后存在顺序vue instance beforeCreate --> router.init --> history.transitionTo --> setupListeners --> history.setupListeners()  -->  window.addEventListener(eventType, handleRoutingEvent)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
  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 () {      // 一直向上找到 new Vue({router}) 的这个 vue instance, 一般情况下是根组件(this._routerRoot)      if (isDef(this.$options.router)) {        this._routerRoot = this        // 这里的 this 是 current vue instance        // const router = new VueRouter(); Vue.use(VueRouter); new Vue({ router })        this._router = this.$options.router        // 这里会调用 router.init        this._router.init(this)        // 这里会让 _route 响应式, 其实我们访问的 $route 对应的就是这里的 _route        // 我猜测在这里不用 Vue.set(xxx) 是因为 set 中有个判断 if (target._isVue || (ob && ob.vmCount)) 的判断不允许往 vue instance 动态设置响应式数据        Vue.util.defineReactive(this, '_route', this._router.history.current)      } else {        this._routerRoot = (this.$parent && this.$parent._routerRoot) || this      }      registerInstance(this, this)    },    destroyed () {      registerInstance(this)    }  })
  Object.defineProperty(Vue.prototype, '$router', {    get () { return this._routerRoot._router }  })
  Object.defineProperty(Vue.prototype, '$route', {    get () { return this._routerRoot._route }  })
  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}
// src/router.jsexport default class VueRouter {  // ...  constructor (options: RouterOptions = {}) {    // ...
    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}`)        }    }  }
  init (app: any /* Vue component instance */) {    // ...
    const history = this.history
    // 初始就会先 transitionTo (location: RawLocation, onComplete?: Function, onAbort?: Function), 也就是说不管是  onComplete 或者 onAbort 都会调用 setupListeners --> history.setupListeners()    if (history instanceof HTML5History || history instanceof HashHistory) {      // ...      const setupListeners = routeOrError => {        history.setupListeners()        handleInitialScroll(routeOrError)      }      history.transitionTo(        history.getCurrentLocation(),        setupListeners,        setupListeners      )    }    // ...}
// We cannot remove this as it would be a breaking changeVueRouter.install = installVueRouter.version = '__VERSION__'VueRouter.isNavigationFailure = isNavigationFailureVueRouter.NavigationFailureType = NavigationFailureTypeVueRouter.START_LOCATION = START
if (inBrowser && window.Vue) {  window.Vue.use(VueRouter)}export class History {  // ...
  constructor (router: Router, base: ?string) {    this.router = router
    // ...  }
  // ...
  transitionTo (    location: RawLocation,    onComplete?: Function,    onAbort?: Function  ) {    let route    // catch redirect option https://github.com/vuejs/vue-router/issues/3201    try {      route = this.router.match(location, this.current)    } catch (e) {      this.errorCbs.forEach(cb => {        cb(e)      })      // Exception should still be thrown      throw e    }    const prev = this.current    this.confirmTransition(      route,      () => {        this.updateRoute(route)        onComplete && onComplete(route)        // ...      },      err => {        if (onAbort) {          onAbort(err)        }        // ...      }    )  }
  confirmTransition (route: Route, onComplete: Function, onAbort?: Function) {    // ...  }
}
// src/history/hash.jsexport class HashHistory extends History {  constructor (router: Router, base: ?string, fallback: boolean) {    super(router, base)
    // ...  }
  // setupListeners 在 vue app mount 之后才会调用,这样可以避免 hashchange 监听器被过早触发  // this is delayed until the app mounts  // to avoid the hashchange listener being fired too early  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)        }      })    }
    // 对于 hash mode,会查看当前环境是否支持 supportsPushState,来决定监听什么事件    // 也就是说并不是简单的 hash mode 就用 haschange event    const eventType = supportsPushState ? 'popstate' : 'hashchange'    window.addEventListener(      eventType,      handleRoutingEvent    )    this.listeners.push(() => {      window.removeEventListener(eventType, handleRoutingEvent)    })  }
  // ...}
// src/history/html5.jsexport class HTML5History extends History {  // ...
  constructor (router: Router, base: ?string) {    super(router, base)    // ...  }
  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
      // Avoiding first `popstate` event dispatched in some browsers but first      // history route not updated since async guard at the same time.      const location = getLocation(this.base)      if (this.current === START && location === this._startLocation) {        return      }
      this.transitionTo(location, route => {        if (supportsScroll) {          handleScroll(router, route, current, true)        }      })    }
    // 对于 history mode,直接监听 popstate    window.addEventListener('popstate', handleRoutingEvent)    this.listeners.push(() => {      window.removeEventListener('popstate', handleRoutingEvent)    })  }
  // ...}
// src/util/push-state.jsexport const supportsPushState = inBrowser && (function () {  const ua = window.navigator.userAgent
  // 检查是否需要降级处理  if (    (ua.indexOf('Android 2.') !== -1 || ua.indexOf('Android 4.0') !== -1) &&    ua.indexOf('Mobile Safari') !== -1 &&    ua.indexOf('Chrome') === -1 &&    ua.indexOf('Windows Phone') === -1  ) {    return false  }
  return window.history && typeof window.history.pushState === 'function'})()