装置

  1. 增加全局的 beforeCreate、destroyed 生命周期办法;Vue.prototype 上增加$route和$router 属性;注册 router-view 和 router-link 组件;增加新的生命周期 beforeRouteEnter、beforeRouteLeave、beforeRouteUpdate;
  2. beforeCreate 周期中,将路由 VueRouter 实例 router 增加根组件(蕴含 VueRouter 实例选项的组件)上,同时增加路由信息\_route 属性到根组件上,设置该属性响应式。子组件\_routerRoot 属性指向该根组件。
  3. Vue.prototy 上的$router和$route 属性值也是来自于根组件的 router 和\_route。
根组件的 beforeCreate 周期中调用了 VueRouter 实例的 init 办法进行初始化。
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; // this._routerRoot 根组件        },    });    Object.defineProperty(Vue.prototype, "$route", {        get: function get() {            return this._routerRoot._route; // this._routerRoot 根组件        },    });    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;}

router 实例

  1. VueRouter 结构器中,依据配置的路由模式生成对应的 History 实例。
调用了 createMatcher 办法,依据选项中的路由生成门路、名称和路由配置项的映射,并返回 matcher,提供 match 和 addRoutes 办法。
  1. 依据路由模式配置,生成对应的 History 实例。
var VueRouter = function VueRouter(options) {    if (options === void 0) options = {};    this.app = null;    this.apps = [];    this.options = options;    this.beforeHooks = [];    this.resolveHooks = [];    this.afterHooks = [];    this.matcher = createMatcher(options.routes || [], this);    var mode = options.mode || "hash";    this.fallback =        mode === "history" && !supportsPushState && options.fallback !== false;    if (this.fallback) {        mode = "hash";    }    if (!inBrowser) {        mode = "abstract";    }    this.mode = mode;    switch (mode) {        case "history":            this.history = new HTML5History(this, options.base);            break;        case "hash":            this.history = new HashHistory(this, options.base, this.fallback);            break;        case "abstract":            this.history = new AbstractHistory(this, options.base);            break;        default:            if (process.env.NODE_ENV !== "production") {                assert(false, "invalid mode: " + mode);            }    }};

RouterView 组件

  1. props 的 name 属性匹配对应的路由命名视图。
  2. 调用 render 函数,渲染以后路由对应的组件。通过 parent.\$route 读取以后的路由信息,而后向上查找,依据 routerView 路由组件标识 routerView,获取以后的路由层级。依据路由层级和命名视图的名称获取对应的组件选项,依据组件选项生成对应的节点(VNode 实例)返回。
