简述vue-router实现原理

router源码解读

阅读请关注下代码注释

打个广告:哪位大佬教我下sf怎么排版啊,不会弄菜单二级导航(扑通.gif)

logo.png

<h2>1. router是什么</h2>

首先,你会从源码里面引入Router,然后再传入参数实例化一个路由对象

// router/index.js
import Router from 'vue-router'
new Router({...})
...

源码基础类:

// 源码index.js
export default class VueRouter {
  ...
  constructor (options: RouterOptions = {}) {
    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'   // 不选择模式会默认使用hash模式
    this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false
    if (this.fallback) {
      mode = 'hash'
    }
    if (!inBrowser) {         // 非浏览器环境默认nodejs环境
      mode = 'abstract'
    }
    this.mode = mode

    switch (mode) { // 根据参数选择三种模式的一种
      case 'history':
        this.history = new HTML5History(this, options.base) // 根据HTML5版History的方法和属性实现的模式
        break
      case 'hash':
        this.history = new HashHistory(this, options.base, this.fallback) // 利用url中的hash特性实现
        break
      case 'abstract':
        this.history = new AbstractHistory(this, options.base) // 这种模式原理暂不清楚
        break
      default:
        if (process.env.NODE_ENV !== 'production') {
          assert(false, `invalid mode: ${mode}`)
        }
    }
  }
  ...
  // 一些api方法,你应该很熟悉,$router.push(...)
  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)
  }

  go (n: number) {
    this.history.go(n)
  }

  back () {
    this.go(-1)
  }

  forward () {
    this.go(1)
  }
  ...
}

我们创建的路由都是VueRouter类的实例化,用来管理我们的【key-components-view】,一个key(代码中的path)对应一个组件,view也就是<router-view>在template里面占个坑,用来根据key展示对应的组件,实例上的func让我们可以控制路由,也就是官网的api
说简单点,路由就是一个‘轮播图’,emmmmmm,说轮播好像也不过分哈,写个循环切换key的func就是‘轮播了’,而key就是轮播的index,手动滑稽。那么,vue-router是如何实现不发送请求就更新视图的呢,让我们来看看vue如何使用路由的
实例化后的路由输出:区分下route和router
clipboard.png

2. router工作原理
如果你要使用到router,你会在实例化Vue的参数options中加入router

// main.js
improt xxx from xxx
import router from xxx
new Vue({
  el: '#app',
  router: router,
  components: { App },
  template: '<App/>'
})

那,Vue是如何使用这个参数呢,vue-router是作为插件加入使用的,通过mixin(混合)来影响每一个Vue实例化,在beforeCreate 钩子的时候就会完成router的初始化,从参数获取router -> 调用init初始化 -> 加入响应式(defineReactive方法在vue源码用的很多,也是底层实现响应式的核心方法)clipboard.png

// 源码install.js
Vue.mixin({
    beforeCreate () {
      if (isDef(this.$options.router)) {
        this._routerRoot = this
        this._router = this.$options.router   // 获取options里面的router配置
        this._router.init(this)               // 初始化,这个init是VueRouter类里面的方法,实例化后会继承这个方法,方法代码见下方 
        Vue.util.defineReactive(this, '_route', this._router.history.current) // 这个是把_route加入数据监控,所以你可以watch到_route
      } else {
        this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
      }
      registerInstance(this, this)
    },
    destroyed () {
      registerInstance(this)
    }
  })

初始化会做些什么:
-判断主程序状态(已经初始化了的vue实例不会再重新初始化路由,也就是你不能手动去再init一次)
-把实例加入内置数组
-判断history的类型,做一些类似优化的事,比如hash模式的setupListeners方法,就是延迟监听hashChange事件,等到vue完成挂载再监听,太细节不用深入
-listen定义一个callback,listen是定义在最底层History类上的,作用就是定义一个callback,listen会在需要的时候被调用,在路由发生变化的时候会执行这个callback

// 源码index.js
export default class VueRouter {
...
init (app: any /* Vue component instance */) {
    process.env.NODE_ENV !== 'production' && assert(
      install.installed,
      `not installed. Make sure to call \`Vue.use(VueRouter)\` ` +
      `before creating root instance.`
    )

    this.apps.push(app)   // 这个apps存储了让所有的Vue实例化(根组件),后面遍历的时候,会把当前标记route挂到所有根组件的,也就是 vm._route 也是 vm._router.history.current

    // main app already initialized.
    if (this.app) {
      return
    }

    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 => {               // 注意这个listen会在后面用到
      this.apps.forEach((app) => {
        app._route = route                  // 根组件全部获取当前route
      })
    })
  }
...
}

关于route的变化过程会在下面具体模式中说明,这里先跳过,接下来先说vue拿到router后,怎么使用router来渲染组件的

3. vue如何使用router的

在安装vue-router插件的时候

export function install (Vue) {
  ...
  Vue.component('RouterView', View)  // <router-link> & <router-view> 你应该很熟悉,本质就是vue组件,看源码之前我的猜测也是组件
  Vue.component('RouterLink', Link)
  ...
}

