• Vue Router 源码分析


    专栏分享:vue2源码专栏玩具项目专栏,硬核 💪 推荐 🙌

    欢迎各位 ITer 关注点赞收藏 🌸🌸🌸

    本篇文章参考版本:vue-router v3.x
    最终成果,实现了一个可运行的核心路由工程:柏成/vue-router3.x
    目录结构如下:

    .
    |-- components           // 组件(view/link)
    |   |-- router-link.js
    |   `-- router-view.js
    |-- create-matcher.js    // Route 匹配
    |-- create-route-map.js  // Route 映射表
    |-- history              // Router 处理 (hash模式、history模式)
    |   |-- base.js
    |   |-- hash.js
    |   `-- html5.js
    |-- index.js             // Router 类
    `-- install.js           // Router 插件安装

    1. 路由注册

    起步

    我们先来看一个基本例子,熟悉其简单的应用配置

    import Vue from 'vue'
    import VueRouter from 'vue-router'
    
    Vue.use(VueRouter)
    
    // 1. 定义 (路由) 组件
    const Foo = { template: '
    foo
    '
    } const Bar = { template: '
    bar
    '
    } // 2. 定义路由 const routes = [ { path: '/foo', component: Foo }, { path: '/bar', component: Bar } ] // 3. 创建 router 实例,然后传 routes 配置 const router = new VueRouter({ mode: 'history', routes }) // 4. 创建和挂载根实例,记得注入路由器,从而让整个应用都有路由功能 const app = new Vue({ router }).$mount('#app')

    通过注入路由器,我们可以在任何组件内通过 this.$router 访问路由器,也可以通过 this.$route 访问当前路由(后面我们会详细介绍其内部实现)

    use

    我们发现,如果要在一个模块化工程中使用 vue-router,必须要通过 Vue.use() 明确地安装路由功能
    Vue.use(plugin) 是一个全局插件注册API,官方是这样介绍的:

    安装 Vue.js 插件时调用,如果插件是一个对象,必须提供 install 方法。如果插件是一个函数,它本身会被作为 install 方法。install 方法调用时,会将 Vue 对象作为第一个参数传入

    将 Vue 对象当做参数的好处就是插件的编写方不需要额外 import Vue 了,可减少插件包的体积

    install

    vue-router 的入口文件是 src/index.js,其中定义了 VueRouter 类;也实现了 install 的静态方法,它的定义在 src/install.js

    Vue.use(VueRouter) 默认会调用 VueRouter 类上的 install 方法

    src/index.js:

    import install from './install'
    export default class VueRouter{ }
    VueRouter.install = install

    src/install.js:

    import routerLink from './components/router-link'
    import routerView from './components/router-view'
    
    // 静态全局变量
    export let Vue
    
    function install (_Vue) {
      Vue = _Vue
    
      // mixin 内部会调用 mergeOptions方法, 所有组件初始化都会调用这个方法
      // 这里不能直接将属性定义在原型上, 只有在 new Vue 中传入了 router路由实例 才能被后代组件共享
      Vue.mixin({
        beforeCreate () {
          // 组件渲染是从父到子的
          // 这样保证了有 router路由实例才加,没有 router路由实例就不加
          if (this.$options.router) {
            this._routerRoot = this // 根实例
            this._router = this.$options.router // router路由实例
    
            this._router.init(this) // this 就是我们的根应用 new Vue()
    
            // 给根实例添加一个属性 _route,值就是当前的 current对象,并将 this._route变成了响应式对象(数据变化应该会引起页面重新渲染)
            // 注意!!!current 改变并不会触发 _route的改变,我们需要在 current变化时手动更新 this._route的值
            // let current = {}; let _route = current; current = {name: '新的'}; _route 仍然是 {}
            Vue.util.defineReactive(this, '_route', this._router.history.current)
    
            // this._router 可以拿到路由实例
            // this._route 可以拿到current对象
          } else {
            // 在所有后代组件上都增加 _routerRoot,其指向根实例
            this._routerRoot = this.$parent && this.$parent._routerRoot
          }
        }
      })
    
      // 代理实例上的 $router 属性,this.$router
      Object.defineProperty(Vue.prototype, '$router', {
        get () {
          return this._routerRoot && this._routerRoot._router
        }
      })
      // 代理实例上的 $route 属性,this.$route
      Object.defineProperty(Vue.prototype, '$route', {
        get () {
          return this._routerRoot && this._routerRoot._route
        }
      })
    
      // 注册 router-link 全局组件
      Vue.component('router-link', routerLink)
      // 注册 router-view 全局组件
      Vue.component('router-view', routerView)
    }
    export default install

    在 install 方法中,我们主要做了 3 件事 !

    1. 我们利用 Vue.mixin 将 beforeCreate 生命周期钩子注入到了每一个组件中,并在组件自身钩子之前调用

      在 beforeCreate 钩子中,我们将根实例 _routerRoot 共享给了所有的后代组件;给根实例添加了一个 _router 属性(VueRouter实例)并调用了 router 的初始化方法 this._router.init();然后用 defineReactive 方法给根实例添加了一个响应式属性 _route(当前路由对象)

    2. 在 Vue 原型上代理了 $router$route 属性,这就是为什么我们可以在任何组件内通过 this.$router 访问路由器、通过 this.$route 访问当前路由

    3. 通过 Vue.component 注册了全局组件

    下一节我们将分析一下 VueRouter 对象的实现和它的初始化工作

    2. VueRouter 类

    VueRouter 的实现是一个类,在入口文件 src/index.js中定义

    import install from './install'
    import createMatcher from './create-matcher'
    import HashHistory from './history/hash'
    import Html5History from './history/html5'
    
    class VueRouter {
      constructor (options) {
        // 用户传递的路由配置
        const routes = options.routes || []
        this.beforeEachHooks = []
    
        // 路由匹配器,可以匹配也可以添加新的路由
        this.matcher = createMatcher(routes)
    
        const mode = options.mode || 'hash'
        if (mode === 'hash') {
          this.history = new HashHistory(this) // popstate, hashchange
        } else if (mode === 'history') {
          this.history = new Html5History(this) // popstate
        }
      }
    
      // 路由守卫,缓存回调钩子,在 transitionTo方法中执行回调钩子
      beforeEach (cb) {
        this.beforeEachHooks.push(cb)
      }
    
      // router初始化方法(只会在 根vue实例中的 beforeCreate钩子中调用一次)
      init (app) {
        console.log('router初始化方法(init)')
        const history = this.history
        // 手动根据当前路径去匹配对应的组件,渲染,之后监听路由变化
        history.transitionTo(history.getCurrentLocation(), () => {
          history.setupListener()
        })
    
        // 在 transitionTo 方法中执行这个回调,目的就是在 current变化时手动更新 app._route的值,数据变化会自动重新渲染视图
        history.listen((newRoute) => {
          app._route = newRoute
        })
      }
    
      // 简化用户调用层级  this.match ≈ this.matcher.match
      match (location) {
        return this.matcher.match(location)
      }
    
      // 调用 HashHistory or Html5History 的跳转逻辑(点击router-link触发)
      push (location) {
        // 针对hash模式: window.location.hash
        // 针对history模式: history.pushState
        return this.history.push(location)
      }
    }
    
    // 当执行 Vue.use(VueRouter) 时,如果 VueRouter插件是一个对象,必须提供 install方法,install方法调用时,会将 Vue作为参数传入
    VueRouter.install = install
    
    export default VueRouter

    constructor

    我们先来分析一下 constructor构造函数,看看当我们 new VueRouter({}) 时执行了哪些操作

    在构造函数中,我们定义了一些私有属性。 this.beforeEachHooks 属性用来记录路由守卫钩子回调(最终在 transitionTo 方法中执行回调,后面会详细介绍);this.matcher 属性代表路由匹配器对象createMatcher() 方法返回了 match、addRoute、addRoutes 等方法,可以匹配、添加新的路由(我们会在 transitionTo 方法中应用 match,后面会详细介绍);this.history 属性代表路由历史实例,是具体执行各种路由操作的执行者,其根据选项模式 options.mode 的不同,去 new 了一个相对应的 History 实例 HashHistory or Html5History(后面会详细介绍)

    tip:transitionTo 方法的实现是在 src/history/base.js 中。他负责处理所有的跳转逻辑,会根据路径匹配对应的路由记录,更新 app._route(响应式对象)为最新的路由对象,从而触发 setter 劫持,通知 去渲染新的组件(先有个大概认知,后面会详细介绍)

    init

    触发时机:还记得吗?在前面 VueRouter.install 方法中,我们利用 Vue.mixin 将 beforeCreate 生命周期钩子注入到了每一个组件中。我们在 new Vue({}) 根实例时,会把 router 路由器注入到根实例,所以我们只会在根vue实例中的 beforeCreate 钩子中调用一次router.init方法

    src/install.js:

    beforeCreate () {
      if (this.$options.router) {
        this._routerRoot = this // 根实例
        this._router = this.$options.router // router路由实例
    
        this._router.init(this) // this 就是我们的根应用 new Vue()
    
        Vue.util.defineReactive(this, '_route', this._router.history.current)
      } else {
        this._routerRoot = this.$parent && this.$parent._routerRoot
      }
    }

    我们在 router 初始化方法中主要做了两件事!

    1. 执行 history.transitionTo 方法,根据当前路径去匹配对应的组件,并渲染。然后通过 history.setupListener 在回调中添加路由监听器,当路由变化时,我们就可以做一些事情了!当然,不同的路由模式有不同的 setupListener 实现(后面会详细介绍)
    history.transitionTo(history.getCurrentLocation(), () => {
      history.setupListener()
    })
    1. 执行 history.listen 方法,缓存更新 app._route 的回调。后续我们会在 transitionTo 方法中执行这个回调,目的就是在 current 对象变化时手动更新一下 app._route 的值。
    history.listen((newRoute) => {
      app._route = newRoute
    })

    我们之前用 defineReactive 方法将根实例上的 _route 属性变成了一个响应式对象,其数据变化后,会触发 setter 劫持,通知 去自动渲染新的组件

    tip:为什么要手动更新 app._route 的值呢?因为 current 改变并不会触发 _route 的改变,所以我们需要在 current 变化时手动更新一下 _route 的值,看个小例子就明白了

    // Vue.util.defineReactive(this, '_route', this._router.history.current)
    
    let current = {}; let _route = current; current = {name: '新值'}; // _route 仍然是 {}

    push

    当我们通过 跳转路由时会触发此方法,内部调用了 history 对象(路由历史实例)的 push 方法,不同的 mode 选项有不同的实现方式

    hash模式下,我们通过 transitionTo 方法匹配渲染新的组件,然后通过 history.pushState/location.hash (优雅降级处理)往路由栈中添加一条路由记录(transitionTo 方法后面会详细介绍)

    HashHistory 类在 src/history/hash.js中实现

    const supportsPushState = window.history && typeof window.history.pushState === 'function'
    
    class HashHistory extends Base {
      push (location) {
        this.transitionTo(location, () => {
          if (supportsPushState) {
            window.history.pushState({}, '', getUrl(location))
          } else {
            window.location.hash = location
          }
        })
      }
    }

    history模式下,我们也是通过 transitionTo 方法匹配渲染新的组件,然后通过 history.pushState 往路由栈中添加一条路由记录

    Html5History 类在 src/history/html5.js中实现

    class HTML5History extends Base {
      push (location) {
        this.transitionTo(location, () => {
          window.history.pushState({}, '', location)
        })
      }
    }

    下一节我们将分析一下 matcher 路由匹配器的实现

    3. matcher

    create-matcher

    createMatcher 方法返回了 match、addRoute、addRoutes 等方法,可以匹配、添加新的路由。他的定义在 src/create-matcher.js

    其中 match 方法可以根据一个 location 路径,去 createRouteMap 方法返回的 pathMap 路由映射表中匹配到对应的路由信息

     export default function createMatcher (routes) {
      // pathList:收集所有的路由路径,['/', '/a', '/b', '/about', '/about/a', '/about/b']
      // pathMap:收集路径的对应路由记录,['/':{/的记录}, '/a':{/a的记录}, '/b':{/b的记录}, '/about':{/about的记录}, ...]
      const { pathList, pathMap } = createRouteMap(routes)
    
      // 动态添加多个路由规则 在v4.x中已废弃:使用 router.addRoute() 代替
      function addRoutes (routes) {
        createRouteMap(routes, pathList, pathMap)
      }
      // 动态添加一条新路由规则
      function addRoute (route) {
        createRouteMap([route], pathList, pathMap)
      }
      // 根据一个路径获取对应的路由信息 在v4.x中已废弃:删除 router.match 改为 router.resolve
      function match (location) {
        return pathMap[location]
      }
    
      return {
        addRoutes,
        addRoute,
        match
      }
    }

    createMatcher 触发时机:VueRouter 类的 constructor 构造函数中,我们定义了一些私有属性,其中就包括this.matcher 路由匹配器对象。VueRouter 类在 src/index.js 中实现

    class VueRouter {
      constructor (options) {
        // 用户传递的路由配置
        const routes = options.routes || []
        this.matcher = createMatcher(routes)
          ...
      }
    
      // 简化用户调用层级  this.match ≈ this.matcher.match
      match (location) {
        return this.matcher.match(location)
      }
    }

    match 应用时机:我们会在 transitionTo 方法中运行 match 方法,用以匹配对应的路由信息。然后更新 app._route(响应式对象)为最新的路由对象,从而触发 setter 劫持,通知 去渲染新的组件

    create-route-map

    createMatcher 创建路由匹配器时,会用到 createRouteMap 方法去创建路由映射关系,它的定义在 src/create-route-map.js

    该方法根据用户传入的 routes选项,返回了 pathList(收集所有的路由路径) 和 pathMap(收集路径的对应路由记录,这就是我们的路由映射表) 2个对象

     // @return pathList:收集所有的路由路径,['/', '/a', '/b', '/about', '/about/a', '/about/b']
     // @return pathMap:收集路径的对应路由记录,['/':{/的记录}, '/a':{/a的记录}, '/b':{/b的记录}, '/about':{/about的记录}, ...]
    export default function createRouteMap (routes, pathList, pathMap) {
      // 当第一次加载的时候没有 pathList 和 pathMap
      pathList = pathList || []
      pathMap = pathMap || {}
      routes.forEach(route => {
        addRouteRecord(route, pathList, pathMap)
      })
    
      return {
        pathMap
      }
    }
    
    // 添加路由信息
    function addRouteRecord (route, pathList, pathMap, parentRecord) {
      const path = parentRecord ? `${parentRecord.path}${parentRecord.path.endsWith('/') ? '' : '/'}${route.path}` : route.path
      const record = {
        path,
        component: route.component,
        props: route.props,
        meta: route.meta,
        parent: parentRecord
      }
    
      // 维护路径对应的属性
      if (!pathMap[path]) {
        pathList.push(path)
        pathMap[path] = record
      }
    
      route.children && route.children.forEach(childRoute => {
        addRouteRecord(childRoute, pathList, pathMap, record)
      })
    }
    

    下一节我们将分析一下 HashHistory(hash模式)、HTML5History(history模式)对象的实现

    4. 路由模式

    HashHistory 和 Html5History 的实现是两个类,他们均继承自 base 基类,由于 base 基类中主要是路由跳转相关的逻辑,我们打算在下一章节和路由组件、导航守卫一起分析,它在 src/history/base.js中定义

    HashHistory

    hash 模式,优先使用 history.pushState/repaceState API 来完成 URL 跳转和 onpopstate 事件监听路由变化,不支持再降级为 location.hash API 和 onhashchange 事件。在src/history/hash.js中定义

    import Base from './base'
    
    const supportsPushState = window.history && typeof window.history.pushState === 'function'
    
    class HashHistory extends Base {
      constructor (router) {
        super(router)
    
        // 初始化 hash路由时,给一个默认的 hash路径 /
        ensureSlash()
      }
    
      // 获取hash路径片段 http://192.168.21.144/framework-assets#/assets/1522392838?id=1522392838 => '/assets/1522392838?id=1522392838'
      getCurrentLocation () {
        return getHash()
      }
    
      // 添加监听器,监听hash值的变化(在 vueRouter类的init方法中调用)
      // 当用户在浏览器点击后退、前进,或者在js中调用HTML5 history API【history.back(),history.go(),history.forward()】等,会触发 popstate事件 和 hashchange事件
      // 用户通过 location.hash = 'xxx' 也会触发 popstate事件 和 hashchange事件
      // 但 history.pushState(),history.replaceState()不会触发这两个事件!!!
      setupListener () {
        const eventType = supportsPushState ? 'popstate' : 'hashchange'
        window.addEventListener(eventType, () => {
          this.transitionTo(getHash()) // 初始化执行的 ensureSlash方法也会触发此回调
        })
      }
    
      // 跳转页面
      // 为什么要手动执行 transitionTo,而不是直接改变地址,通过路由监听器去间接执行 transitionTo?
      // 因为 window.history.pushState() 不会触发 popstate事件!!!
      push (location) {
        this.transitionTo(location, () => {
          if (supportsPushState) {
            window.history.pushState({}, '', getUrl(location))
          } else {
            window.location.hash = location
          }
        })
      }
    }
    
    // http://localhost:8080/  ==>  http://localhost:8080/#/
    function ensureSlash () {
      if (window.location.hash) {
        return
      }
      window.location.hash = '/'
    }
    
    // 获取当前hash值(去掉 #)
    // '#/assets/1522392838?id=1522392838'  ==>  '/assets/1522392838?id=1522392838'
    function getHash () {
      return window.location.hash.slice(1)
    }
    
    // 绝对路径
    function getUrl (path) {
      const href = window.location.href
      const i = href.indexOf('#')
      const base = i >= 0 ? href.slice(0, i) : href
      return `${base}#${path}`
    }
    
    export default HashHistory

    ensureSlash

    当我们实例化一个 history 对象时,会默认在 constructor 构造函数中执行 ensureSlash 方法,如果没有hash 值的话就给一个默认的 hash 路径 /,确保存在 hash 锚点

    其作用就是将 http://localhost:8080/ 自动修改为 http://localhost:8080/#/

    setupListener

    添加路由监听器,当 hash 值变化时调用 transitionTo 方法统一处理跳转逻辑。事件注册采用了降级处理,优先使用 onpopstate 事件,若不支持,则降级使用 onhashchange 事件

    当用户点击浏览器的后退、前进按钮,在 js 中调用 HTML5 history API,如 history.back()history.go()history.forward(),或者通过 location.hash = 'xxx' 都会触发 popstate 事件 和 hashchange 事件
    需要注意的是调用 history.pushState() 或者 history.replaceState() 不会触发 popstate 事件 和 hashchange 事件

    触发时机:在 vueRouter 类的 init 方法中调用

    class VueRouter {
      // router初始化方法(只会在 根vue实例中的 beforeCreate钩子中调用一次)
      init (app) {
        const history = this.history
        // 手动根据当前路径去匹配对应的组件,渲染,之后监听路由变化
        history.transitionTo(history.getCurrentLocation(), () => {
          history.setupListener()
        })
    
        ...
      }
    }

    push

    跳转页面,手动调用 transitionTo 方法去处理跳转逻辑,并在回调中通过 history.pushStatelocation.hash 向路由栈添加一条路由记录,更新地址栏 URL

    触发时机:当我们通过 跳转路由时会触发根实例上的 app._router.push() 方法( VueRouter 类中的 push 方法),其内部就调用了该方法,即 history 对象(路由历史实例)的 push 方法,当然,不同的 mode 选项有不同的实现方式

    class VueRouter {
      // 调用 HashHistory or Html5History 的跳转逻辑(点击router-link触发)
      push (location) {
        // 针对hash模式: history.pushState、不支持再降级为 window.location.hash
        // 针对history模式: history.pushState
        return this.history.push(location)
      }
    }

    为什么要手动执行 transitionTo 方法?而不是通过 history.pushStatelocation.hash 改变路由栈,然后利用 onpopstate 事件 或 onhashchange 事件去间接执行 transitionTo 方法呢 ❓

    答:因为 history.pushState 不会触发 onpopstate 事件 ❗ 这里引出了一个问题,如果我们手动执行了 transitionTo 方法,然后在回调中用 location.hash 改变路由栈,就又会通过 onhashchange 事件再次执行 transitionTo 方法,这里重复执行了 2 遍,所以我们要在 transitionTo 内部做去重处理(后面会详细介绍其去重逻辑)

    Html5History

    history 模式,使用 history.pushState/repaceState API 来完成 URL 跳转,使用onpopstate 事件监听路由变化。在src/history/html5.js中定义

    import Base from './base'
    
    class HTML5History extends Base {
      constructor (router) {
        super(router)
      }
    
      // 添加监听器,监听pathname变化(在 vueRouter类的init方法中调用)
      // 当用户在浏览器点击后退、前进,或者在js中调用 history.back(),history.go(),history.forward()等,会触发popstate事件
      // 但 pushState、replaceState不会触发这个事件
      setupListener () {
        window.addEventListener('popstate', () => {
          this.transitionTo(window.location.pathname)
        })
      }
    
      // 获取pathname  http://192.168.21.144/framework-assets#/assets/1522392838?id=1522392838 => '/framework-assets'
      getCurrentLocation () {
        return window.location.pathname
      }
    
      // 跳转页面
      // 为什么要手动执行 transitionTo,而不是直接改变地址,通过路由监听器去间接执行 transitionTo?
      // 因为 window.history.pushState() 不会触发 popstate事件!!!
      push (location) {
        this.transitionTo(location, () => {
          window.history.pushState({}, '', location)
        })
      }
    }
    
    export default HTML5History

    setupListener

    添加路由监听器,当激活同一文档中不同的历史记录条目时,调用 transitionTo 方法统一处理跳转逻辑。使用了 onpopstate 事件监听路由变化

    触发时机:在 vueRouter 类的 init 方法中调用

    调用 history.pushState() 或者 history.replaceState() 不会触发 popstate 事件

    push

    跳转页面,手动调用 transitionTo 方法去处理跳转逻辑,并在回调中通过 history.pushState 向路由栈添加一条路由记录,更新地址栏 URL

    触发时机:当我们通过 跳转路由时会触发根实例上的 app._router.push() 方法( VueRouter 类中的 push 方法),其内部就调用了该方法

    下一节我们分析一下路由切换到底做了哪些工作

    5. 路由切换

    当我们点击 进行路由切换时,会通过 push 方法调用 base 基类中的 transitionTo 方法处理跳转逻辑,然后触发一系列的导航守卫钩子,如果全部钩子都执行完了,就会更新根实例上的 _route 响应式属性,通知 去渲染新的组件

    ok!我们具体分析下每一步都是如何实现的

    全局组件的注册是在 VueRouter 类上的 install 方法中,其组件实现是在 src/components/link.js

    点击 时,会调用根实例上的 app._router.push() 方法(即 VueRouter 类中的 push 方法),其内部调用了 history 对象(路由历史实例)的 push 方法(即 HashHistory 类或 Html5History 类或中的 push 方法),内部手动调用 transitionTo 方法去处理跳转逻辑,并在回调中通过 history.pushState 或 location.hash 向路由栈添加一条路由记录,更新地址栏 URL

    export default { 
      props: {
        to: { type: String, required: true },
        tag: { type: String, default: 'a' }
      },
      methods: {
        handler () {
          this.$router.push(this.to)
        }
      },
      render () {
        const tag = this.tag
        return <tag style={ { cursor: 'pointer' } } onClick={this.handler}>{this.$slots.default}tag>
    }
    }
    

    接下来我们先看下导航守卫,然后一起分析 base 基类中 transitionTo 方法的内部实现

    beforeEach

    这里,我们只介绍全局前置守卫 beforeEach。用户可以注册多个全局前置守卫

    const router = new VueRouter({ ... })
    
    router.beforeEach((to, from, next) => {
      // ...
      // 返回 false 以取消导航
      return false
    })
    
    router.beforeEach((to, from) => {
      // ...
      // 返回 false 以取消导航
      return false
    })

    当一个导航触发时,全局前置守卫按照创建顺序调用。守卫是异步解析执行,此时导航在所有守卫 resolve 完之前一直处于等待中。守卫方法接收三个参数

    • to: 即将要进入的目标路由
    • from: 当前导航正要离开的路由
    • next: 进行管道中的下一个守卫钩子

    我们需要用 beforeEachHooks 数组来收集用户注册的守卫钩子,后续会在 transitionTo 方法中依次执行

    class VueRouter {
      constructor (options) {
        this.beforeEachHooks = []
        ...
      }
    
      // 路由守卫,缓存回调钩子,在 transitionTo方法中执行回调钩子
      beforeEach (cb) {
        this.beforeEachHooks.push(cb)
      }
    }

    注意!只有当全部钩子都执行完了,才会去渲染新的路由组件

    transitionTo (location, listener) {
    	const record = this.router.match(location) // 匹配路由记录
      const route = createRoute(record, { path: location }) // 生成路由对象
      
      const queue = [].concat(this.router.beforeEachHooks)
      runQueue(queue, this.current, route, () => {
        this.current = route // 更新当前的 current对象, 稍后我们就可以切换页面显示
        listener && listener() // 添加路由监听器 or 更改地址栏url
        this.cb && this.cb(route) // 更新 app._route
      })
    }
    
    function runQueue (queue, from, to, cb) {
      function step (index) {
        if (index >= queue.length) return cb()
        const hook = queue[index] // hook就是我们的钩子方法
        hook(from, to, () => step(index + 1)) // 第三个参数就是 next方法
      }
      step(0)
    }

    base

    base 是 HashHistory 和 Html5History 的基类,主要负责统一处理路由跳转逻辑,它在 src/history/base.js中定义。让我们重点来分析下 transitionTo 的内部实现

    class Base {
      constructor (router) {
        this.router = router
        this.current = createRoute(null, {
          path: '/'
        })
      }
    
      // 缓存更新_route的回调(this._route = route)
      listen (cb) {
        this.cb = cb
      }
    
      // 所有的跳转逻辑都在这个方法中实现
      // 根据路径匹配对应的路由记录,然后更新当前的 current对象 和 app._route对象。
      // 我们之前用 defineReactive 将 app._route 变成了响应式对象,app._route发生变化后,会触发 setter 劫持,通知 router-view 重新渲染新路径的组件
      transitionTo (location, listener) {
        // 根据一个路径匹配对应的路由信息
        const record = this.router.match(location)
    
        const route = createRoute(record, { path: location })
    
        // 去重:当前跳转的路径location 和 我们之前存的current.path 相同,而且匹配结果也相同(初始化path:'/'需要额外判断匹配结果matched),则不再跳转了
        if (location === this.current.path && route.matched.length === this.current.matched.length) {
          return
        }
        // 如果是 hash模式,并且使用 hashchange监听路由时,初始化页面 和 通过route-link跳转页面时
        // transitionTo方法执行了两次(此处打印了两遍),需要去重处理,当前跳转的路由location 和 上次的跳转的路由(v3中实现此属性)作比较;若一致,则return
        console.log('transitionTo(record)', record, route)
    
        const queue = [].concat(this.router.beforeEachHooks) // 我们可能有多个钩子
        runQueue(queue, this.current, route, () => {
          this.current = route // 更新当前的 current对象, 稍后我们就可以切换页面显示
    
          // 添加路由监听器 or 更改地址栏url
          listener && listener()
    
          // 更新 app._route
          this.cb && this.cb(route)
        })
      }
    }
    
    export default Base
    
    /**
     * @desc 根据树形结构record路由信息 返回一个 扁平化的上下级路由数据
     * 返回示例:{path:'/', matched:[]}
     * 返回示例:{path:'/about/a', matched:[aboutRecord, aboutARecord]}
     */
    function createRoute (record, location) {
      const matched = []
      if (record) {
        while (record) {
          matched.unshift(record) // [about, about/a]
          record = record.parent
        }
      }
      return {
        ...location,
        matched
      }
    }
    
    /**
     * @name 执行路由守卫钩子
     * @desc 如果有多个beforeEach钩子,只有在上一个钩子中执行了next方法,我们才会运行下一个钩子
     * @desc 只要有一个钩子未执行next方法,则终止(后续的钩子、跳转逻辑均不执行)
     */
    function runQueue (queue, from, to, cb) {
      function step (index) {
        if (index >= queue.length) return cb()
        const hook = queue[index] // hook就是我们的钩子方法
        hook(from, to, () => step(index + 1)) // 第三个参数就是 next方法
      }
      step(0)
    }

    让我们根据一个具体场景去进行分析,假如我们要跳转http://localhost:8080/about/a URL,路由配置如下:

    const routes = [
      {
        path: '/about',
        name: 'About',
        component: About,
        children: [
          {
            path: 'a',
            component: {
              render: (h) => <h2>about ah2>
            }
          },
        ]
      },
      ...
    ]
    1. 首先会通过 router.match() 根据路径 /about/a 去匹配对应的路由记录 record,match 方法是之前由 createMatcher 生成的,record 路由记录结构如下,此处仅展示部分属性
    {
      "path": "/about/a",
      "component": { about/a 组件定义 },
      "parent": {
        "path": "/about",
        "component": { about 组件定义 }
      }
    }
    1. 然后通过 createRoute() 生成一个扁平化的上下级路由数据 route,这就是我们常用的 this.$route 路由对象,route 对象格式如下,此处仅展示部分属性
    {
      "path": "/about/a",
      "matched": [
      	{
      		"path": "/about",
      		"component": { about 组件定义 }
    		},
    		{
      		"path": "/about/a",
      		"component": { about/a 组件定义 },
      		"parent": {
        			"path": "/about",
        			"component": { about 组件定义 }
      		}
    		}
    	]	
    }
    1. 跳转一次路由有可能会多次执行 transitionTo 方法,所以我们需要做去重处理,如果当前跳转的路径和我们之前缓存的相同,则 return
    2. 如果全部导航守卫钩子都执行完了,则更新当前的 current 对象,并更新根实例上的 _route 响应式对象,然后通知 去渲染新的路由组件,至于 是如何去渲染新组件的,我们下一章节再去分析

    router-view

    全局组件的注册也是在 VueRouter 类上的 install 方法中,其组件实现是在 src/components/view.js

    export default {
      functional: true,
      render (h, { parent, data }) {
        // 默认先渲染 app.vue中的 router-view;再渲染 Home 或 About中的 router-view
    
        data.routerView = true // 标识该组件是通过 route-view 渲染出来的
        const route = parent.$route // install.js中代理的$route
    
        let depth = 0
        while (parent) {
          // $vnode 指的是组件本身虚拟DOM
          if (parent.$vnode && parent.$vnode.data.routerView) {
            depth++
          }
          parent = parent.$parent // 不停的向上查找父组件
        }
    
        // matched是一个包含上下父子路由记录的数组,格式如下:[aboutRecord, aboutARecord]
        const record = route.matched[depth]
    
        // 没有匹配到组件直接return
        if (!record) {
          return h()
        }
    
        return h(record.component, data)
      }
    }
    

    那么跳转路由后,又是如何知道去渲染新组件的呢?何时渲染?渲染哪一个组件?

    先来看第一个问题,何时去渲染?

    我们之前用 defineReactive 方法将根实例上的 _route 属性变成了一个响应式对象,并在 Vue 原型上代理了 $route 属性。而 组件的 render 函数引用了 $route 属性,所以当 _route 变化后,会触发 setter 劫持,通知 去自动渲染新的组件

    beforeCreate () {
      if (this.$options.router) {
        Vue.util.defineReactive(this, '_route', this._router.history.current)
      }
    }
    
    Object.defineProperty(Vue.prototype, '$route', {
      get () {
        return this._routerRoot && this._routerRoot._route
      }
    })

    第二个问题,他怎么知道渲染哪个组件?

    $route 对象的 matched 属性是一个包含上下父子路由记录的数组,在 transitionTo 方法中被创建

    让我们看一个具体的路由配置:

    const routes = [
      {
        path: '/about',
        name: 'About',
        component: About,
        children: [
          {
            path: 'a', // children中路径不能增加 /
            component: {
              render: (h) => <h2>about ah2>
            }
          },
        ]
      },
    	...
    ]

    当我们访问 http://localhost:8080/about/a URL时, _route 对象(即 this.$route)格式如下:

    {
      "path": "/about/a",
    	"matched": [
      	{
        	"path": "/about",
        	"component": { about 组件定义 }
      	},
      	{
        	"path": "/about/a",
        	"component": { about/a 组件定义 },
        	"parent": {
          	"path": "/about",
          	"component": { about 组件定义 }
        	}
      	}
    	]
    }

    中 render 函数执行时,我们会不停的向上查找父组件,看是否有 routerView 标识,若存在,则索引深度 depth + 1,然后渲染 h(route.matched[depth].component, data)

    组件的渲染是树状的,默认先渲染 app.vue 中的 h(route.matched[0].component, data),即 About 组件;再渲染 About 中的 h(route.matched[1].component, data),即 About/a 组件

    6. 参考链接

    浅显易懂的vue-router源码解析(一)

    vue-router 源码分析 - 李宇仓 | Li Yucang

    珠峰公开课 | vue-router 源码


    __EOF__

  • 本文作者: 柏成
  • 本文链接: https://www.cnblogs.com/burc/p/17482180.html
  • 关于博主: 评论和私信会在第一时间回复。或者直接私信我。
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
  • 声援博主: 如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。
  • 相关阅读:
    【Jmeter+Influxdb+Grafana性能监控平台安装与部署】
    算法日常训练12.4(最接近目标价格甜点成本)
    【JavaEE】网络编程---TCP数据报套接字编程
    深入浅出nginx
    探店通系统源码,短视频矩阵源码,抖音矩阵系统源码。
    调用网络时报错name weight already exists
    Oracle database oracle12c RAC 增加PDB
    Spring MVC框架看这篇就够了
    vue 前端参值后端接收的几种方式
    科技云报道:都想当中国VMware,但用户体验拉平了吗?
  • 原文地址:https://www.cnblogs.com/burc/p/17482180.html