前端入门之(vue-router全解析二)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/vv_bug/article/details/82766049

前面一篇文章 前端入门之(vue-router全解析一)我们简单的了解了一下vue-router的原理,今天我们继续vue-router,我们从头到尾的全部撸一遍哈.小伙伴开车啦,跟紧了哦~~

我这里以vue-cli+webpack创建的但页面工程为例子了哈,我们直接选带有vue-router的模版代码为demo.

先上一下vue-router的官网:
https://router.vuejs.org/zh/guide/#javascript

在创建好的vue工程中,我们可以看到我们的main.js文件:


import router from './router'
....
new Vue({
  el: '#app',
  router,
  store,
  render(h){
    return h(App)
  }
})


然后是我们的router.js文件:

import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)

export default new Router({
  mode:'hash',
  routes: [
    {
      path: '/',
      name: 'HelloWorld',
      component: HelloWorld
    }
  ]
})

然后在App.vue中配置router-view:

<template>
  <div id="app">
    <router-view/>
  </div>
</template>

就这么简单的几个操作,我们的vue-router就可以跑起来了,好啦! 我们转过头来一步一步的研究vue-router是怎么走起来的.

首先我们看我们的router.js文件:

Vue.use(Router)

export default new Router({
...
 )}

我们看一下Vue.use(Router)干了什么~~

我们翻开vue-router的源码,找到install方法:

function install (Vue) {
  if (install.installed && _Vue === Vue) { return }
  install.installed = true;

  _Vue = Vue;

  var isDef = function (v) { return v !== undefined; };
  
  var registerInstance = function (vm, callVal) {
    var i = vm.$options._parentVnode;
    if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
      i(vm, callVal);
    }
  };

  Vue.mixin({
    beforeCreate: function beforeCreate () {
      if (isDef(this.$options.router)) {
        this._routerRoot = this;
        this._router = this.$options.router;
        this._router.init(this);
        Vue.util.defineReactive(this, '_route', this._router.history.current);
      } else {
        this._routerRoot = (this.$parent && this.$parent._routerRoot) || this;
      }
      registerInstance(this, this);
    },
    destroyed: function destroyed () {
      registerInstance(this);
    }
  });

  Object.defineProperty(Vue.prototype, '$router', {
    get: function get () { return this._routerRoot._router }
  });

  Object.defineProperty(Vue.prototype, '$route', {
    get: function get () { return this._routerRoot._route }
  });

  Vue.component('router-view', View);
  Vue.component('router-link', Link);

  var strats = Vue.config.optionMergeStrategies;
  // use the same hook merging strategy for route hooks
  strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created;
}

我们看到这么一段代码:

扫描二维码关注公众号,回复: 4282460 查看本文章
  Vue.mixin({
    beforeCreate: function beforeCreate () {
      if (isDef(this.$options.router)) {
        this._routerRoot = this;
        this._router = this.$options.router;
        this._router.init(this);
        Vue.util.defineReactive(this, '_route', this._router.history.current);
      } else {
        this._routerRoot = (this.$parent && this.$parent._routerRoot) || this;
      }
      registerInstance(this, this);
    },
    destroyed: function destroyed () {
      registerInstance(this);
    }
  });

在混入中添加了两个生命周期方法,一个beforeCreate、一个destroyed,在beforeCreate我们可以看到一行代码:

 Vue.util.defineReactive(this, '_route', this._router.history.current);

可以说这一行代码发挥的作用还是挺大的,先剧透一下, 这个方法就是给_routerRoot也就是我们demo中的App.vue中的vue实例注册了一个动态变换属性“_route”, 在上一节中我们知道,vue-router原理就是使用pushState跟hash来做路由切换,vue-router会根据location的变换来动态的改变"_route"变量,而我们的"_route"变量中包含了当前路由需要渲染的组件等信息,而在router-view中,"_route"变量会当成属性传递给router-view,当_route变换的时候,我们的router-view就会重新走render方法,然后render方法中再根据_route中的当前组件,最后把当前页面渲染在页面.

好啦~!! 不知道小伙伴懂了没呢? 不懂也没关系,我们到时讲到vouter-view的时候还会走一遍代码,install方法最重要的也就是这一段代码了,我们继续往下走哈.

接着就是在Vue的原型中定义了两个属性 r o u t e r router跟 route

  Object.defineProperty(Vue.prototype, '$router', {
    get: function get () { return this._routerRoot._router }
  });

  Object.defineProperty(Vue.prototype, '$route', {
    get: function get () { return this._routerRoot._route }
  });

所以我们可以在Vue的所有实例中使用this. r o u t e r t h i s . router跟this. route了,this.$router就是我们传递的router对象:

