vue-router源码解析

源码下载:github.com/vuejs/vue-r…

1. 简述

vue-router基于Vue组件化的概念,使用VueRouter对vue进行组件化的路由管理。也就是说,路由变化时,使用组件替换的方式更新页面展示。替换的地方,就是router-view组件所在地方,router-view组件是一个functional组件,根据路径和match算法,找到要渲染的组件进行渲染。这种渲染只有路径变化才会发生。

引起路由(route)变化主要有以下几种情况: (1) 浏览器上前进、后退按钮 (2) 页面刷新 (3) 使用router调用push、pop、replace、go、back、forward等方法,主动改变路由 (4) 浏览器url输入栏更改location (5) router-link组件点击触发

VueRouter实例化后,才具有路由功能,router和route是两个重要的概念,都可称作路由,容易混淆,这里将router称作路由管理器,route称作路由。router是VueRouter的实例,具有路由管理功能,也就是说管理route。route是随着location(url)变化而变化的,一般来说route与组件的关系是1-n,具体由路由配置表项routes配置决定。路由(router)的配置过程是:

(1) Vue.use(VueRouter) 
(2) 创建路由实例router,用于整个vue项目的路由管理
router = new VueRouter({
	mode:...
	routes:[
	{
		path:...
		component:...
		children:...
	},
	...
]
})
(3) main.js中将router注入到根实例
new Vue({
	...
	router,
	...
})
复制代码

2. 目录结构

目录结构如下,

3. index.js——VueRouter类

index.js定义了VueRouter类。

3.1 构造函数

constructor (options: RouterOptions = {}) {
  this.app = null //vue实例
  this.apps = [] //存放正在被使用的组件(vue实例),只有destroyed掉的
组件,才会从这里移除)
  this.options = options //vueRouter实例化时,传来的参数,也就是
router.js中配置项的内容
  this.beforeHooks = [] //存放各组件的全局beforeEach钩子
  this.resolveHooks = [] //存放各组件的全局beforeResolve钩子
  this.afterHooks = [] //存放各组件的全局afterEach钩子
  this.matcher = createMatcher(options.routes || [], this) //由
createMatcher生成的matcher,里面有一些匹配相关方法
  let mode = options.mode || 'hash'  //模式

//模式的回退或者兼容方式,若设置的mode是history,而js运行平台不支持
supportsPushState 方法,自动回退到hash模式
  this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false
  if (this.fallback) {
    mode = 'hash'
  }

//若不在浏览器环境下,强制使用abstract模式
  if (!inBrowser) {
    mode = 'abstract'
  }
  this.mode = mode

 //不同模式,使用对应模式Histroty管理器去管理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}`)
      }
  }
}
复制代码

3.2 vueRouter内部方法

3.2.1 match

输入参数raw,current,redirectedFrom,结果返回匹配route

 match (
    raw: RawLocation, // /user/4739284722这种形式,类似route的path
    current?: Route,
    redirectedFrom?: Location
  ): Route {
    return this.matcher.match(raw, current, redirectedFrom)
  }
复制代码

3.2.2 currentRoute

是一个get方法,用于获取当前history.current,也就是当前route,包括path、component、meta等。

  get currentRoute (): ?Route {
    return this.history && this.history.current
  }
复制代码

3.2.3 init

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

// assert是个断言,测试install.install是否为真,为真,则说明vueRouter已经安装了
  process.env.NODE_ENV !== 'production' && assert(
    install.installed,
    `not installed. Make sure to call \`Vue.use(VueRouter)\` ` +
    `before creating root instance.`
  )
  //将vue实例推到apps列表中,install里面最初是将vue根实例推进去的
  this.apps.push(app)

  // set up app destroyed handler
  // https://github.com/vuejs/vue-router/issues/2639

// app被destroyed时候,会$emit ‘hook:destroyed’事件,监听这个事件,执行下面方法
// 从apps 里将app移除
  app.$once('hook:destroyed', () => {
    // clean out app from this.apps array once destroyed
    const index = this.apps.indexOf(app)
    if (index > -1) this.apps.splice(index, 1)
    // ensure we still have a main app or null if no apps
    // we do not release the router so it can be reused
    if (this.app === app) this.app = this.apps[0] || null
  })

  if (this.app) {
    return
  }
  // 新增一个history,并添加route监听器
  //并根据不同路由模式进行跳转。hashHistory需要监听hashchange和popshate两个事件,而html5History监听popstate事件。
  this.app = app

  const history = this.history

  if (history instanceof HTML5History) {
    //HTML5History在constructor中包含了监听方法,因此这里不需要像
    //HashHistory那样setupListner。
    history.transitionTo(history.getCurrentLocation())
  } else if (history instanceof HashHistory) {
    const setupHashListener = () => {
      history.setupListeners()
    }
    history.transitionTo(
      history.getCurrentLocation(),
      setupHashListener,
      setupHashListener
    )
  }
 //将apps中的组件的_route全部更新至最新的
  history.listen(route => {
    this.apps.forEach((app) => {
      app._route = route
    })
  })
}
复制代码

