美文网首页程序员
vue-router 执行过程及源码解析。

vue-router 执行过程及源码解析。

作者: 臣以君纲 | 来源:发表于2019-03-02 12:01 被阅读0次

    我们都知道,vue提供的cli脚手架工具中直接会询问是否安装vue-router,可见vue-router和vue的情之深思之切。
    vue-router其实主要通过push操作修改route,从而触发setter完成路由组件router-view的重新渲染,
    下面我们将从何时触发的push和如何触发组件重新渲染对vue-router核心原理进行讲解
    我们在使用vue-router第一个步骤都是先执行Vue.use(Router),Vue.use其实是一个vue插件的安装过程,最后执行到插件中 的install方法,我们先来看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('RouterView', View);
      Vue.component('RouterLink', Link);
    
      var strats = Vue.config.optionMergeStrategies;
      // use the same hook merging strategy for route hooks
      strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created;
    }
    

    首先会对重复安装进行过滤,然后向全局Vue的options中混入了beforeCreate方法和destroyed方法,我们知道所有组件的options都会继承Vue的option,所以,在每个组件初始化过程中都会混入这两个钩子函数,
    除了混入这两个钩子函数外,还对$router和$route两个属性进行了劫持,使我们可以直接通过Vue对象实例访问到,这两个一个存储的是VueRouter对象一个存储的是匹配到的路由信息,最后全局注册了RouterviewRouterLink两个组件,所以我们才可以在任何地方使用这两个组件,这两个组件的内容我们稍后分析,
    我们先看混入每个组件的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);
    

    调用Vue中定义的defineReactive对_route进行劫持,这个劫持之前有说过,其实是执行的依赖收集的过程,执行_route的get就会对当前的组件进行依赖收集,如果对_route进行重新赋值触发setter就会使收集的组件重新渲染,这里也是路由重新渲染的核心所在,这里还执行了init方法,我们来看下init

    VueRouter.prototype.init = function init (app /* Vue component instance */) {
        var this$1 = this;
    
      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;
    
      var history = this.history;
    
      if (history instanceof HTML5History) {
        history.transitionTo(history.getCurrentLocation());
      } else if (history instanceof HashHistory) {
        var setupHashListener = function () {
          history.setupListeners();
        };
        history.transitionTo(
          history.getCurrentLocation(),
          setupHashListener,
          setupHashListener
        );
      }
    
      history.listen(function (route) {
        debugger
        this$1.apps.forEach(function (app) {
          app._route = route;
        });
      });
    };
    

    重点是根据我们初使化的路由类型,执行transitionTo方法,这个是路由跳转的核心所在,这里同时还对浏览器hashChange事件进行了监听,比如我们执行浏览器后退事件也会触发transitionTo方法,这里在初始化的时候会根据当前路径执行一次transitionTo,,这也是为什么当我们直接在地址栏输入localhost:8080最后会变成localhost:8080/#/,
    最后执行了history.listen方法,这里是当路由改变时对_route进行重新赋值从而触发组件更新,setter的执行也是在这里,这里的调用是在transitionTo完成之后,我们看下transitionTo的定义

    History.prototype.transitionTo = function transitionTo (location, onComplete, onAbort) {
        var this$1 = this;
      debugger
      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我们来看代码

    History.prototype.confirmTransition = function confirmTransition (route, onComplete, onAbort) {
        var this$1 = this;
    
      var current = this.current;
      var abort = function (err) {
        if (isError(err)) {
          if (this$1.errorCbs.length) {
            this$1.errorCbs.forEach(function (cb) { cb(err); });
          } else {
            warn(false, 'uncaught error during route navigation:');
            console.error(err);
          }
        }
        onAbort && onAbort(err);
      };
      if (
        isSameRoute(route, current) &&
        // in the case the route map has been dynamically appended to
        route.matched.length === current.matched.length
      ) {
        this.ensureURL();
        return abort()
      }
    
      var ref = resolveQueue(this.current.matched, route.matched);
        var updated = ref.updated;
        var deactivated = ref.deactivated;
        var activated = ref.activated;
    
      var queue = [].concat(
        // in-component leave guards
        extractLeaveGuards(deactivated),
        // global before hooks
        this.router.beforeHooks,
        // in-component update hooks
        extractUpdateHooks(updated),
        // in-config enter guards
        activated.map(function (m) { return m.beforeEnter; }),
        // async components
        resolveAsyncComponents(activated)
      );
    
      this.pending = route;
      var iterator = function (hook, next) {
        if (this$1.pending !== route) {
          return abort()
        }
        try {
          hook(route, current, function (to) {
            if (to === false || isError(to)) {
              // next(false) -> abort navigation, ensure current URL
              this$1.ensureURL(true);
              abort(to);
            } else if (
              typeof to === 'string' ||
              (typeof to === 'object' && (
                typeof to.path === 'string' ||
                typeof to.name === 'string'
              ))
            ) {
              // next('/') or next({ path: '/' }) -> redirect
              abort();
              if (typeof to === 'object' && to.replace) {
                this$1.replace(to);
              } else {
                this$1.push(to);
              }
            } else {
              // confirm transition and pass on the value
              next(to);
            }
          });
        } catch (e) {
          abort(e);
        }
      };
    
      runQueue(queue, iterator, function () {
        var postEnterCbs = [];
        var isValid = function () { return this$1.current === route; };
        // wait until async components are resolved before
        // extracting in-component enter guards
        var enterGuards = extractEnterGuards(activated, postEnterCbs, isValid);
        var queue = enterGuards.concat(this$1.router.resolveHooks);
        runQueue(queue, iterator, function () {
          if (this$1.pending !== route) {
            return abort()
          }
          this$1.pending = null;
          onComplete(route);
          if (this$1.router.app) {
            this$1.router.app.$nextTick(function () {
              postEnterCbs.forEach(function (cb) { cb(); });
            });
          }
        });
      });
    };
    

    首先会有重复路由的判断,如果进入相同的路由,直接调用abort回调函数,函数退出,不会执行后面的各组件的钩子函数,这也是为什么我们重复进入相同路由不会触发组建的重新渲染也不会触发路由的各种钩子函数,
    如果判断不是相同路由,就会执行各组件的钩子函数,这里的逻辑我们不讲,可以根据文档描述来看路由执行顺序
    这里我们贴一下官网描述的导航守卫执行顺序

    导航被触发。
    在失活的组件里调用离开守卫。
    调用全局的 beforeEach 守卫。
    在重用的组件里调用 beforeRouteUpdate 守卫 (2.2+)。
    在路由配置里调用 beforeEnter。
    解析异步路由组件。
    在被激活的组件里调用 beforeRouteEnter。
    调用全局的 beforeResolve 守卫 (2.5+)。
    导航被确认。
    调用全局的 afterEach 钩子。
    触发 DOM 更新。
    用创建好的实例调用 beforeRouteEnter 守卫中传给 next 的回调函数。
    

    按顺序执行好导航守卫后,就会执行传入的成功的回调函数,也就是

    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); });
        }
      }
    

    这里会执行初始化的时候通过listen注册的回调函数,从而对_route进行赋值,触发setter,从而使组件重新渲染
    下面我们来看下平时我们通过this.$router.push或者router-link是如何执行transitionTo的,我们先来看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的方法,我们再来看下routerLink这个全局注册的组件

     render: function render (h) {
        var this$1 = this;
    
        var router = this.$router;
        var current = this.$route;
        var ref = router.resolve(this.to, current, this.append);
        var location = ref.location;
        var route = ref.route;
        var href = ref.href;
    
        var classes = {};
        var globalActiveClass = router.options.linkActiveClass;
        var globalExactActiveClass = router.options.linkExactActiveClass;
        // Support global empty active class
        var activeClassFallback = globalActiveClass == null
          ? 'router-link-active'
          : globalActiveClass;
        var exactActiveClassFallback = globalExactActiveClass == null
          ? 'router-link-exact-active'
          : globalExactActiveClass;
        var activeClass = this.activeClass == null
          ? activeClassFallback
          : this.activeClass;
        var exactActiveClass = this.exactActiveClass == null
          ? exactActiveClassFallback
          : this.exactActiveClass;
        var compareTarget = location.path
          ? createRoute(null, location, null, router)
          : route;
    
        classes[exactActiveClass] = isSameRoute(current, compareTarget);
        classes[activeClass] = this.exact
          ? classes[exactActiveClass]
          : isIncludedRoute(current, compareTarget);
    
        var handler = function (e) {
          if (guardEvent(e)) {
            if (this$1.replace) {
              router.replace(location);
            } else {
              router.push(location);
            }
          }
        };
    
        var on = { click: guardEvent };
        if (Array.isArray(this.event)) {
          this.event.forEach(function (e) { on[e] = handler; });
        } else {
          on[this.event] = handler;
        }
    
        var data = {
          class: classes
        };
    
        if (this.tag === 'a') {
          data.on = on;
          data.attrs = { href: href };
        } else {
          // find the first <a> child and apply listener and href
          var a = findAnchor(this.$slots.default);
          if (a) {
            // in case the <a> is a static node
            a.isStatic = false;
            var aData = a.data = extend({}, a.data);
            aData.on = on;
            var aAttrs = a.data.attrs = extend({}, a.data.attrs);
            aAttrs.href = href;
          } else {
            // doesn't have <a> child, apply listener to self
            data.on = on;
          }
        }
    
        return h(this.tag, data, this.$slots.default)
      }
    

    这是routerLink组建的render函数,可以看到为此组件注册了click事件,事件的回调函数便是调用router.push,所以,可以看到所有的路由跳转最后都是调用的transitionTo方法,
    最后我们再看下routerView这个全局组件的定义

     render: function render (_, ref) {
        debugger
        var props = ref.props;
        var children = ref.children;
        var parent = ref.parent;
        var data = ref.data;
    
        // used by devtools to display a router-view badge
        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)
      }
    

    其实就是执行$createElement,把路由匹配到的组件渲染到routerView上,这里会有嵌套路由的判断,这里先不展开讲了,
    这篇文章到这里位置,整个vue-

    相关文章

      网友评论

        本文标题:vue-router 执行过程及源码解析。

        本文链接:https://www.haomeiwen.com/subject/kblsuqtx.html