关于vue-router:vuerouter源码解析

6次阅读

共计 13966 个字符,预计需要花费 35 分钟才能阅读完成。

装置

  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();
                    });
                });
            }
        });
    });
};
正文完
 0