3.2.4 全局路由钩子

在路由切换的时候被调用,可以自定义fn。可以在main.js中使用VueRoute实例router进行调用。比如:

router.beforeEach((to,from,next)=>{})
复制代码
 //将回调方法fn注册到beforeHooks里。registerHook会返回,fn执行后的callback方法,功能是将fn从beforeHooks删除。
 beforeEach (fn: Function): Function {
    return registerHook(this.beforeHooks, fn)
  }
//将回调方法fn注册到resolveHooks里。registerHook会返回,fn执行后的callback方法,功能是将fn从resolveHooks删除。
  beforeResolve (fn: Function): Function {
    return registerHook(this.resolveHooks, fn)
  }
//将回调方法fn注册到afterHooks里。registerHook会返回,fn执行后的callback方法,功能是将fn从afterHooks删除。
  afterEach (fn: Function): Function {
    return registerHook(this.afterHooks, fn)
  }
复制代码

3.2.5 registerHook

将callback(参数fn)插入list,返回一个方法,方法实现的是从list中删除fn。也就是在callback执行后,通过调用这个方法,可以将fn从list中移除。

function registerHook (list: Array, fn: Function): Function {
  list.push(fn)
  return () => {
    const i = list.indexOf(fn)
    if (i > -1) list.splice(i, 1)
  }
}
复制代码

3.2.6 主动改变路由的功能

通常我们在组件内$router.push调用的就是这里的push方法。 其中onReady方法,添加一个回调函数,它会在首次路由跳转完成时被调用。此方法通常用于等待异步的导航钩子完成,比如在进行服务端渲染的时候,示例代码如下: return new Promise((resolve, reject) => { const {app, router} = createApp() router.push(context.url) // 主动去推,服务端 router.onReady(() => {// 页面刷新,检测路由是否匹配 const matchedComponents = router.getMatchedComponents() if (!matchedComponents.length) { return reject(new Error('no component matched')) } resolve(app) }) })

onReady (cb: Function, errorCb?: Function) {
  this.history.onReady(cb, errorCb)
}
//报错
onError (errorCb: Function) {
  this.history.onError(errorCb)
}
//新增路由跳转
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
  this.history.push(location, onComplete, onAbort)
}
//路由替换
replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
  this.history.replace(location, onComplete, onAbort)
}

//前进n条路由
go (n: number) {
  this.history.go(n)
}
//回退一步
back () {
  this.go(-1)
}
//前进一步
forward () {
  this.go(1)
}
	
复制代码

3.2.7 getMatchedComponents & resolve

getMatchedComponents 根据路径或者路由获取匹配的组件,返回目标位置或是当前路由匹配的组件数组(是组件的定义或构造类,不是实例)。通常在服务端渲染的数据预加载时使用到。里面的组件满足:前一个组件嵌套着下一个组件的。参考下图:

getMatchedComponents (to?: RawLocation | Route): Array {
  const route: any = to
    ? to.matched
      ? to
      : this.resolve(to).route
    : this.currentRoute
  if (!route) {
    return []
  }
  return [].concat.apply([], route.matched.map(m => {
    return Object.keys(m.components).map(key => {
      return m.components[key]
    })
  }))
}


//一般用于解析目标位置
//返回包含如下属性的对象
{
  location:Location;
  route:Route;
  href:string;
}

resolve (
  to: RawLocation,
  current?: Route,
  append?: boolean
): {
  location: Location,
  route: Route,
  href: string,
  // for backwards compat
  normalizedTo: Location,
  resolved: Route
} {
  current = current || this.history.current
  const location = normalizeLocation(
    to,
    current,
    append,
    this
  )
  const route = this.match(location, current)
  const fullPath = route.redirectedFrom || route.fullPath
  const base = this.history.base
  const href = createHref(base, fullPath, this.mode)
  return {
    location,
    route,
    href,
    // for backwards compat
    normalizedTo: location,
    resolved: route
  }
}
复制代码

3.2.8 createHref&addRoutes

//建立路由在浏览器中显示格式
function createHref (base: string, fullPath: string, mode) {
  var path = mode === 'hash' ? '#' + fullPath : fullPath
  return base ? cleanPath(base + '/' + path) : path
}
 
//动态新增路由
addRoutes (routes: Array) {
  this.matcher.addRoutes(routes)
复制代码

4.install.js

VueRouter的一个方法,Vue.use(插件)来注册插件的时候会找到插件的install方法进行执行。install主要有以下几个目的: (1)子组件通过从父组件获取router实例,从而可以在组件内进行路由切换、在路由钩子内设置callback、获取当前route等操作,所有router提供的接口均可以调用。 (2)设置代理,组件内部使用this.route等同于获得当前_route (3)设置_route设置数据劫持,也就是在数值变化时,可以notify到watcher (4)注册路由实例,router-view中注册route。 (5)注册全局组件RouterView、RouterLink

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
  // 从父节点拿到registerRouteInstance,注册路由实例
  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
//调用初始化init方法
        this._router.init(this)
// 设置_route为Reactive
        Vue.util.defineReactive(this, '_route', this._router.history.current)
      } else {
//子组件从父组件获取routerRoot
        this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
      }
      registerInstance(this, this)
    },
    destroyed () {
      registerInstance(this)
    }
  })
  //设置代理 this.$router === this._routerRoot._router,组件内部通过