var View = {    name: "RouterView",    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;        // 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) {            var vnodeData = parent.$vnode ? parent.$vnode.data : {};            if (vnodeData.routerView) {                depth++;            }            if (                vnodeData.keepAlive &&                parent._directInactive &&                parent._inactive            ) {                inactive = true;            }            parent = parent.$parent;        }        data.routerViewDepth = depth;        // render previous view if the tree is inactive and kept-alive        if (inactive) {            var cachedData = cache[name];            var cachedComponent = cachedData && cachedData.component;            if (cachedComponent) {                // #2301                // pass props                if (cachedData.configProps) {                    fillPropsinData(                        cachedComponent,                        data,                        cachedData.route,                        cachedData.configProps                    );                }                return h(cachedComponent, data, children);            } else {                // render previous empty view                return h();            }        }        var matched = route.matched[depth]; // 获取以后层级的组件        var component = matched && matched.components[name]; // 获取该层级下命名视图对应的组件        // render empty node if no matched route or no config component        if (!matched || !component) {            cache[name] = null;            return h();        }        // cache component        cache[name] = { component: component };        // 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;        };        // register instance in init hook        // in case kept-alive component be actived when routes changed        data.hook.init = function (vnode) {            if (                vnode.data.keepAlive &&                vnode.componentInstance &&                vnode.componentInstance !== matched.instances[name]            ) {                matched.instances[name] = vnode.componentInstance;            }        };        var configProps = matched.props && matched.props[name];        // save route and configProps in cachce        if (configProps) {            extend(cache[name], {                route: route,                configProps: configProps,            });            fillPropsinData(component, data, route, configProps);        }        return h(component, data, children);    },};
route.matched 顺次保留了以后门路对应的组件选项,以上面路由配置为例:/home/tab/list门路对应的 matched 是:[Home, Tab, List]
const router = new Router({    mode: "history",    routes: [        {            name: "home",            path: "/home",            component: Home,            children: [                {                    name: "tab",                    path: "tab",                    component: Tab,                    children: [                        {                            name: "list",                            path: "list",                            component: List,                        },                    ],                },            ],        },    ],});

实例 init 初始化

  1. 根组件的 beforeCreate 周期中调用了 VueRouter 实例 init 办法。默认初始门路为“/”,并依据该门路获取对应的路由信息,而后和以后实在的 url 门路比对,更新为以后 url 对应的路由信息,更改\_router 值,因为设置了\_router 属性响应式,且 router-view 读取了该值,当\_router 从新赋值时,就有从新渲染 router-view 组件,加载以后门路对应的组件页面。
VueRouter.prototype.init = function init(app /* Vue component instance */) {    // 每个组件初始化路由    var this$1 = this;    assert(        install.installed,        "not installed. Make sure to call `Vue.use(VueRouter)` " +            "before creating root instance."    );    this.apps.push(app);    // set up app destroyed handler    // https://github.com/vuejs/vue-router/issues/2639    app.$once("hook:destroyed", function () {        // clean out app from this.apps array once destroyed        var index = this$1.apps.indexOf(app);        if (index > -1) {            this$1.apps.splice(index, 1);        } // 组件销毁时从apps移除        // ensure we still have a main app or null if no apps        // we do not release the router so it can be reused        if (this$1.app === app) {            this$1.app = this$1.apps[0] || null;        }        if (!this$1.app) {            // clean up event listeners            // https://github.com/vuejs/vue-router/issues/2341            this$1.history.teardownListeners();        }    });    // main app previously initialized    // return as we don't need to set up new history listener    if (this.app) {        return;    }    this.app = app;    var history = this.history;    if (history instanceof HTML5History || history instanceof HashHistory) {        var setupListeners = function () {            history.setupListeners();        };        history.transitionTo(            history.getCurrentLocation(),            setupListeners,            setupListeners        );    }    history.listen(function (route) {        this$1.apps.forEach(function (app) {            app._route = route; // 更新路由        });    });};// 初始路由信息var START = createRoute(null, {    path: "/",});

路由跳转

  1. HTML5History,HashHistory,AbstractHistory 继承了 Histroy,Histroy 实例上的 router 指向 VueRouter 实例。Histroy 提供了 transitionTo 和 confirmTransition 办法,将在路由跳转时调用。
/*  */var History = function History (router, base) {    this.router = router;// VueRouter实例    this.base = normalizeBase(base);// 格式化根底门路    // start with a route object that stands for "nowhere"    this.current = START;// "/"对应的路由信息(初始路由信息)    this.pending = null;    this.ready = false;    this.readyCbs = [];    this.readyErrorCbs = [];    this.errorCbs = [];    this.listeners = [];  };  History.prototype.transitionTo = function transitionTo (    location,// 以后URL对应路由的门路    onComplete,    onAbort  ) {      var this$1 = this;    var route = this.router.match(location, this.current);// 调用VueRouter实例的match办法,返回行将跳转的路由信息,current以后的路由信息    this.confirmTransition(      route,      function () {        var prev = this$1.current;        this$1.updateRoute(route);// 更新为跳转后的路由信息      },      ...    );  };  History.prototype.confirmTransition = function confirmTransition (route, onComplete, onAbort) {      ...  };  History.prototype.updateRoute = function updateRoute (route) {    this.current = route;    this.cb && this.cb(route);  };  History.prototype.setupListeners = function setupListeners () {    ...  };  History.prototype.teardownListeners = function teardownListeners () {// 革除事件监听    ...  };var HTML5History = /*@__PURE__*/ (function (History) {  //继承了History  function HTML5History(router, base) {    History.call(this, router, base);    this._startLocation = getLocation(this.base);  }  if (History) HTML5History.__proto__ = History; // 继承History,复用结构器上的办法  HTML5History.prototype = Object.create(History && History.prototype); // 继承History,复用实例上的办法  HTML5History.prototype.constructor = HTML5History;// 继承History,复用结构器  HTML5History.prototype.setupListeners = function setupListeners() {    var this$1 = this;    if (this.listeners.length > 0) {      return;    }    var router = this.router;    var expectScroll = router.options.scrollBehavior;    var supportsScroll = supportsPushState && expectScroll;    if (supportsScroll) {      this.listeners.push(setupScroll()); //增加事件    }    var handleRoutingEvent = function () {      var current = this$1.current;      // Avoiding first `popstate` event dispatched in some browsers but first      // history route not updated since async guard at the same time.      var location = getLocation(this$1.base);      if (this$1.current === START && location === this$1._startLocation) {        return;      }      this$1.transitionTo(location, function (route) {        if (supportsScroll) {          handleScroll(router, route, current, true);        }      });    };    window.addEventListener("popstate", handleRoutingEvent);    this.listeners.push(function () {      window.removeEventListener("popstate", handleRoutingEvent);    });  };  HTML5History.prototype.go = function go(n) {    // 路由回退或者后退    window.history.go(n);  };  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)); // 保留页面滚动信息,设置页面跳转url,增加一条记录到history中        handleScroll(this$1.router, route, fromRoute, false);        onComplete && onComplete(route);      },      onAbort    );  };  HTML5History.prototype.replace = function replace(    location,    onComplete,    onAbort  ) {    var this$1 = this;    var ref = this;    var fromRoute = ref.current;    this.transitionTo(      location,      function (route) {        replaceState(cleanPath(this$1.base + route.fullPath)); // 保留页面滚动信息,批改以后路由,并批改history以后记录        handleScroll(this$1.router, route, fromRoute, false); // 页面滚动        onComplete && onComplete(route); // 跳转胜利回调函数      },      onAbort    );  };  /**   *  获取路由门路(不蕴含根底门路)   */  HTML5History.prototype.getCurrentLocation = function getCurrentLocation() {    return getLocation(this.base);  };  return HTML5History;})(History);

页面跳转

  1. 路由跳转时先依据以后路由和跳转路由对应的组件,别离提取出须要更新,须要解冻(暗藏或移除),须要激活(创立或显示)的组件。而后顺次调用组件上路由相干的生命周期钩子函数,包含加载异步组件。
  2. 调用路由生命周期钩子函数后,更新以后的路由数据。
  3. 最初更新 history 状态,并保留页面的滚动状态。
因为对路由数据设置了响应式,更新以后的路由数据会触发页面从新渲染
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)); // 保留页面滚动信息,设置页面跳转url,增加一条记录到history中            handleScroll(this$1.router, route, fromRoute, false);            onComplete && onComplete(route);        },        onAbort    );};History.prototype.transitionTo = function transitionTo(    location, // 跳转选项    onComplete,    onAbort) {    var this$1 = this;    var route = this.router.match(location, this.current); // 调用VueRouter实例的match办法,返回跳转选项匹配的路由信息    this.confirmTransition(        route,        function () {            var prev = this$1.current;            this$1.updateRoute(route); // 更新为曾经跳转的路由            onComplete && onComplete(route);            this$1.ensureURL();            this$1.router.afterHooks.forEach(function (hook) {                hook && hook(route, prev);            });            // 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);                });            }        }    );};History.prototype.confirmTransition = function confirmTransition(    route,    onComplete,    onAbort) {    var this$1 = this;    var current = this.current;    var abort = function (err) {        // changed after adding errors with        // https://github.com/vuejs/vue-router/pull/3047 before that change,        // redirect and aborted navigation would produce an err == null        if (!isRouterError(err) && 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(); // 确保以后路由和页面的url统一        return abort(createNavigationDuplicatedError(current, route));    }    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) // 返回函数function(to, from, next)    );    this.pending = route;    var iterator = function (hook, next) {        if (this$1.pending !== route) {            return abort(createNavigationCancelledError(current, route));        }        try {            // 调用钩子办法            hook(route, current, function (to) {                if (to === false) {                    // next(false) -> abort navigation, ensure current URL                    this$1.ensureURL(true);                    abort(createNavigationAbortedError(current, route));                } else if (isError(to)) {                    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(createNavigationRedirectedError(current, route));                    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 () {        // 调用组件leave、update等钩子函数,异步获取组件选项,创立组件结构器,最初调用这里的回调函数        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); // 异步加载组件选项中的enter钩子办法        var queue = enterGuards.concat(this$1.router.resolveHooks);        runQueue(queue, iterator, function () {            if (this$1.pending !== route) {                return abort(createNavigationCancelledError(current, route));            }            this$1.pending = null;            onComplete(route); // 更新路由            if (this$1.router.app) {                this$1.router.app.$nextTick(function () {                    postEnterCbs.forEach(function (cb) {                        cb();                    });                });            }        });    });};