router-link你不一定会使用,但是router-view你肯定会使用,它就是作为'窗口'的存在,来渲染你需要展示的组件。
那,从这个组件开始说,一个前提条件是:vnode是通过render来创建的,也就是说改变_route的值会执行render函数,Router-View这个组件定义了自己的render,省略了大部分代码,这两行够了,你最终通过<router-view>看到的视图就是这么来的

// vue源码render.js
export function renderMixin (Vue: Class<Component>) {
...
vnode = render.call(vm._renderProxy, vm.$createElement)
...
}
// router源码 view.js
render (_, { props, children, parent, data }) {
...
const h = parent.$createElement
...
return h(component, data, children)
}

第一种:hashHistory模式

流程

$router.push() --> HashHistory.push() --> History.transitionTo() --> History.updateRoute() --> {app._route = route} --> vm.render()

1. 关于hash
url中#号后面的参数,别名哈希值,关于hash的一些特性

1.改变hash并不会引起页面重载
2.HTTP请求不包括#,所以使用hash不会影响到其他功能
3.改变#会改变浏览器的访问历史
4.window.location.hash可以读取哈希值
5.JavaScript可以通过onhashchange监听到hash值的变化,这就意味着可以知道用户在浏览器手动改变了hash值

clipboard.png

clipboard.png

因为这些特性才有的hashHistory
更多关于hash知识见 URL的井号 - 阮一峰的网络日志

2. hashHistory源码
首先,这三种模式都是通过继承一个基础类History来的

export class HashHistory extends History {
...
}

那,三种模式肯定有相同的属性,相同的方法,肯定不会去创建三次所以从一个基类继承,然后各自的部分属性or方法会有差异,至于History这个类,我是不会去细看的,反正我也看不懂,哈哈哈哈

clipboard.png

router上的实例属性、方法可以在VueRouter、HashHistory/HTML5History/AbstractHistory、History上找到,这里说下HashHistory的几个func的实现、

// router源码hash.js
export class HTML5History extends History {
...
go (n: number) {
    window.history.go(n)
  }
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    const { current: fromRoute } = this
    this.transitionTo(location, route => {  // History类上的func
      pushHash(route.fullPath)
      handleScroll(this.router, route, fromRoute, false)
      onComplete && onComplete(route)
    }, onAbort)
  }

function pushHash (path) {
  if (supportsPushState) { // 是否浏览器环境且环境支持pushstat方法,这个func下面会说
    pushState(getUrl(path)) // 支持的话往window.history添加一条数据
  } else {
    window.location.hash = path // 不支持的话直接修改location的hash
  }
}

  replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    const { current: fromRoute } = this
    this.transitionTo(location, route => {
      replaceHash(route.fullPath)
      handleScroll(this.router, route, fromRoute, false)
      onComplete && onComplete(route)
    }, onAbort)
  }
// 其实replace和push只有两个区别
1.
window.location.hash = path
window.location.replace(getUrl(path))
2.
if (replace) { // replace调这个func会传一个true
  history.replaceState({ key: _key }, '', url)
} else {
  _key = genKey()
  history.pushState({ key: _key }, '', url)
}
...
}

还有一点就是,在初始化hash模式路由的时候,会执行一个func,监听hashchange事件

setupListeners () {
    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)
        }
      })
    })
}

第二种:HTML5History模式

HTML5--History 科普

主要是新增的两个api

1.History.pushState()

clipboard.png
[优点写的清清楚楚]
clipboard.png

clipboard.png

HTML5History的push、replace跟hash模式的差不多,就不上代码了
一个标记是否支持HTML5的flag,这么写的,有需要的可以刨回去用

export 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 && 'pushState' in window.history
})()

还有一个就是scrollBehavior,用来记录路由跳转的时候滚动条的位置,这个只能在HTML5模式下使用,即支持pushState方法的时候,部分博客说只有在HTML5History下才能使用,这个等我明天验证一下,我个人觉得支持HTML5就可以了

2.History.replaceState()

clipboard.png
说的也很直观,就是不创新新纪录而覆盖一条记录,just do it

结束语

别问第三种情况(我是谁、我在哪、谁打我)

我兜子好沃,早知道不做前端了~

在学习router源码的时候阅读了熵与单子的代码本的文章,看完这篇文章配合源码基本都可以很好掌握vue-router的大概,感谢作者,另外说明下本文由本人学习结束后加上自己的理解一字一字敲出来的,可能有些相似之处,侵删请联系我,写文章的目的是看看自己能否表述清楚,对知识点的掌握情况,讲的不对的地方,请各位大佬指正~

~感谢潘童鞋的指导(^▽^)

当然,我也稀罕你的小❤❤,点个赞再走咯~

以上图片均来自MDN网页截图、vue官网截图、百度首页截图,不存在版权问题 /滑稽

【注】:内容有不当或者错误处请指正~转载请注明出处~谢谢合作!

阅读 3.1k更新于 3月3日
推荐阅读
木鱼coc
用户专栏

悟空,救我~

768 人关注
7 篇文章
专栏主页
目录