//this.$router调用_router
  Object.defineProperty(Vue.prototype, '$router', {
    get () { return this._routerRoot._router }
  })
//组件内部通过this.$route调用_route
  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
}
复制代码

5. base.js

这个文件定义了History类,VueRouter中的history,根据mode,可能是HTML5History、HashHistory或Abstract实例,其中HTML5History、HashHistory等都是继承自History。History提供了一些路由操作的基本方法,如下:

  1. Listen //listen callback
  2. onReady //监听路由是否ready,ready时,将所有cb装进readyCbs列表
  3. onError
  4. transitionTo //路由的跳转,会判断跳转to的路径是否在路由表中,是,才进行组件替换,调用confirmTransition。

5.1 constructor

constructor (router: Router, base: ?string) {
    this.router = router  //当前router
    this.base = normalizeBase(base)  //获取路由base
    // start with a route object that stands for "nowhere"
    this.current = START //由createRoute生成的基础路由,path:'/'
    this.pending = null
    this.ready = false //history状态,路由是否已经更新好
    this.readyCbs = [] //ready状态下的callbacks
    this.readyErrorCbs = [] //
    this.errorCbs = [] //
  }
复制代码

6. HashHistroy

contructor中由fallback的判断与执行方法。

  1. go //调用window.history.go(n)方法
  2. push
  3. replace
  4. ensureUrl //导航栏url替换,当组件被替换后,会调用此方法改变location。至此route切换完成。ready 状态为 true。然后readyCbs被一次执行。根据参数push为true,执行pushHash,false执行replaceHash。
  5. getCurrentLocation://获取当前location,返回的是hash值
  6. setupListeners:hashHitory需要同时监听hashchange和popstate事件,前者来自#hash中的hash变化触发,后者来自浏览器的back,go按钮的触发。根据hash进行切换。

其中看下setupListeners这个方法:

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

	//ensureSlash路径是否正确,path的第一为是否是'/'
        if (!ensureSlash()) {
          return
        }

	
//transitionTo调用的父类History下的跳转方法,跳转后路径会进行hash化。
        this.transitionTo(getHash(), route => {
  //根据router配置的scrollBehavior,进行滚动到last pos。
          if (supportsScroll) {
            handleScroll(this.router, route, current, true)
          }
          if (!supportsPushState) {
            replaceHash(route.fullPath)
          }
        })
      }
    )
  }

//判断supportsPushState(浏览器对'pushState'是否支持),true时调用
//push-state.js中的pushState方法。反之直接将当前url中hash替换。
function pushHash (path) {
  if (supportsPushState) {
    pushState(getUrl(path))
  } else {
    window.location.hash = path
  }
}

//判断supportsPushState(浏览器对'pushState'是否支持),true时调用
//push-state.js中的replaceState方法。反之直接将当前url替换。
function replaceHash (path) {
  if (supportsPushState) {
    replaceState(getUrl(path))
  } else {
    window.location.replace(getUrl(path))
  }
}
复制代码

7. Html5History

  1. go
  2. push
  3. replace
  4. ensureUrl // 与HashHistory的对应方法类型,根据参数push为true,执行 pushState,false执行replaceState。HashHistory相比多了一步 supportsPushState的判断。
  5. getCurrentLocation

html5History下只需监听浏览器的‘popstate’事件,根据传来的参数获取location,然后进行路由跳转即可,这部分在constructor中包含,html5History实例化的时候就会执行。

constructor (router: Router, base: ?string) {
    super(router, base)

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

    if (supportsScroll) {
      setupScroll()
    }
    
    const initLocation = getLocation(this.base)
    window.addEventListener('popstate', e => {
      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 === initLocation) {
        return
      }

      this.transitionTo(location, route => {
        if (supportsScroll) {
          handleScroll(router, route, current, true)
        }
      })
    })
  }
复制代码

8. Hash&History两种mode比较

  1. url的美观性,hash模式中,一定要有#与路由path隔开,history的url更美观
  2. hash模式下,页面刷新还是能正常现在页面,而history存在刷新404的问题,需要与后端配合设置静态资源根路径的问题。
  3. hash模式window监听hashchange和popstate两种事件,而history只监听popshate事件。
  4. history模式,实际向后端提出了http请求,而hash模式没有,只是hash变化,不与后端有任何请求。

转载于:https://juejin.im/post/5d4e90b8f265da03d42f9457

你可能感兴趣的:(vue-router源码解析)