Vue-router 原理

669 阅读1分钟

原文:https://www.cnblogs.com/xuzhudong/p/8869699.html
前端应用的主流形式:单页应用(SPA) 大型的单页应用最显著的特点之一:采用前端路由系统,通过改变URL,在不重新请求页面的情况下,更新页面视图

更新视图但不重新请求页面是前端路由原理的核心之一,目前唉浏览器环境中这一功能的实现主要有两种方式:

  • 利用URL中的hash("#")
  • 利用History interface在html5中新增的方法

h5 history api:https://developer.mozilla.org/zh-CN/docs/Web/API/History

vue-router是Vue.js框架的路由插件,下面我们从它的源码入手,了解下vue-router 是如何通过这两种方式实现前端路由的

模式参数

在vue-router中是通过mode这一参数控制路由的实现模式的:


const router = new VueRouter({
    mode: 'history',
    routes: [...]
})

vue-router的实际源码:


export default class VueRouter {
 
 mode: string; // 传入的字符串参数,指示history类别
 history: HashHistory | HTML5History | AbstractHistory; // 实际起作用的对象属性,必须是以上三个类的枚举
 fallback: boolean; // 如浏览器不支持,'history'模式需回滚为'hash'模式
 
 constructor (options: RouterOptions = {}) {
 
 let mode = options.mode || 'hash' // 默认为'hash'模式
 this.fallback = mode === 'history' && !supportsPushState // 通过supportsPushState判断浏览器是否支持'history'模式
 if (this.fallback) {
     mode = 'hash'
 }
 if (!inBrowser) {
     mode = 'abstract' // 不在浏览器环境下运行需强制为'abstract'模式
 }
 this.mode = mode

 // 根据mode确定history实际的类并实例化
 switch (mode) {
 case 'history':
     this.history = new HTML5History(this, options.base); breakcase 'hash':
      this.history = new HashHistory(this, options.base, this.fallback);breakcase 'abstract':
      this.history = new AbstractHistory(this, options.base); break
 default:
      if (process.env.NODE_ENV !== 'production') {
           assert(false, `invalid mode: $`)
      }
    }
 }

 init (app: any /* Vue component instance */) {
 const history = this.history;
 // 根据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 => {
     this.apps.forEach((app) => {
        app._route = route
   })
 })
 }

 // VueRouter类暴露的以下方法实际是调用具体history对象的方法
VueRouter.prototype.beforeEach = function beforeEach (fn) {
     return registerHook(this.beforeHooks, fn)
};

VueRouter.prototype.beforeResolve = function beforeResolve (fn) {
    return registerHook(this.resolveHooks, fn)
};

VueRouter.prototype.afterEach = function afterEach (fn) {
    return registerHook(this.afterHooks, fn)
};

VueRouter.prototype.onReady = function onReady (cb, errorCb) {
    this.history.onReady(cb, errorCb);
};

VueRouter.prototype.onError = function onError (errorCb) {
    this.history.onError(errorCb);
};

VueRouter.prototype.push = function push (location, onComplete, onAbort) {
    this.history.push(location, onComplete, onAbort);
};

VueRouter.prototype.replace = function replace (location, onComplete, onAbort) {
    this.history.replace(location, onComplete, onAbort);
};

VueRouter.prototype.go = function go (n) {
   this.history.go(n);
};

VueRouter.prototype.back = function back () {
   this.go(-1);
};

VueRouter.prototype.forward = function forward () {
    this.go(1);
};

从上面代码可以看出:

  1. 作为参数创图的字符串属性mode只是一个标记,用来只是实际起作用的对象属性history的的实现类,两者对应关系如下: mode的对应--history:HTML5History; 'hash':HashHishtory; abstract:AbstractHistory
  2. 在初始化对应的history之前,会对mode做一些校验:若浏览器不支持HTML5History方式(通过supportPushState变量判断),则mode强制设为'abstract'
  3. VueRouter类中的onReady(), push()等方法只是一个代理,实际是调用的具体history对象的对应方法,在init()方法中初始化时,也是根据history对象具体的类别执行不同操作

在浏览器环境下的两种方式,分别就是在HTML5History,HashHistory两个雷中实现的。History中定义的是公用和基础的方法,简单说下: ### HashHistory hash("#")符号的本来作用是加在URL中指示网页中的位置:
    > http://www.example.com/index.html#print
  • #号本身以及它后面的字符称之为hash,可通过window.location.hash属性读取。它具有以下特点:

    • hash虽然出现在URL中,但不会被包括在HTTP请求中。它是用来指导浏览器动作的,对服务器端完全无用,因此,改变hash不会重新加载页面
    • 可以为hash的改变添加监听事件

    window.addEventListener("hashchange", funcRef, false)

    • 每一次改变hash(window.location.hash),都会在浏览器的访问历史中增加一个记录

    由此可见,利用hash的特点,就尅来实现前端路由“更新视图但不重新请求页面”的功能。

    HashHistory.push()

    源码:

    
    HashHistory.prototype.push = function push (location, onComplete, onAbort) {
    var this$1 = this;
    
    var ref = this;
    var fromRoute = ref.current;
    this.transitionTo(location, function (route) {
      pushHash(route.fullPath);
      handleScroll(this$1.router, route, fromRoute, false);
      onComplete && onComplete(route);
    }, onAbort);
    };
    // 对window的hash进行直接赋值
    function pushHash (path) {
    if (supportsPushState) {
        pushState(getUrl(path));
    } else {
        window.location.hash = path;
    }
    }
    
    

transitionTo()方法是父类中定义用来处理路由变化中的基础逻辑的,push()方法最主要的是对window的hash进行了直接赋值

pushHash(route.fullPath)

hash的改变会自动添加到浏览器的访问历史记录中


通过Vue.mixin()方法,全局注册一个混合,影响注册之后所有创建的每个Vue实例,该混合在beforeCreate钩子洪通过Vue.util.defineReactive()定义了响应式的_route属性。所谓响应式属性,即当_route值改变时,会自动调用Vue实例中的render()方法,更新视图。
总结一下,从**设置路由**改变到**视图更新**的流程如下:

$router.push() -->HashHistory.push() --> History.transitionTo() --> History.updateRoute() --> vm.render()