vue-router ——从Vue.use到$route.push

今天来分析一下vue-router的部分源码,本篇主要分析hash模式的push。

URL语法通用格式

直观感受

hash模式在路由改变时,只会改变#后面的这个字符串,也就是location.hash,此时页面不会真正地刷新,但页面的确发生了渲染。

槽点:身为一个hybrid app,链接丑成啥样都不要紧,但客户端和服务端都得注意要对他们的h5链接拼接函数做健壮性修复。

调试

拿一个熟悉的项目打断点调试


vue.use

使用
import Router from 'vue-router';
Vue.use(Router);
const Router=new Router({})
源码
  • Vue.use
  Vue.use = function (plugin: Function | Object) {
    const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
    if (installedPlugins.indexOf(plugin) > -1) {
      return this
    }

    // additional parameters
    const args = toArray(arguments, 1)
    args.unshift(this)
    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方法,则调用它;否则,调用这个plugin。

  • vue-router提供的install方法如下:
    粗略地看,在这个函数里使用了Vue.mixin;在Vue.prototype挂了$router$route;注册了两个组件RouterView、RouterLink
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 () {
      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)
    }
  })

  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
}

值得一提的是,使用Vue.mixin全局混入,在beforeCreate这个钩子干了十分关键的事情

this._routerRoot = this
this._router = this.$options.router
this._router.init(this)
Vue.util.defineReactive(this, '_route', this._router.history.current)
  1. 将new Vue时传入的router实例赋值给_router
  2. 调用init
 init (app: any /* Vue component instance */) {
    ……
    this.app = app

    const history = this.history

    if (history instanceof HTML5History) {
      history.transitionTo(history.getCurrentLocation())
    } else if (history instanceof HashHistory) {
      const setupHashListener = () => {
        history.setupListeners()
      }
      history.transitionTo(
        history.getCurrentLocation(),
        setupHashListener,
        setupHashListener
      )
    }

    history.listen(route => {
      this.apps.forEach((app) => {
        app._route = route
      })
    })
  }
 setupListeners () {
    const router = this.router
    const expectScroll = router.options.scrollBehavior
    const supportsScroll = supportsPushState && expectScroll

    if (supportsScroll) {
      setupScroll()
    }

    window.addEventListener(
      supportsPushState ? 'popstate' : 'hashchange',
      () => {
        const current = this.current
        if (!ensureSlash()) {
          return
        }
        this.transitionTo(getHash(), route => {
          if (supportsScroll) {
            handleScroll(this.router, route, current, true)
          }
          if (!supportsPushState) {
            replaceHash(route.fullPath)
          }
        })
      }
    )
  }

这个transitionTo入参为
transitionTo ( location: RawLocation, onComplete?: Function, onAbort?: Function )
可以看到,不管是哪个模式,都会执行transitionTo,如果判定为hash模式,onComplete结束回调为setupListeners。也就是说,添加一个popstate事件的监听,这个事件被触发之后,也会调用transitionTo

  1. Vue.util.defineReactive(this, '_route', this._router.history.current)
    _routeVue.util.defineReactive 变为响应式对象。这样,当它的值发生变更时,将触发render()函数从而触发页面的渲染动作。

transitionTo

这个无处不在的transitionTo就是处理跳转的关键函数

  • transitionTo
 transitionTo (
    location: RawLocation,
    onComplete?: Function,
    onAbort?: Function
  ) {
    const route = this.router.match(location, this.current)
    this.confirmTransition(
      route,
      () => {
        this.updateRoute(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)
          })
        }
      }
    )
  }

使用router.match得到了一个route对象如下:


route

之后调用了confirmTransition,在这个函数中,主要是走导航守卫的逻辑
然后执行传给它的onComplete

 this.updateRoute(route)
 onComplete && onComplete(route)
 this.ensureURL()
  • 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)
    })
  }

保存当前的路由this.current用以调用他的afterHooks,并设置this.current为即将跳转的route。然后调用this.cb

  • this.cb
    这个cb在History的listen函数里被设置上,也就是init函数里。
    app为init传入的Vue component instance
 history.listen(route => {
      this.apps.forEach((app) => {
        app._route = route
      })
    })

干的事情就是对当前的每一个Vue component instance都提供这个_route。之前我们提到过,如果修改了_route,由于这个对象为vue的响应式对象,render函数会被调用以触发页面渲染。

push

  • push
    调用了$router.push,就是
    $router.push->VueRouter.prototype.push->HashHistory.prototype.push

hash.js 的 push

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
    )
  }
function getUrl (path) {
  const href = window.location.href
  const i = href.indexOf('#')
  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
  }
}
export function pushState (url?: string, replace?: boolean) {
  saveScrollPosition()
  // try...catch the pushState call to get around Safari
  // DOM Exception 18 where it limits to 100 pushState calls
  const history = window.history
  try {
    if (replace) {
      // preserve existing history state as it could be overriden by the user
      const stateCopy = extend({}, history.state)
      stateCopy.key = getStateKey()
      history.replaceState(stateCopy, '', url)
    } else {
      history.pushState({ key: setStateKey(genStateKey()) }, '', url)
    }
  } catch (e) {
    window.location[replace ? 'replace' : 'assign'](url)
  }
}

可以看到push给this.transitionTo传入的结束回调里,pushHash会先调用getUrl更新#后面的字符串,得到新的链接,然后再传给pushState。最终会调用history.pushState来修改历史栈以保证浏览器前进返回行为像真正的跳转那样表达。

帮助理解的流程图

——in 在梅雨里的七月中旬

你可能感兴趣的:(vue-router ——从Vue.use到$route.push)