export default new Router({
  mode:'hash',
  routes: [
    {
      path: '/',
      name: 'HelloWorld',
      component: HelloWorld
    },

this.$route是当前的route对象,其中包含了当前页面中的所有信息.

接着就是注册了两个全局组件了:

 Vue.component('router-view', View);
  Vue.component('router-link', Link);

想必稍微了解过vue-router的小伙伴都知道这两个组件,router-view是router的根组件,所有的页面跳转都是基于router-view做跳转的.
router-link则是vue-router提供的一个方便开发者使用api而封装的一个组件,童鞋们也可以不用router-link,自己走api的方式来实现路由跳转.

好啦~ 分析了好久才把install方法简单地分析完,嘿嘿,真是佩服写框架的大佬们.

分析完install方法,我们紧接着来说一下vue-router组件,我们可以在vue-router的源码中看到vue-router组件的代码:

var View = {
  name: 'router-view',
  functional: true,
  props: {
    name: {
      type: String,
      default: 'default'
    }
  },
  render: function render (_, ref) {
    var props = ref.props;
    var children = ref.children;
    var parent = ref.parent;
    var data = ref.data;
    data.routerView = true;

    // directly use parent context's createElement() function
    // so that components rendered by router-view can resolve named slots
    var h = parent.$createElement;
    var name = props.name;
    var route = parent.$route;
    var cache = parent._routerViewCache || (parent._routerViewCache = {});

    // determine current view depth, also check to see if the tree
    // has been toggled inactive but kept-alive.
    var depth = 0;
    var inactive = false;
    while (parent && parent._routerRoot !== parent) {
      if (parent.$vnode && parent.$vnode.data.routerView) {
        depth++;
      }
      if (parent._inactive) {
        inactive = true;
      }
      parent = parent.$parent;
    }
    data.routerViewDepth = depth;

    // render previous view if the tree is inactive and kept-alive
    if (inactive) {
      return h(cache[name], data, children)
    }

    var matched = route.matched[depth];
    // render empty node if no matched route
    if (!matched) {
      cache[name] = null;
      return h()
    }

    var component = cache[name] = matched.components[name];

    // attach instance registration hook
    // this will be called in the instance's injected lifecycle hooks
    data.registerRouteInstance = function (vm, val) {
      // val could be undefined for unregistration
      var current = matched.instances[name];
      if (
        (val && current !== vm) ||
        (!val && current === vm)
      ) {
        matched.instances[name] = val;
      }
    }

    // also register instance in prepatch hook
    // in case the same component instance is reused across different routes
    ;(data.hook || (data.hook = {})).prepatch = function (_, vnode) {
      matched.instances[name] = vnode.componentInstance;
    };

    // resolve props
    var propsToPass = data.props = resolveProps(route, matched.props && matched.props[name]);
    if (propsToPass) {
      // clone to prevent mutation
      propsToPass = data.props = extend({}, propsToPass);
      // pass non-declared props as attrs
      var attrs = data.attrs = data.attrs || {};
      for (var key in propsToPass) {
        if (!component.props || !(key in component.props)) {
          attrs[key] = propsToPass[key];
          delete propsToPass[key];
        }
      }
    }

    return h(component, data, children)
  }
};

代码有点多,我们捡重点的来分析,通过源码我们可以看到,router-view是一个无状态的组件,也就是设置了functional属性,在vue的官网中我们可以找到,当一个组件设置了functional属性后,这个组件也就是一个无状态的组件(也就是没有data),无实例(没有 this 上下文),要深入研究的童鞋可以去看一下vue的官网
https://cn.vuejs.org/v2/guide/render-function.html

render: function render (_, ref) {
    ...
    var route = parent.$route;

	...
    var matched = route.matched[depth];
...

    var component = cache[name] = matched.components[name];

	...
    return h(component, data, children)
  }
};

在render方法中,我们可以看到,首先去获取route对象,然后获取route对象中的当前页面component,最后通过render的h方法把component(当前页面)渲染出来.

所以当我们执行vue-router的push、replace、back、go等方法的时候,其实就是改变location然后再改变当前route对象,当route对象改变的时候,就会触发router-view的render方法,然后render方法根据route做页面切换. 好吧,说了半天,想必小伙伴也都累了,我们就以vue-router的push方法为例子走一遍整个流程.

push这个方法会向 history 栈添加一个新的记录,所以,当用户点击浏览器后退按钮时,则回到之前的 URL。

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

因为vue-router中有hash跟history模式,我们就以HTML5History为例子了,我们找到HTML5History的push方法:

 HTML5History.prototype.push = function push (location, onComplete, onAbort) {
    var this$1 = this;

    var ref = this;
    var fromRoute = ref.current;
    this.transitionTo(location, function (route) {
      pushState(cleanPath(this$1.base + route.fullPath));
      handleScroll(this$1.router, route, fromRoute, false);
      onComplete && onComplete(route);
    }, onAbort);
  };

幸好代码不是很多,我们可以看到最主要的就是执行了一个叫transitionTo的方法,我们点进去:

History.prototype.transitionTo = function transitionTo (location, onComplete, onAbort) {
    var this$1 = this;

  var route = this.router.match(location, this.current);
  this.confirmTransition(route, function () {
    this$1.updateRoute(route);
    onComplete && onComplete(route);
    this$1.ensureURL();

    // fire ready cbs once
    if (!this$1.ready) {
      this$1.ready = true;
      this$1.readyCbs.forEach(function (cb) { cb(route); });
    }
  }, function (err) {
    if (onAbort) {
      onAbort(err);
    }
    if (err && !this$1.ready) {
      this$1.ready = true;
      this$1.readyErrorCbs.forEach(function (cb) { cb(err); });
    }
  });
};

然后可以看到,又执行了一个叫confirmTransition的方法,简单说一下confirmTransition方法,confirmTransition方法主要是用于处理一些导航守卫函数, 一个一个的遍历这些守卫函数,最后执行,直到执行完所有的过渡函数,最后回调在transitionTo方法中传递的函数

History.prototype.transitionTo = function transitionTo (location, onComplete, onAbort) {
    ...
  this.confirmTransition(route, function () {
    this$1.updateRoute(route);
   ...
  }, function (err) {
   ...
  });
};

可以看到,当执行完confirmTransition方法后,执行了一行代码:

 this$1.updateRoute(route);

这行代码是干什么的呢? 我们继续往下走:

History.prototype.updateRoute = function updateRoute (route) {
  var prev = this.current;
  this.current = route;
  this.cb && this.cb(route);
  this.router.afterHooks.forEach(function (hook) {
    hook && hook(route, prev);
  });
};

可以看到,最主要的就是改变了current(也就是当前路由),然后执行了

this.cb && this.cb(route);

把当前路由传递给了cb方法,那么cb方法又是啥呢?找呀找呀,我们找到了HtmlHistory的父类:

History.prototype.listen = function listen (cb) {
  this.cb = cb;
};

所以我们需要看一下listen方法在哪调用的,我们继续找呀找呀,最后在vue-router的init方法中找到了它:

VueRouter.prototype.init = function init (app /* Vue component instance */) {
   ....

  history.listen(function (route) {
    this$1.apps.forEach(function (app) {
      app._route = route;
    });
  });
};

那么init方法又是哪调用的呢? 我们继续找呀找呀,最后在install方法中找到了init方法:

function install (Vue) {
  ...
  Vue.mixin({
    beforeCreate: function beforeCreate () {
      if (isDef(this.$options.router)) {
        this._routerRoot = this;
        this._router = this.$options.router;
        this._router.init(this);
        Vue.util.defineReactive(this, '_route', this._router.history.current);
      } else {
        ...
}

我重点看init方法中的这段代码:

VueRouter.prototype.init = function init (app /* Vue component instance */) {
   ....
  history.listen(function (route) {
    this$1.apps.forEach(function (app) {
      app._route = route;
    });
  });
};

遍历apps,然后给apps中的每个_route对象重新赋值最新的route路由信息,apps是什么呢? 从install方法中我们可以看到,apps其实就是像App.vue中的vue实例的一个集合, 因为我们demo中的rootRouter就一个,所以apps集合中也就一个vue实例,也就是App.vue.

文章一开始我们就解析了vue-router的install方法,在install中给rootRouter定义了一个可以变化的属性"_route":

 Vue.util.defineReactive(this, '_route', this._router.history.current);

所以当_route改变的时候,我们的的router-view重新走render方法,页面改变.

我们回到HTML5History的push方法:

 HTML5History.prototype.push = function push (location, onComplete, onAbort) {
    var this$1 = this;

    var ref = this;
    var fromRoute = ref.current;
    this.transitionTo(location, function (route) {
      pushState(cleanPath(this$1.base + route.fullPath));
      handleScroll(this$1.router, route, fromRoute, false);
      onComplete && onComplete(route);
    }, onAbort);
  };

前面分析我们直到,当我们执行完transitionTo方法的时候,其实我们的页面已经改变了,但是我们的链接地址还没变,所以接下来就调用了 pushState方法,在上一节中,我们已经认识了pushState方法了,所以我们点进去看看是不是跟我们上一节说的一样:

function pushState (url, replace) {
  saveScrollPosition();
  // try...catch the pushState call to get around Safari
  // DOM Exception 18 where it limits to 100 pushState calls
  var history = window.history;
  try {
    if (replace) {
      history.replaceState({ key: _key }, '', url);
    } else {
      _key = genKey();
      history.pushState({ key: _key }, '', url);
    }
  } catch (e) {
    window.location[replace ? 'replace' : 'assign'](url);
  }
}

哈哈!! 我就不解释了哈,小伙伴感兴趣的可以去看我上一篇文章
https://blog.csdn.net/vv_bug/article/details/82795248

好啦,到这里vue-router的全部流程我们就简单的走完了,小伙伴要深入研究的自己可以去试着看看源码,也可以一起交流学习一下哈,嘿嘿!!! 最后还落了一个router-link组件, 好晚啦,实在写不动了,放到下一节了~

欢迎入群,欢迎交流,qq群链接:
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/vv_bug/article/details/82766049