Vue-Router源码分析之index.js

1,656 阅读6分钟

前言

虽然最近需求着实不少,但是感觉自己学习劲头还是蛮足的,并没有被需求压垮。今天,带来Vue-Router源码解析系列的第二篇文章:index.js。

系列文章:

Vue-Router 源码学习之我们从API中看些门道

Vue-Router源码分析之index.js

Vue-Router源码分析之install方法

正文

vue-router类里面都做了什么?

index.js是vue-router这个类的主构造函数,所以内容上算是比较关键的:

从图片中我们可以看出来,这是一个ES6声明类的方法,vue-router源码中类的声明都是使用类ES的语法,constructor (options: RouterOptions = {}),在vue-router中使用了flow.js做了类型的检查,

什么是flow.js?flow.js怎么使用呢?因为篇幅原因,这里就暂时先不做涉及。各位小伙伴,可以参看官网:flow.org/en/docs/typ…

解析:constructor

首先我们来看一下constructor内的代码,

constructor (options: RouterOptions = {}) {
    this.app = null
    this.apps = []
    this.options = options
    this.beforeHooks = []
    this.resolveHooks = []
    this.afterHooks = []
    this.matcher = createMatcher(options.routes || [], this)
    //默认为hash锚点
    let mode = options.mode || 'hash'
      //当然使用的是history模式 h5的pushState的方式来实现路由跳转的,对options设置fallback属性为true时会回退到hash模式
      // 是否支持回退
    this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false
    if (this.fallback) {
      mode = 'hash'
    }
    if (!inBrowser) {
      mode = 'abstract'
    }
    //没有fallback的话选择锚点模式,node环境选择abstract模式
    this.mode = mode

    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实例的时候是怎么做的?

let router = new Router({
    base : '/',
    mode : 'history',
    routes : [{
        component : xxx,
        path : xxx
    },xxx]
})
constructor (options: RouterOptions = {}) 
options就是我们刚才上面的一个对象,里面有base、mode、routes等属性

这时候我们知道options是个什么东西了,我们来看看内部对options进行了哪些处理。

首先我们说一下vue-router最核心的内容之一:

解析:mode

我们知到vue-router的路由有两种方式,一种是#锚点性的,第二种是和正常路径一样的,可是vue构建的应用是一个但页面应用如何让他像正常的多页面应用一样,是在不停的改变路径呢? 这里面就使用了html5的history的pushState与replaceState(让页面看起来无刷新的改变路径),具体内容大家可以看一下官网文档和大神张鑫旭的博客(www.zhangxinxu.com/wordpress/2…

在vue-router源码中有一个工具类专门做了这个事情:

我们来看一下vue-router是如何匹配mode的吧:

// vue-router默认使用hash模式
let mode = options.mode || 'hash';
this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false
// 如果选择了history但是pushState方法并不能使用并且设置了
// 在当浏览器不支持 history.pushState 控制路由是否应该回退到 hash 模式的情况下。
// (options.fallback默认就是true)
// 如果发现需要回退了,就回到hash锚点模式
    if (this.fallback) {
      mode = 'hash'
    }
// 不在浏览器环境就选择abstract模式(在node环境)
    if (!inBrowser) {
      mode = 'abstract'
    }
    this.mode = mode
// 根据三种情况是成不同的路由转换实例。
// 如果没有mode不是这三种情况就报错。
// HTML5History、HTML5History、HTML5History三个类都是继承与一个base类
// 里面有这三种模式对于路径转换时做的事情进行了一定的封装。
    switch (mode) {
      case 'history':
        this.history = new HTML5History(this, options.base)
        break
      case 'hash':
        this.history = new HTML5History(this, options.base, this.fallback)
        break
      case 'abstract':
        this.history = new HTML5History(this, options.base)
        break
      default:
        if (process.env.NODE_ENV !== 'production') {
          assert(false, `invalid mode: ${mode}`)
        }
    }

到这里大家应该对我们写的mode模式有一点的了解了吧。

解析:init

下面说一下init方法,上一章我们讲了在根节点的beforeCreate生命周期钩子中,使用了init方法,如果忘记了可以翻看上一篇install方法的学习来回归一下

所以app就是根组件,init在执行前要判断一下,vue-router是不是被vue成功use了,因为成功use之后,会把install方法的installed属性设置为true:

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)
    // main app already initialized. (根组件已经被初始化)
    if (this.app) {
      return
    }

    this.app = app

    const history = this.history
    // 当我们的根组件完成了对vue-router的init的时候我们就要完成第一次路由的跳转了
    // 当我们的项目启动的时候肯定会有一个路径,这个路径是什么不重要
    // 我们在第一次进入这个路径的时候,会进行vue-router的初始化
    // 初始化之后要开始展示对应的组件,可是我们vue-router那些popState的事件肯定没有绑定,不会触发啊,
    // 怎么办?? 那就手动触发一下这个事情,
    // 第一次进入肯定没有对应的事件,不会完成跳转时该做的事情。
    if (history instanceof HTML5History) {
      history.transitionTo(history.getCurrentLocation())
    } else if (history instanceof HashHistory) {
      const setupHashListener = () => {
        history.setupListeners()
      }
      history.transitionTo(
        history.getCurrentLocation(),
        setupHashListener,
        setupHashListener
      )
    }
    // 针对设置的不同mode(模式)
    // 将mode的时候,每种模式的history实例来源于三个不同的类,所以instanceof足够判断是哪种模式。

