Vue-Router实现原理

2,854 阅读2分钟

如果对大佬有帮助,请给小弟一个赞哦。

简述

单页面的优缺点不在本文讲,只讲原理。实现单页面的功能有两种方式:

  1. 使用锚点,hashHistory
  2. 利用浏览器的browserHistory原理

hashHistory原理

添加hashchange监听

window.addEventListener(
    'hashchange',
    function() {
        // hash改变时,进行页面更换
    }
)

改变hash

window.location.hash = 'aaa';

browserHistory原理

添加popstate监听,可以监听到浏览器前进、后退。但不能监听到pushState、replaceState,所以在执行pushState、replaceState的时候进行页面更换。

window.addEventListener(
    'popstate',
    function() {
        // url改变时,进行页面更换
    }
)

改变url,pushState、replaceState的具体文档可以去看MDN。

history.pushState({}, '', path);
history.replaceState({}, '', path);

Vue-Router的实践

一、Router类收集数据,确定路由类型

  1. mode:确定采用哪种方式的路由;对HTML5History或HashHistory进行实例化。
  2. routes:收集所有的路由信息;
class Router {
    constructor(options) {
        this.options = options;
        let mode = options.mode || 'hash';
        this.routes = options.routes;
        switch (mode) {
            case 'history':
              this.history = new HTML5History(this, options.base)
              break
            case 'hash':
              this.history = new HashHistory(this, options.base, this.fallback)
              break
            default:
              // 报错
          }
    }
    ...
}

二、HTML5History

  1. setupListeners方法会监听popstate,获取到当前的path
  2. push方法改变路由
class HTML5History {
    constructor (route, base) {
        this.current = '';
    }
    setupListeners() {
        const handleRoutingEvent = () => {
            this.current = location.pathname?.slice(1) || '/';
        }
        window.addEventListener(
            'popstate',
            handleRoutingEvent
        )
    }
    push (path) {
        history.pushState({}, '', path);
    }
}

三、HashHistory

  1. setupListeners方法会监听hashchange,获取到当前的path
  2. push方法改变路由
class HashHistory {
    constructor (route, base) {
        this.current = '';
    }
    setupListeners() {
        const handleRoutingEvent = () => {
            this.current = location.hash?.slice(1) || '/';
        }
        window.addEventListener(
            'hashchange',
            handleRoutingEvent
        )
    }
    push (location) {
        window.location.hash = location
    }
}

四、触发setupListeners

Router中会有一个init初始化,用来执行监听;push方法执行路由修改。

class Router {
    init() {
        this.history.setupListeners()
    }
    push (location) {
        this.history.push(location)
    }
}

为保障阅读体验,先断开,讲一下vue怎样使用插件,然后回来继续写Vue-Router

使用Router(看main.js)

会发现router出现在app.$options中

const app = new Vue({
  el: '#app',
  router,
  store,
  render: h => h(App)
})
console.log(app)

理解Vue.use

Vue.use(Router)执行,回调用插件Router的静态方法install,并把Vue当作参数传进去,所以我们的Router需要一个install的静态方法。

import Router from '@/utils/router'
Vue.use(Router)

五、静态方法install

  1. 在组件实例化前beforeCreate阶段,把实例赋值给this._routerRoot,并执行init,触发上面的步骤四
  2. 设置劫持,访问this.$router,返回this._routerRoot._router
  3. 新增全局组件router-view,用来渲染路由组件
Router.install = function(Vue) {
    Vue.mixin({
        beforeCreate () {
            if (this.$options.router) {
                this._routerRoot = this
                this._router = this.$options.router
                this._router.init(this)
            } else {
                this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
            }
        }
    })
    Object.defineProperty(Vue.prototype, '$router', {
        get () { return this._routerRoot._router }
    })
    Vue.component('router-view',{
        render(h){
            return h(com) // com是内容组件
        }
    })
    
}

目前我们设置了路由监听,注册了router-view组件,那么路由改变时,怎样把对应的组件渲染近router-view组件呢?

六、router-view内容切换

  1. 在外部作用域声明一个_Vue,在install执行时把Vue赋值给_Vue。
let _Vue;

