[VUE]带你了解vue-router机制

前言

作为vue全家桶的一员,vue-router一直是广大前端开发者平常离不开的工具库之一。今天我们就来一起来了解一下vue-rouer。

快速说明

Vue Router 是 Vue.js (opens new window)官方的路由管理器。

官方地址:router.vuejs.org/zh/

使用

import Vue from 'vue'
// 引入vue-router
import VueRouter from 'vue-router'

// 通过use激活使用
Vue.use(VueRouter)

const router = new VueRouter({
  routes:[
  	//...路由配置
  ]
});

// 在实例化vue的时候把router传入。
const app = new Vue({
  router
}).$mount('#app')
复制代码

Vue.use(VueRouter)

再来看看我们在use的时候,vue-router做了什么。因为vue.use会调用传入对象的install方法。我们就从install入手。

export function install (Vue) {
  // 确保只会install一次
  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)
    }
  }
	// 全局mixin
  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)
    }
  })
	// 数据劫持 $router和$route
  Object.defineProperty(Vue.prototype, '$router', {
    get () { return this._routerRoot._router }
  })
  Object.defineProperty(Vue.prototype, '$route', {
    get () { return this._routerRoot._route }
  })
	// 注册组件RouterView和RouterLink
  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.use(VueRouter)的时候,主要做了以下几件事:

  1. 全局mixin,页面每次渲染时,会把组件赋值给组件的this._routerRoot。在组件销毁时,注销注册。
  2. 数据劫持Vue.prototype. r o u t e r V u e . p r o t o t y p e . router和Vue.prototype. route
  3. 注册了RouterView和RouterLink。

VueRouter类

前面提到,我们在用vue-router的时候需要先new一个VueRoute实例,然后传入app中。那这个VueRoute类是怎样的呢?

export default class VueRouter {
  // ...
  constructor (options) {
    // 存储路由信息
    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'
    this.fallback =
      mode === 'history' && !supportsPushState && options.fallback !== false
    if (this.fallback) {
      mode = 'hash'
    }
    if (!inBrowser) {
      mode = 'abstract'
    }
    this.mode = 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}`)
        }
    }
  }
  // ...
}
复制代码

vue-router如何监听路由变化

相信大家都知道vue-router支持2种mode,hash和history。而他们是怎么监听的呢?

首先,vue-router的实例中会记录这很多信息,其中包括我们传入的路由配置。

export default class VueRouter {
// ....
  constructor (options) {
    this.app = null
    this.apps = []
    this.options = options
    this.beforeHooks = []
    this.resolveHooks = []
    this.afterHooks = []
    this.matcher = createMatcher(options.routes || [], this)
    // ...
    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}`)
        }
    }
  }
// ....
}
复制代码

hash

先来看看hash的处理,

export class HashHistory extends History {
  //...
  // 当vue app挂载时会触发
  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
      // 确保hash的路径正确
      if (!ensureSlash()) {
        return
      }
      // 路由判断并跳转
      this.transitionTo(getHash(), route => {
        if (supportsScroll) {
          handleScroll(this.router, route, current, true)
        }
        if (!supportsPushState) {
          replaceHash(route.fullPath)
        }
      })
    }
    // 如果浏览器支持popstate就监听popstate事件,如果不支持就监听hashchange事件
    const eventType = supportsPushState ? 'popstate' : 'hashchange'
    window.addEventListener(
      eventType,
      handleRoutingEvent
    )
    this.listeners.push(() => {
      window.removeEventListener(eventType, handleRoutingEvent)
    })
  }
  //...
}
复制代码

history

接下来是history的源码,记住history模式需要依赖HTML5的History API。

关于History API:developer.mozilla.org/zh-CN/docs/…

export class HTML5History extends History {
// ...
   setupListeners () {
    // ...
    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
      // 避免在某些浏览器中手册渲染会触发popstate事件
      const location = getLocation(this.base)
      if (this.current === START && location === this._startLocation) {
        return
      }
			// 判断路由并跳转
      this.transitionTo(location, route => {
        if (supportsScroll) {
          handleScroll(router, route, current, true)
        }
      })
    }
    window.addEventListener('popstate', handleRoutingEvent)
    this.listeners.push(() => {
      window.removeEventListener('popstate', handleRoutingEvent)
    })
  }
//...
}
复制代码

可以看出history是用popstate事件监听路由的。

页面是如何渲染的?

我们看到vue-router实例的工具其实是记录了大量的信息(包括我们的路由配置,还有一些当前路由下的信息),但我们的页面是怎么渲染的呢?

既然要用vue-router,就一定会用到router-view组件,而这个组件就是负责渲染的。

export default {
  name: 'RouterView',
  functional: true,
  props: {
    name: {
      type: String,
      default: 'default'
    }
  },
  render (_, { props, children, parent, data }) {
    // ...
    let inactive = false
    // ...
    // 如果用了keep-alive 会把component缓存
    if (inactive) {
      const cachedData = cache[name]
      const cachedComponent = cachedData && cachedData.component
      if (cachedComponent) {
        if (cachedData.configProps) {
          fillPropsinData(cachedComponent, data, cachedData.route, cachedData.configProps)
        }
        return h(cachedComponent, data, children)
      } else {
        // 渲染之前的缓存组件
        return h()
      }
    }
    // ... 
    return h(component, data, children)
  }
复制代码

router-view本身就是一个组件,他的render中维持一个缓存,当搭配了keep-alive之后会把展示过的组件缓存起来。下次如果再渲染时可以直接用。

总结

今天我们了解了vue-router的机制逻辑。大致为,先通过Vue.use注册一些组件,以及监听Vue.prototype.$router等。当通过new创建router实例的时候,router实例中会维持我们的路由信息,并根据mode生成history实例。页面配合router-view组件实现渲染。希望可以帮助大家有个大致的了解帮助。

参考

router.vuejs.org/

zhuanlan.zhihu.com/p/27588422

router.vuejs.org

おすすめ

転載: juejin.im/post/7050789249393639437