到现在,我们上一章的init函数已经串联起来了,不知道大家感觉怎么样?我感觉舒服了很多了。

路由守卫

用过vue-router的同学们都知道路由守卫的概念,这是在路由跳转的前后等三个地方设置了不同的钩子,帮助我们在进入离开路由前做一些事情。这个钩子是怎么做的呢?

声明了三个数组,存放每个周期内的钩子上绑定的函数。

vue-router全局级别的beforeEach、beforeResolve、afterEach做了什么?

就这么两行代码你敢信??我也很纠结你就干了这么点事情。 执行一下registerHook函数,从字面意思一看就是注册钩子,

怎么注册的呢?

function registerHook (list: Array<any>, fn: Function): Function {
  list.push(fn)
  // 返回值是一个function
  return () => {
    const i = list.indexOf(fn)
    if (i > -1) list.splice(i, 1)
  }
}

接收一个生命周期的钩子数组,将我们要执行的函数传到数组内就可以完成注册了,我还没看到这三个数组的内容,但是直觉告诉我很有可能就是,观察者模式(以后就探索去)。注册到这应该就OK了,

为什么还有个返回值呢? 返回值的内容一看就是要清楚钩子内的函数呀,我们调用这个registerHook函数后,可以得到注册函数的清除函数,清除的是钩子数组中对应的函数,还有这么一手,牛的一匹。(让代码教你如何熟练使用闭包~)

vue-router的编程式导航是怎么做的?

push方法与replace、go方法调用对应路由转换实例的对应方法,因为不同模式大家方法肯定都不一样, back与forward都是go方法传入特殊参数,所以看到这里我们发现history这个实例的内容很关键。


清一清嗓

到了这里我们vue-router的主线流程我们已经进行了一个梳理,不知道大家对这一块内容感觉满意吗? 不满意就请不要邮寄刀片哈。

所以到现在我们简单进行一下总结

1:mode是设置模式的,有hash、history、abstract三种模式、每个模式会导致vue-router实例的history不同,来自三个不同的类、每个类又继承于总的base类。 2:init方法会初始化整个组件、并且在vue-router的实例中设置了app属性存放根组件(这个确实很有用)、手动的完成初始化后的第一次路由跳转。 3:beforeEach、beforeREsolve、afterEach三个全局的钩子都有对应的钩子函数数组,存放每个周期钩子内要做的事情、使用registerHook方法来完成钩子函数的注册,registerHook也可以清除钩子内对应的函数。 4:push、replace、go等方法都是使用history方法内的对应方法。

总结完毕 我们学到了什么? history真重要,我要好好看看他内部的实现

真的没出息的总结。

vue-router类中还有一部分对options.routes的处理

options.routes 就是 [{path : 'xxx',components : 'xxx'}] 这个数组

生成一个根据options.routes的一份比对程序,完成程序的比对。

下一期的内容要在继续学习index.js和开荒history中进行一个抉择,具体是什么内容大家可以积极留言,给个方向呀。

结束语

每一个前端er(boy and girl) 你们都不是一个人在战斗。

安排!!!!

我是一个应届生,最近和朋友们维护了一个公众号,内容是我们在从应届生过渡到开发这一路所踩过的坑,已经我们一步步学习的记录,如果感兴趣的朋友可以关注一下,一同加油~

个人公众号:IT面试填坑小分队