Router.install = function(Vue) {
    _Vue = Vue;
}
  1. HashHistory、HTML5History使用_Vue创建实例
class HashHistory {
    constructor (route, base) {
        this.current = '';
        this.app = new _Vue({
            data() {
                return {
                    path: '/'
                }
            },
        })
    }
}
class HTML5History {
    constructor (route, base) {
        this.current = '';
        this.app = new _Vue({
            data() {
                return {
                    path: '/'
                }
            },
        })
    }
}
  1. router-view组件使用步骤2创建的实例中的path,对所有路由信息筛选,找到对应的组件,作为内容。因为router-view组件用到了app.path,就添加了依赖。当app.path改变时,router-view组件重新执行。
Router.install = function(Vue) {
    ...
    Vue.component('router-view',{
        render(h){
            const path = this._routerRoot._router.history.app.path;
            const routes = this._routerRoot._router.routes;
            const route = routes.find((i) => i.path === `/${path}`)
            const com = route ? route.component : routes.find((i) => i.path === `/404`).component
            return h(com)
        }
    })
    
}
  1. 监听到路由变化,改变this.app.path,使router-view组件重新执行。pushState不能触发popstate监听,所有单独修改this.app.path
class HTML5History {
    ...
    setupListeners() {
        const handleRoutingEvent = () => {
            this.current = location.pathname?.slice(1) || '/';
            this.app.path = this.current;
        }
        window.addEventListener(
            'popstate',
            handleRoutingEvent
        )
    }
    push (path) {
        history.pushState({}, '', path);
        this.app.path = path;
    }
}
class HashHistory {
    ...
    setupListeners() {
        const handleRoutingEvent = () => {
            this.current = location.hash?.slice(1) || '/';
            this.app.path = this.current;
        }
        window.addEventListener(
            'hashchange',
            handleRoutingEvent
        )
    }
    push (location) {
        window.location.hash = location
    }
}

完整代码

let _Vue;



class HTML5History {
    constructor (route, base) {
        this.current = '';
        this.app = new _Vue({
            data() {
                return {
                    path: '/'
                }
            },
        })
    }
    setupListeners() {
        const handleRoutingEvent = () => {
            this.current = location.pathname?.slice(1) || '/';
            this.app.path = this.current;
        }
        window.addEventListener(
            'popstate',
            handleRoutingEvent
        )
    }
    push (path) {
        history.pushState({}, '', path);
        this.app.path = path;
    }
}
class HashHistory {
    constructor (route, base) {
        this.current = '';
        this.app = new _Vue({
            data() {
                return {
                    path: '/'
                }
            },
        })
    }
    setupListeners() {
        const handleRoutingEvent = () => {
            this.current = location.hash?.slice(1) || '/';
            this.app.path = this.current;
        }
        window.addEventListener(
            'hashchange',
            handleRoutingEvent
        )
    }
    push (location) {
        window.location.hash = location
    }
}
class Router {
    constructor(options) {
        this.options = options;
        let mode = options.mode || 'hash';
        this.routes = options.routes;
        switch (mode) {
            case 'history':
              this.history = new HTML5History(this, options.base)
              break
            case 'hash':
              this.history = new HashHistory(this, options.base, this.fallback)
              break
            default:
              // 报错
          }
    }
    init() {
        this.history.setupListeners()
    }
    push (location) {
        this.history.push(location)
    }
}
Router.install = function(Vue) {
    _Vue = Vue;
    Vue.mixin({
        beforeCreate () {
            if (this.$options.router) {
                this._routerRoot = this
                this._router = this.$options.router
                this._router.init(this)
            } else {
                this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
            }
        }
    })
    Object.defineProperty(Vue.prototype, '$router', {
        get () { return this._routerRoot._router }
    })

    Object.defineProperty(Vue.prototype, '$route', {
        get () { return this._routerRoot._route }
    })
    Vue.component('router-view',{
        render(h){
            const path = this._routerRoot._router.history.app.path;
            const routes = this._routerRoot._router.routes;
            const route = routes.find((i) => i.path === `/${path}`)
            const com = route ? route.component : routes.find((i) => i.path === `/404`).component
            return h(com)
        }
    })
    
}
export default Router;