这里大概分析一下vue router整体流程, push方法原理和如何监听用户修改url,然后进行视图渲染。

在一个项目中我们可能会这么去配置一个router

Vue.use(VueRouter)

const routes: Array<RouteConfig> = [
    {
        path: '/',
        redirect: '/login',
        component: login
    },
]

const router = new VueRouter({
    base: process.env.BASE_URL,
    routes
})

在这段代码中做了三件事情。第一:Vue.use     第二:设置配置项  第三:new 一个Router实例。下面分析下这第一和第三做了什么事情。

Vue.use

看上去只是简单的一句话。但是在这句话里面vue会加载它的插件,会执行插件定义的install方法。下面是Vue Router Install的源码

mport 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 () {
      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初始化全局API的时候执行,即Vue源码中一个叫做initGlobalAPI的方法吗,在这里Vue会主动去执行插件的install方法。对于Vue Router来说就是上面这段install的代码。

install代码主要做了几件关键的事情。关注这段代码(如下)。

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)
    }
  })

这段代码做了一件事情,在Vue组建中都加入一个混入beforeCreate生命钩子函数。这个函数里面有一句这样的话,这句话的作用就是在Vue上添加一个_route属性,并且他是响应式的,在我们使用push方法的时候会给这个属性重新赋值,从而触发拦截器的set方法然后去进行视图的更新(这部分需要对Vue响应式有所了解)

Vue.util.defineReactive(this, '_route', this._router.history.current)

在上面整个install方法还有一个地方值得关注。

this._router.init(this)

现在看看init的作用是什么

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

    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 && 'fullPath' in routeOrError) {
          handleScroll(this, routeOrError, from, false)
        }
      }
      //这里设置侦听器 监控用户修改浏览器地址的行为
      const setupListeners = routeOrError => {
        history.setupListeners()
        handleInitialScroll(routeOrError)
      }
      history.transitionTo(
        history.getCurrentLocation(),
        setupListeners,
        setupListeners
      )
    }

history.listen(route => { this.apps.forEach(app => { app._route = route }) }) }

开头有一个if语句。if下有一个setupListeners的方法,在不同的路由模式这个方法作用有所区别。但是共同点就是用来监听用户修改url的时候,然后根据用户的修改的url进行路由匹配从而更新页面的渲染

这个if语句下有一段这样的话,这句话就是刚刚开始的时候讲的给_route属性重新赋值,从而触发拦截器set然后进行视图的更新

 history.listen(route => {
      this.apps.forEach(app => {
        app._route = route
      })
    })

现在就介绍完了在Vue.use的时候做得几件重要的事情

 

new Router

这个阶段做的事情不是很多,第一件事情,就是根据用户传入过来的router选项,然后根据这个选项整理为Vue Router里面所要使用的数据结构。 第二件事情就是根据不同的路由模式去创建不同的路由实例,代码如下:

constructor (options: RouterOptions = {}) {
    ....省略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}`)
        }
    }
  }

 

push方法

这里以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
    )
  }

push本质上执行的就是transitionTo方法,这个方法源码是这样的。

transitionTo (location: RawLocation, onComplete?: Function, onAbort?: Function) {
  const route = this.router.match(location, this.current)
  this.confirmTransition(route, () => {
    this.updateRoute(route)
    ...
  })
}

第一句话的match方法就是用来匹配当前路由对应的组件。然后在comfirmTransition有一个updateRoute的方法。这个方法做了一件事情就是触发一个cb,代码如下:

updateRoute (route: Route) {
    this.current = route
    this.cb && this.cb(route)
}

那这个cb是什么,回到前面的init方法中:

 history.listen(function (route) {
    this$1.apps.forEach(function (app) {
      app._route = route;
    });
 });


//
history的listen方法
listen (cb) {
this.cb = cb;
}

listen方法的cb其实就是上面的函数,所以执行cb本质就是在执行函数参数,执行函数参数就会执行对_route属性的赋值,。从而触发set,然后完成视图更新。

现在整个push如何触发视图的重新渲染已经清晰了,所以这也解释了为什么vue是一个单页面,本质上就是在对dom的替换,而不是在请求页面。

 

 

如何监听url改变

  我们知道vue路由其中的两种模式是hash和history。这里只介绍如何监听hash。hash有一个典型的标志就是#符号。假如用户改变#后面的url,浏览器是不会向后台发送请求的。并且会在浏览器种产生一条记录,就是说用户点击浏览器前进或者后退是可以追溯到你的hash改变记录。假如#后面的值发生改变浏览器提供了特定的事件hashchange可以对这个行为进行监听。

  基于这个特点,那么vue就利用这个监听的机制。每次用户改变url的时候,就会触发监听方法。然后vue就拿取用户改变的url后缀。然后去匹配相应的组件,然后去完成视图的更新。这个就是改变url的渲染原理。

  我们回到init方法上来。

init (app /* Vue component instance */) {
  ...省略
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 && 'fullPath' in routeOrError) { handleScroll(this, routeOrError, from, false); } }; const setupListeners = routeOrError => { history.setupListeners(); handleInitialScroll(routeOrError); }; history.transitionTo( history.getCurrentLocation(), setupListeners, setupListeners ); } }

关注有一个这段

 history.setupListeners()

然后在hashHistory这个方法是这样的:

setupListeners () {
    ...省略
    const eventType = supportsPushState ? 'popstate' : 'hashchange'
    window.addEventListener(
      eventType,
      handleRoutingEvent
    )
    this.listeners.push(() => {
      window.removeEventListener(eventType, handleRoutingEvent)
    })
  }

看到这段代码。本质上hash模式在浏览器支持的情况下优先使用popstate,其次才是使用hashchange监听。所以就是,在beforeCreate生命周期中,就已经开始了这个监听。从而实现用户在修改url的时候进行

视图的更新。

 

上面的内容大致vue router原理的简单介绍。

 

posted on 2020-09-19 21:01  余圣源  阅读(399)  评论(0编辑  收藏  举报