本文基于vue-router 4.1.6版本源码进行剖析

前言

在上一篇《Vue3相干源码-Vue Router源码解析(一)》文章中,咱们曾经剖析了createWebHashHistory()createRouter()的相干内容,本文将持续下一个知识点app.use(router)展现剖析

// 1. 定义路由组件.// 也能够从其余文件导入const Home = { template: '<div>Home</div>' }const About = { template: '<div>About</div>' }// 2. 定义一些路由// 每个路由都须要映射到一个组件。// 咱们前面再探讨嵌套路由。const routes = [  { path: '/', component: Home },  { path: '/about', component: About },]// 3. 创立路由实例并传递 `routes` 配置// 你能够在这里输出更多的配置,但咱们在这里// 临时放弃简略const router = VueRouter.createRouter({  // 4. 外部提供了 history 模式的实现。为了简略起见,咱们在这里应用 hash 模式。  history: VueRouter.createWebHashHistory(),  routes, // `routes: routes` 的缩写})// 5. 创立并挂载根实例const app = Vue.createApp({})//确保 _use_ 路由实例使//整个利用反对路由。app.use(router)app.mount('#app')

初始化

app.use(router)应用VueRouter

use(plugin, ...options) {  if (plugin && isFunction(plugin.install)) {    installedPlugins.add(plugin);    plugin.install(app, ...options);  }  else if (isFunction(plugin)) {    installedPlugins.add(plugin);    plugin(app, ...options);  }  return app;}

router.install(app)

从下面Vue3的源码能够晓得,最终会触发Vue Routerinstall()办法

const START_LOCATION_NORMALIZED = {  path: '/',  name: undefined,  params: {},  query: {},  hash: '',  fullPath: '/',  matched: [],  meta: {},  redirectedFrom: undefined,};const currentRoute = vue.shallowRef(START_LOCATION_NORMALIZED);const router = {  //....  addRoute,  removeRoute,  push,  replace,  beforeEach: beforeGuards.add,  isReady,  install(app) {    //...省略,内部app.use()时调用  },};

如上面代码块所示,次要执行了:

  • 注册了RouterLinkRouterView两个组件
  • 注册routerVue全局对象,保障this.$router能注入到每一个子组件中
  • push(routerHistory.location):初始化时触发push()操作(部分作用域下的办法)
  • provide: routerreactiveRoute(实质是currentRoutereactive构造模式)、currentRoute
install(app) {    const router = this;    app.component('RouterLink', RouterLink);    app.component('RouterView', RouterView);    app.config.globalProperties.$router = router;    Object.defineProperty(app.config.globalProperties, '$route', {        enumerable: true,        get: () => vue.unref(currentRoute), // 主动解构Ref,拿出.value    });    if (isBrowser &&        // used for the initial navigation client side to avoid pushing        // multiple times when the router is used in multiple apps        !started &&        currentRoute.value === START_LOCATION_NORMALIZED) {        // see above        started = true;        push(routerHistory.location).catch(err => {            warn('Unexpected error when starting the router:', err);        });    }    const reactiveRoute = {};    for (const key in START_LOCATION_NORMALIZED) {        reactiveRoute[key] = vue.computed(() => currentRoute.value[key]);    }    app.provide(routerKey, router); // 如下面代码块所示    app.provide(routeLocationKey, vue.reactive(reactiveRoute));    app.provide(routerViewLocationKey, currentRoute); // 如下面代码块所示    // 省略Vue.unmount的一些逻辑解决...}
Vue Router整体初始化的流程曾经剖析结束,一些根底的API,如router.push等也在初始化过程中剖析,因而上面将剖析Vue Router提供的自定义组件以及组合式API的内容

组件

RouterView

咱们应用代码扭转路由时,也在扭转<router-view></router-view>Component内容,实现组件渲染
<script src="https://unpkg.com/vue@3"></script><script src="https://unpkg.com/vue-router@4"></script><div id="app">  <h1>Hello App!</h1>  <p>    <!--应用 router-link 组件进行导航 -->    <!--通过传递 `to` 来指定链接 -->    <!--`<router-link>` 将出现一个带有正确 `href` 属性的 `<a>` 标签-->    <router-link to="/">Go to Home</router-link>    <router-link to="/about">Go to About</router-link>  </p>  <!-- 路由进口 -->  <!-- 路由匹配到的组件将渲染在这里 -->  <router-view></router-view></div>

<router-view></router-view>就是一个Vue Component

<router-view>代码量还是挺多的,因而上面将切割为2个局部进行剖析
const RouterViewImpl = vue.defineComponent({    setup(props, { attrs, slots }) {        //========= 第1局部 =============        const injectedRoute = vue.inject(routerViewLocationKey);        const routeToDisplay = vue.computed(() => props.route || injectedRoute.value);        const injectedDepth = vue.inject(viewDepthKey, 0);        const depth = vue.computed(() => {            let initialDepth = vue.unref(injectedDepth);            const { matched } = routeToDisplay.value;            let matchedRoute;            while ((matchedRoute = matched[initialDepth]) &&                !matchedRoute.components) {                initialDepth++;            }            return initialDepth;        });        const matchedRouteRef = vue.computed(() => routeToDisplay.value.matched[depth.value]);        vue.provide(viewDepthKey, vue.computed(() => depth.value + 1));        vue.provide(matchedRouteKey, matchedRouteRef);        vue.provide(routerViewLocationKey, routeToDisplay);        const viewRef = vue.ref();        vue.watch(() => [viewRef.value, matchedRouteRef.value, props.name], ([instance, to, name], [oldInstance, from, oldName]) => {            if (to) {                to.instances[name] = instance;                if (from && from !== to && instance && instance === oldInstance) {                    if (!to.leaveGuards.size) {                        to.leaveGuards = from.leaveGuards;                    }                    if (!to.updateGuards.size) {                        to.updateGuards = from.updateGuards;                    }                }            }            if (instance &&                to &&                (!from || !isSameRouteRecord(to, from) || !oldInstance)) {                (to.enterCallbacks[name] || []).forEach(callback => callback(instance));            }        }, { flush: 'post' });        return () => {            //========= 第2局部 =============            //...        };    },});

第1局部:初始化变量以及初始化响应式变动监听

routerViewLocationKey拿到目前的路由injectedRoute
// <router-view></router-view>代码const injectedRoute = vue.inject(routerViewLocationKey);

app.use(router)的源码中,咱们能够晓得,routerViewLocationKey代表的是目前的currentRoute,每次路由变动时,会触发finalizeNavigation(),同时更新currentRoute.value=toLocation
因而currentRoute代表的就是目前最新的路由

install(app) {    //...    const router = this;    const reactiveRoute = {};    for (const key in START_LOCATION_NORMALIZED) {        reactiveRoute[key] = vue.computed(() => currentRoute.value[key]);    }    app.provide(routerKey, router); // 如下面代码块所示    app.provide(routeLocationKey, vue.reactive(reactiveRoute));    app.provide(routerViewLocationKey, currentRoute); // 如下面代码块所示    // 省略Vue.unmount的一些逻辑解决...}
const START_LOCATION_NORMALIZED = {    path: '/',    name: undefined,    params: {},    query: {},    hash: '',    fullPath: '/',    matched: [],    meta: {},    redirectedFrom: undefined,};function createRouter(options) {  const currentRoute = vue.shallowRef(START_LOCATION_NORMALIZED);}function finalizeNavigation(toLocation, from, isPush, replace, data) {    //...    currentRoute.value = toLocation;    //...}
routeToDisplay实时同步为目前最新要跳转的路由

应用computed监听路由变动,优先获取props.route,如果有产生路由跳转景象,则routeToDisplay会动态变化

// <router-view></router-view>代码const routeToDisplay = vue.computed(() => props.route || injectedRoute.value);
depth实时同步为目前要跳转的路由对应的matched数组的index

当路由发生变化时,会触发routeToDisplay发生变化,depth应用computed监听routeToDisplay变动

const depth = vue.computed(() => {    let initialDepth = vue.unref(injectedDepth);    const { matched } = routeToDisplay.value;    let matchedRoute;    while ((matchedRoute = matched[initialDepth]) &&        !matchedRoute.components) {        initialDepth++;    }    return initialDepth;});
Vue Router是如何利用depth来一直加载目前路由门路上所有的Component的呢?

如上面代码块所示,咱们在push()->resolve()的流程中,会收集以后路由的所有parent

举个例子,门路path="/child1/child2",那么咱们拿到的matched就是
[{path: '/child1', Component: parent}, {path: '/child1/child2', Component: son}]
function push(to) {    return pushWithRedirect(to);}function pushWithRedirect(to, redirectedFrom) {  const targetLocation = (pendingLocation = resolve(to));  //...}function resolve(location, currentLocation) {    //....    const matched = [];    let parentMatcher = matcher;    while (parentMatcher) {        // reversed order so parents are at the beginning        matched.unshift(parentMatcher.record);        parentMatcher = parentMatcher.parent;    }    return {matched, ...}}

当咱们跳转到path="/child1/child2"时,会首先加载path="/child1"对应的Child1组件,此时injectedDepth=0,在depthcomputed()计算中,得出initialDepth=0,因为matchedRoute.components是存在的,无奈进行initialDepth++
因而此时<router-view>组件拿的数据是matched[0]={path: '/child1', Component: parent}

[{path: '/child1', Component: parent}, {path: '/child1/child2', Component: son}]
// Child1组件<div>目前路由是Child1</div><router-view></router-view>const injectedDepth = vue.inject(viewDepthKey, 0);const depth = vue.computed(() => {    let initialDepth = vue.unref(injectedDepth);    const { matched } = routeToDisplay.value;    let matchedRoute;    while ((matchedRoute = matched[initialDepth]) &&        !matchedRoute.components) {        initialDepth++;    }    return initialDepth;});vue.provide(viewDepthKey, vue.computed(() => depth.value + 1));

此时Child1组件依然有<router-view></router-view>组件,因而咱们再次初始化一次RouterView,加载path="/child1/child2"对应的Child2组件
在下面的代码咱们能够晓得,viewDepthKey会变为depth.value + 1,因而此时<router-view>injectedDepth=1,在depthcomputed()计算中,得出initialDepth=1,因为matchedRoute.components是存在的,无奈进行initialDepth++

从这里咱们能够轻易猜测出,如果门路上有一个片段,比方path='/child1/child2/child3'child2没有对应的components,那么就会跳过这个child2,间接渲染child1child3对应的组件

因而此时<router-view>组件拿的数据是matched[1]={path: '/child1/child2', Component: son}

// Child2组件<div>目前路由是Child2</div>const injectedDepth = vue.inject(viewDepthKey, 0);const depth = vue.computed(() => {    let initialDepth = vue.unref(injectedDepth);    const { matched } = routeToDisplay.value;    let matchedRoute;    while ((matchedRoute = matched[initialDepth]) &&        !matchedRoute.components) {        initialDepth++;    }    return initialDepth;});vue.provide(viewDepthKey, vue.computed(() => depth.value + 1));
matchedRouteRef实时同步为目前最新要跳转的路由对应的matcher
// <router-view></router-view>代码const matchedRouteRef = vue.computed(() => routeToDisplay.value.matched[depth.value]);
更新组件示例,触发beforeRouteEnter回调
const viewRef = vue.ref();vue.watch(() => [viewRef.value, matchedRouteRef.value, props.name], ([instance, to, name], [oldInstance, from, oldName]) => {    if (to) {        to.instances[name] = instance;        if (from && from !== to && instance && instance === oldInstance) {            if (!to.leaveGuards.size) {                to.leaveGuards = from.leaveGuards;            }            if (!to.updateGuards.size) {                to.updateGuards = from.updateGuards;            }        }    }    // trigger beforeRouteEnter next callbacks    if (instance &&        to &&        (!from || !isSameRouteRecord(to, from) || !oldInstance)) {        (to.enterCallbacks[name] || []).forEach(callback => callback(instance));    }}, { flush: 'post' });
viewRef.value是在哪里赋值的呢?请看上面组件渲染的剖析

第2局部:组件渲染

setup()中监听routeToDisplay变动触发组件从新渲染

当响应式数据发生变化时,会触发setup()从新渲染,调用vue.h进行新的Component的渲染更新,此时的viewRef.value通过vue.h赋值

const RouterViewImpl = /*#__PURE__*/ vue.defineComponent({    name: 'RouterView',    setup() {        const viewRef = vue.ref();        return () => {            const route = routeToDisplay.value;            const currentName = props.name; //默认值为"default"            const matchedRoute = matchedRouteRef.value;            const ViewComponent = matchedRoute && matchedRoute.components[currentName];            const component = vue.h(ViewComponent, assign({}, routeProps, attrs, {                onVnodeUnmounted,                ref: viewRef,            }));            //...省略了routeProps和onVnodeUnmounted相干代码逻辑            return (                // pass the vnode to the slot as a prop.                // h and <component :is="..."> both accept vnodes                normalizeSlot(slots.default, { Component: component, route }) ||                component);        }    }});

RouterLink

从上面的代码块能够晓得,整个组件实质就是应用useLink()封装的一系列办法,而后渲染一个"a"标签,下面携带了点击事件、要跳转的href、款式等等

因而这个组件的核心内容就是剖析useLink()到底封装了什么办法,这个局部咱们接下来会进行具体的剖析
const RouterLinkImpl = /*#__PURE__*/ vue.defineComponent({    name: 'RouterLink',    props: {        to: {            type: [String, Object],            required: true,        },        replace: Boolean,        //...    },    useLink,    setup(props, { slots }) {        const link = vue.reactive(useLink(props));        const { options } = vue.inject(routerKey);        const elClass = vue.computed(() => ({            [getLinkClass(props.activeClass, options.linkActiveClass, 'router-link-active')]: link.isActive,            [getLinkClass(props.exactActiveClass, options.linkExactActiveClass, 'router-link-exact-active')]: link.isExactActive,        }));        return () => {            const children = slots.default && slots.default(link);            return props.custom                ? children                : vue.h('a', {                    'aria-current': link.isExactActive                        ? props.ariaCurrentValue                        : null,                    href: link.href,                    onClick: link.navigate,                    class: elClass.value,                }, children);        };    },});

组合式API

onBeforeRouteLeave

在下面app.use(router)的剖析中,咱们能够晓得,一开始就会初始化RouterView组件,而在初始化时,咱们会进行vue.provide(matchedRouteKey, matchedRouteRef),将目前匹配路由的matcher放入到key:matchedRouteKey

install(app) {    const router = this;    app.component('RouterLink', RouterLink);    app.component('RouterView', RouterView);    //...}const RouterViewImpl = /*#__PURE__*/ vue.defineComponent({    name: 'RouterView',    //...    setup(props, { attrs, slots }) {        const routeToDisplay = vue.computed(() => props.route || injectedRoute.value);        const matchedRouteRef = vue.computed(() => routeToDisplay.value.matched[depth.value]);        vue.provide(viewDepthKey, vue.computed(() => depth.value + 1));        vue.provide(matchedRouteKey, matchedRouteRef);        return () => {            //...        }    }})

onBeforeRouteLeave()中,咱们拿到的activeRecord就是目前路由对应的matcher,而后将内部传入的leaveGuard放入到咱们的matcher["leaveGuard"]中,在路由跳转的navigate()办法中进行调用

function onBeforeRouteLeave(leaveGuard) {    if (!vue.getCurrentInstance()) {        warn('getCurrentInstance() returned null. onBeforeRouteLeave() must be called at the top of a setup function');        return;    }    const activeRecord = vue.inject(matchedRouteKey,        // to avoid warning        {}).value;    if (!activeRecord) {        warn('No active route record was found when calling `onBeforeRouteLeave()`. Make sure you call this function inside a component child of <router-view>. Maybe you called it inside of App.vue?');        return;    }    registerGuard(activeRecord, 'leaveGuards', leaveGuard);}function registerGuard(record, name, guard) {    const removeFromList = () => {        record[name].delete(guard);    };    vue.onUnmounted(removeFromList);    vue.onDeactivated(removeFromList);    vue.onActivated(() => {        record[name].add(guard);    });    record[name].add(guard);}

onBeforeRouteUpdate

跟下面剖析的onBeforeRouteLeave截然不同的流程,拿到目前路由对应的matcher,而后将内部传入的updateGuards放入到咱们的matcher["updateGuards"]中,在路由跳转的navigate()办法中进行调用

function onBeforeRouteUpdate(updateGuard) {    if (!vue.getCurrentInstance()) {        warn('getCurrentInstance() returned null. onBeforeRouteUpdate() must be called at the top of a setup function');        return;    }    const activeRecord = vue.inject(matchedRouteKey,        // to avoid warning        {}).value;    if (!activeRecord) {        warn('No active route record was found when calling `onBeforeRouteUpdate()`. Make sure you call this function inside a component child of <router-view>. Maybe you called it inside of App.vue?');        return;    }    registerGuard(activeRecord, 'updateGuards', updateGuard);}function registerGuard(record, name, guard) {    const removeFromList = () => {        record[name].delete(guard);    };    vue.onUnmounted(removeFromList);    vue.onDeactivated(removeFromList);    vue.onActivated(() => {        record[name].add(guard);    });    record[name].add(guard);}

useRouter

在之前的剖析app.use(router)中,咱们能够晓得,咱们将以后的router应用provide进行存储,即app.provide(routerKey, router);

install(app) {    //...    const router = this;    const reactiveRoute = {};    for (const key in START_LOCATION_NORMALIZED) {        reactiveRoute[key] = vue.computed(() => currentRoute.value[key]);    }    app.provide(routerKey, router); // 如下面代码块所示    app.provide(routeLocationKey, vue.reactive(reactiveRoute));    app.provide(routerViewLocationKey, currentRoute); // 如下面代码块所示    // 省略Vue.unmount的一些逻辑解决...}

因而useRouter()实质就是拿到目前Vue应用Vue Router示例

function useRouter() {  return vue.inject(routerKey);}

useRoute

从下面useRouter()的剖析中,咱们能够晓得,routeLocationKey对应的就是以后路由currentRoute封装的响应式对象reactiveRoute
因而const route = useRoute()代表的就是以后路由的响应式对象

function useRoute() {  return vue.inject(routeLocationKey);}

useLink

Vue RouterRouterLink的外部行为作为一个组合式 API 函数公开。它提供了与 v-slotAPI 雷同的拜访属性:

import { RouterLink, useLink } from 'vue-router'import { computed } from 'vue'export default {  name: 'AppLink',  props: {    // 如果应用 TypeScript,请增加 @ts-ignore    ...RouterLink.props,    inactiveClass: String,  },  setup(props) {    const { route, href, isActive, isExactActive, navigate } = useLink(props)    const isExternalLink = computed(      () => typeof props.to === 'string' && props.to.startsWith('http')    )    return { isExternalLink, href, navigate, isActive }  },}
RouterLink组件提供了足够的 props 来满足大多数根本应用程序的需要,但它并未尝试涵盖所有可能的用例,在某些高级状况下,你可能会发现自己应用了v-slot。在大多数中型到大型应用程序中,值得创立一个(如果不是多个)自定义 RouterLink 组件,以在整个应用程序中重用它们。例如导航菜单中的链接,解决内部链接,增加 inactive-class
useLink()是为了扩大RouterLink而服务的

从上面的代码块能够晓得,userLink()次要提供了

  • route: 获取props.to进行router.resolve(),拿到要跳转的新路由的route对象
  • href: 监听route.value.href
  • isActive: 以后路由与props.to局部匹配
  • isExactActive: 以后路由与props.to齐全精准匹配
  • navigate(): 跳转办法,理论还是调用router.push(props.to)/router.replace(props.to)
function useLink(props) {    const router = vue.inject(routerKey);    const currentRoute = vue.inject(routeLocationKey);    const route = vue.computed(() => router.resolve(vue.unref(props.to)));    const activeRecordIndex = vue.computed(() => {        //...    });    const isActive = vue.computed(() => activeRecordIndex.value > -1 &&        includesParams(currentRoute.params, route.value.params));    const isExactActive = vue.computed(() => activeRecordIndex.value > -1 &&        activeRecordIndex.value === currentRoute.matched.length - 1 &&        isSameRouteLocationParams(currentRoute.params, route.value.params));    function navigate(e = {}) {        if (guardEvent(e)) {            return router[vue.unref(props.replace) ? 'replace' : 'push'](vue.unref(props.to)                // avoid uncaught errors are they are logged anyway            ).catch(noop);        }        return Promise.resolve();    }    return {        route,        href: vue.computed(() => route.value.href),        isActive,        isExactActive,        navigate,    };}

其中activeRecordIndex的逻辑比较复杂,咱们摘出来独自剖析下

const activeRecordIndex = vue.computed(() => {    const { matched } = route.value;    const { length } = matched;    const routeMatched = matched[length - 1];    const currentMatched = currentRoute.matched;    if (!routeMatched || !currentMatched.length)        return -1;    const index = currentMatched.findIndex(isSameRouteRecord.bind(null, routeMatched));    if (index > -1)        return index;    const parentRecordPath = getOriginalPath(matched[length - 2]);    return (        length > 1 &&            getOriginalPath(routeMatched) === parentRecordPath &&            currentMatched[currentMatched.length - 1].path !== parentRecordPath            ? currentMatched.findIndex(isSameRouteRecord.bind(null, matched[length - 2]))            : index);});
间接看下面的代码可能有些懵,间接去源码中找对应地位提交的git记录


RouterLink.spec.ts中能够发现减少以下这段代码

it('empty path child is active as if it was the parent when on adjacent child', async () => {  const { wrapper } = await factory(    locations.child.normalized,    { to: locations.childEmpty.string },    locations.childEmpty.normalized  )  expect(wrapper.find('a')!.classes()).toContain('router-link-active')  expect(wrapper.find('a')!.classes()).not.toContain(    'router-link-exact-active'  )})

RouterLink.spec.ts拿到locations.childlocations.childEmpty的值,如下所示

child: {    string: '/parent/child',    normalized: {        fullPath: '/parent/child',        href: '/parent/child',        matched: [records.parent, records.child]    }}childEmpty: {    string: '/parent',    normalized: {        fullPath: '/parent',        href: '/parent',        matched: [records.parent, records.childEmpty]    }}

下面理论就是进行/parent/child->/parent的跳转,并且childEmpty对应的/parent还有两个matched元素,阐明它的子路由的path=""
联合下面router-link-activerouter-link-exact-active/parent/child->/parent呈现的关键字,以及Vue Router源码提交记录的App.vuerouter.ts的批改记录,咱们能够构建出以下的测试文件,具体代码放在github地址

<div id="app-wrapper">    <div class="routerLinkWrapper">        <router-link to="/child/a">跳转到/child/a</router-link>    </div>    <div class="routerLinkWrapper">        <router-link :to="{ name: 'WithChildren' }">跳转到父路由/child</router-link>    </div>    <div class="routerLinkWrapper">        <router-link :to="{ name: 'default-child' }">跳转到没有门路的子路由/child</router-link>    </div>    <!-- route outlet -->    <!-- component matched by the route will render here -->    <router-view></router-view></div>
const routes = [    {path: '/', component: Home},    {        path: '/child',        component: TEST,        name: 'WithChildren',        children: [            { path: '', name: 'default-child', component: TEST },            { path: 'a', name: 'a-child', component: TEST  },        ]    }]

当咱们处于home路由时


当咱们点击跳转到/child/a路由时,咱们能够发现

  • 齐全匹配的路由减少两个class: router-link-activerouter-link-exact-active
  • 门路上只有/child匹配的路由减少了class: router-link-active


从下面的测试后果中,咱们能够大略猜测出
如果<route-link to=xxx>中的to所代表的路由是能够在以后路由中找到的,则加上router-link-activerouter-link-exact-active
如果to所代表的路由(或者它的嵌套parent路由,只有它path=嵌套parent路由的path)是能够在以后路由的嵌套parent路由中找到的,加上router-link-active
当初咱们能够尝试对activeRecordIndex代码进行剖析

const route = computed(() => router.resolve(unref(props.to)))const activeRecordIndex = vue.computed(() => {    const { matched } = route.value;    const { length } = matched;    const routeMatched = matched[length - 1];    const currentMatched = currentRoute.matched;    if (!routeMatched || !currentMatched.length)        return -1;    const index = currentMatched.findIndex(isSameRouteRecord.bind(null, routeMatched));    if (index > -1)        return index;    const parentRecordPath = getOriginalPath(matched[length - 2]);    return (        length > 1 &&            getOriginalPath(routeMatched) === parentRecordPath &&            currentMatched[currentMatched.length - 1].path !== parentRecordPath            ? currentMatched.findIndex(isSameRouteRecord.bind(null, matched[length - 2]))            : index);});

因为<router-link>初始化时就会注册,因而每一个<router-link>都会初始化下面的代码,进行computed的监听,而此时route代表传进来的路由,即

<router-link :to="{ name: 'default-child' }">跳转到没有门路的子路由/child</router-link>

初始化时的props.to="{ name: 'default-child' }"
当目前的路由发生变化时,即currentRoute发生变化时,也会触发computed(fn)从新执行一次,此时会去匹配props.to="/child"所对应的matched[最初一位index]是否在currentRoute对应的matched找到,如果找到了,间接返回index
如果找不到,则应用<router-link>目前对应的props.tomatched的倒数第二个matched[最初第二位index],看看这个倒数第二个matched[最初第二位index]能不能在currentRoute对应的matched找到,如果找到了,间接返回index

应用props.tomatched数组的倒数第二个matched[最初第二位index]的前提是<router-link to="/child">对应的路由是它path=嵌套parent路由的path

activeRecordIndex的用途是什么呢?

在下面userLink的源码剖析中,咱们晓得了isActiveisExactActive的赋值是通过computed计算

const isActive = vue.computed(() => activeRecordIndex.value > -1 &&    includesParams(currentRoute.params, route.value.params));const isExactActive = vue.computed(() => activeRecordIndex.value > -1 &&    activeRecordIndex.value === currentRoute.matched.length - 1 &&    isSameRouteLocationParams(currentRoute.params, route.value.params));

在嵌套路由中找到符合条件的<router-link>isActive=true,然而isExactActive=false
只有合乎currentRoute.matched.length - 1条件下匹配的<router-link>才是isActive=trueisExactActive=true

isActiveisExactActive也就是增加router-link-active/router-link-exact-active的条件

issues剖析

Vue Router中有大量正文,其中蕴含一些issues的正文,本文将进行简略地剖析Vue Router 4.1.6源码中呈现的issues正文

每一个issues都会联合github上的探讨记录以及作者提交的源码修复记录进行剖析,通过issues的剖析,能够明确Vue Router 4.1.6源码中很多看起来不晓得是什么货色的逻辑

issues/685正文剖析

function changeLocation(to, state, replace) {    /**     * if a base tag is provided, and we are on a normal domain, we have to     * respect the provided `base` attribute because pushState() will use it and     * potentially erase anything before the `#` like at     * https://github.com/vuejs/router/issues/685 where a base of     * `/folder/#` but a base of `/` would erase the `/folder/` section. If     * there is no host, the `<base>` tag makes no sense and if there isn't a     * base tag we can just use everything after the `#`.     */    const hashIndex = base.indexOf('#');    const url = hashIndex > -1        ? (location.host && document.querySelector('base')            ? base            : base.slice(hashIndex)) + to        : createBaseLocation() + base + to;    try {        // BROWSER QUIRK        // NOTE: Safari throws a SecurityError when calling this function 100 times in 30 seconds        history[replace ? 'replaceState' : 'pushState'](state, '', url);        historyState.value = state;    }    catch (err) {        {            warn('Error with push/replace State', err);        }        // Force the navigation, this also resets the call count        location[replace ? 'replace' : 'assign'](url);    }}

issues/685问题形容

当部署我的项目到子目录后,拜访[https://test.vladovic.sk/router-bug/](https://test.vladovic.sk/router-bug/)

  • 料想后果门路变为: https://test.vladovic.sk/router-bug/#/
  • 理论门路变为: https://test.vladovic.sk/#/
提出issues的人还提供了正确部署链接: [https://test.vladovic.sk/router](https://test.vladovic.sk/router),可能失常跳转到https://test.vladovic.sk/router/#/,跟谬误链接代码的区别在于<html>文件应用了<base href="/">
<!DOCTYPE html><html lang="en">  <head>    <meta charset="utf-8">    <meta http-equiv="X-UA-Compatible" content="IE=edge">    <meta name="viewport" content="width=device-width,initial-scale=1.0">    <link rel="icon" href="<%= BASE_URL %>favicon.ico">    <title><%= htmlWebpackPlugin.options.title %></title>    <base href="/">  </head>  <body>    <noscript>      <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>    </noscript>    <div id="app"></div>    <!-- built files will be auto injected -->  </body></html>
HTML <base> 元素 指定用于一个文档中蕴含的所有绝对 URL 的根 URL。一份中只能有一个 <base> 元素。
一个文档的根本 URL,能够通过应用 document.baseURI(en-US) 的 JS 脚本查问。如果文档不蕴含 <base> 元素,baseURI 默认为 document.location.href。

issues/685问题产生的起因

VueRouter 4.0.2版本中,changeLocation()间接应用base.indexOf("#")进行前面字段的截取

function changeLocation(to, state, replace) {    // when the base has a `#`, only use that for the URL    const hashIndex = base.indexOf('#');    const url = hashIndex > -1        ? base.slice(hashIndex) + to        : createBaseLocation() + base + to;    try {        // BROWSER QUIRK        // NOTE: Safari throws a SecurityError when calling this function 100 times in 30 seconds        history[replace ? 'replaceState' : 'pushState'](state, '', url);        historyState.value = state;    }    catch (err) {        if ((process.env.NODE_ENV !== 'production')) {            warn('Error with push/replace State', err);        }        else {            console.error(err);        }        // Force the navigation, this also resets the call count        location[replace ? 'replace' : 'assign'](url);    }}

将产生问题的我的项目git clone在本地运行,关上http://localhost:8080/router-bug/时,初始化时会触发changeLocation("/"),从而触发
history['pushState'](state, '', '#/')逻辑

  • 目前的location.hrefhttp://localhost:8080/router-bug/
  • <html></html>对应的<base href="/">
  • 触发了history['pushState'](state, '', '#/')

下面3种条件使得目前的链接更改为:http://localhost:8080/#/

issues/685修复剖析

在github issues的一开始的探讨中,最先倡议的是更改<html></html>对应的<base href="/router-bug/">,因为history.pushState会用到<base>属性
前面提交了一个修复记录,如下图所示:

那为什么要减少<base>标签的判断呢?

咱们在一开始初始化的时候就晓得,createWebHashHistory()反对传入一个base默认的字符串,如果不传入,则取location.pathname+location.search,在下面http://localhost:8080/router-bug/这个例子中,咱们是没有传入一个默认的字符串,因而base="/router-bug/#"

function createWebHashHistory(base) {    // Make sure this implementation is fine in terms of encoding, specially for IE11    // for `file://`, directly use the pathname and ignore the base    // href="https://example.com"的location.pathname也是"/"    base = location.host ? (base || location.pathname + location.search) : '';    // allow the user to provide a `#` in the middle: `/base/#/app`    if (!base.includes('#'))        base += '#';    return createWebHistory(base);}

<base> HTML 元素指定用于文档中所有绝对 URL 的根本 URL。如果文档没有 <base> 元素,则 baseURI 默认为 location.href
很显著,目前<html></html>对应的<base href="/">跟目前的base="/router-bug/#"是抵触的,因为url=base.slice(hashIndex)+to是建设在history.pushState对应的地址蕴含location.pathname的前提下,而可能存在<html></html>对应的<base>就是不蕴含location.pathname

// VueRouter 4.0.2,未修复前的代码function changeLocation(to, state, replace) {    // when the base has a `#`, only use that for the URL    const hashIndex = base.indexOf('#');    const url = hashIndex > -1        ? base.slice(hashIndex) + to        : createBaseLocation() + base + to;}

因而当咱们检测到<html></html>存在<base>标签时,咱们间接应用base(通过格式化解决过的,蕴含location.pathname)的字符串进行以后history.pushState()地址的拼接,杜绝这种可能抵触的状况

上面代码中的base不是<html></html><base>标签!!是咱们解决过的base字符串
// VueRouter修复后的代码function changeLocation(to, state, replace) {    const hashIndex = base.indexOf('#');    const url =      hashIndex > -1        ? (location.host && document.querySelector('base')            ? base            : base.slice(hashIndex)) + to        : createBaseLocation() + base + to}

issues/366正文剖析

function push(to, data) {    // Add to current entry the information of where we are going    // as well as saving the current position    const currentState = assign({},        // use current history state to gracefully handle a wrong call to        // history.replaceState        // https://github.com/vuejs/router/issues/366        historyState.value, history.state, {        forward: to,        scroll: computeScrollPosition(),    });    if (!history.state) {        warn(`history.state seems to have been manually replaced without preserving the necessary values. Make sure to preserve existing history state if you are manually calling history.replaceState:\n\n` +            `history.replaceState(history.state, '', url)\n\n` +            `You can find more information at https://next.router.vuejs.org/guide/migration/#usage-of-history-state.`);    }    changeLocation(currentState.current, currentState, true);    const state = assign({}, buildState(currentLocation.value, to, null), { position: currentState.position + 1 }, data);    changeLocation(to, state, false);    currentLocation.value = to;}

issues/366问题形容

这个issues蕴含了两个人的反馈

手动history.replaceState没有传递以后state,手动触发router.push报错

开发者甲在router.push(...)之前调用window.history.replaceState(...)

window.history.replaceState({}, '', ...)router.push(...)

而后就报错:router push gives DOMException: Failed to execute 'replace' on 'Location': 'http://localhost:8080undefined' is not a valid URL

currentState.current拿不到具体的地址
function push(to, data) {    const currentState = assign({}, history.state, {        forward: to,        scroll: computeScrollPosition(),    });    changeLocation(currentState.current, currentState, true);    // this is the line that fails}
跳转到受权页面,受权胜利后回来进行router.push报错

开发者乙从A页面跳转到B页面后,B页面受权胜利后,重定向回来A页面,而后调用router.push()
报错:router push gives DOMException: Failed to execute 'replace' on 'Location': 'http://localhost:8080undefined' is not a valid URL

currentState.current拿不到具体的地址
function push(to, data) {    const currentState = assign({}, history.state, {        forward: to,        scroll: computeScrollPosition(),    });    changeLocation(currentState.current, currentState, true);    // this is the line that fails}

issues/366问题产生的起因

Vue Router作者在官网文档中强调:
Vue Router 将信息保留在history.state上。如果你有任何手动调用 history.pushState() 的代码,你应该防止它,或者用的router.push()history.replaceState()进行重构:

// 将history.pushState(myState, '', url)// 替换成await router.push(url)history.replaceState({ ...history.state, ...myState }, '')

同样,如果你在调用 history.replaceState() 时没有保留以后状态,你须要传递以后 history.state:

// 将history.replaceState({}, '', url)// 替换成history.replaceState(history.state, '', url)

起因:咱们应用历史状态来保留导航信息,如滚动地位,以前的地址等。

以上内容摘录于Vue Router官网文档

开发者甲的问题将以后的history.state增加进去后就解决了问题,即

// 将history.replaceState({}, '', url)// 替换成history.replaceState(history.state, '', url)

而开发者乙的问题是在Vue Router的A页面->B页面->Vue Router的A页面的过程中失落了以后的history.state

开发者乙也在应用的受权开源库提了amplify-js issues

Vue Router作者则倡议以后history.state应该保留,不应该革除


因而无论开发者甲还是开发者乙,实质都是没有保留好以后的history.state导致的谬误

issues/366修复剖析

通过下面问题的形容以及起因剖析后,咱们晓得要修复问题的要害就是保留好以后的history.state
因而Vue Router作者间接应用变量historyState来进行数据合并


当你理论的window.history.state失落时,咱们还有一个本人保护的historyState数据,它在失常路由状况下historyState.value就等于window.history.state
无论因为什么起因失落了以后浏览器本身的window.history.state,最起码有个本人定义的historyState.value,就能保障currentState.current不会为空,执行到语句history.replaceState或者history.pushState时都能有正确的state

如上面代码块所示,historyState.value初始化就是history.state,并且在每次路由变动时都实时同步更新
function useHistoryStateNavigation(base) {    const historyState = { value: history.state };    function changeLocation(to, state, replace) {        //...        history[replace ? 'replaceState' : 'pushState'](state, '', url)        historyState.value = state;    }}function useHistoryListeners(base, historyState, currentLocation, replace) {    const popStateHandler = ({ state, }) => {        const to = createCurrentLocation(base, location);        //...        if (state) {            currentLocation.value = to;            historyState.value = state;        }    }    window.addEventListener('popstate', popStateHandler);}

issues/328剖析

function resolve(rawLocation, currentLocation) {    //...    return assign({        fullPath,        // keep the hash encoded so fullPath is effectively path + encodedQuery +        // hash        hash,        query:            // if the user is using a custom query lib like qs, we might have            // nested objects, so we keep the query as is, meaning it can contain            // numbers at `$route.query`, but at the point, the user will have to            // use their own type anyway.            // https://github.com/vuejs/router/issues/328#issuecomment-649481567            stringifyQuery$1 === stringifyQuery                ? normalizeQuery(rawLocation.query)                : (rawLocation.query || {}),    }, matchedRoute, {        redirectedFrom: undefined,        href,    });}

issues/328问题形容

Vue Router 4.0.0-alpha.13版本中,并没有思考query嵌套的状况,比方上面这种状况

<div id="app">  目前的地址是:{{ $route.fullPath}}  <ul>    <li><router-link to="/">Home</router-link></li>    <li><router-link :to="{ query: { users: { page: 1 } } }">Page 1</router-link></li>    <li><router-link :to="{ query: { users: { page: 2 } } }">Page 2</router-link></li>  </ul>  <router-view></router-view></div>

点击Page1$route.fullPath=/?users%5Bpage%5D=1
点击Page2$route.fullPath还是/?users%5Bpage%5D=1

现实状况下,应该有变动,$route.fullPath会变为/?users%5Bpage%5D=2

issues/328问题产生的起因

Vue Router 4.0.0-alpha.13版本中,在进行路由跳转时,会触发isSameRouteLocation()的检测

function push(to) {    return pushWithRedirect(to);}function pushWithRedirect(to, redirectedFrom) {    const targetLocation = (pendingLocation = resolve(to));    //...    if (!force && isSameRouteLocation(from, targetLocation)) {       failure = createRouterError(4 /* NAVIGATION_DUPLICATED */, { to: toLocation, from });    }    return (failure ? Promise.resolve(failure) : navigate(toLocation, from));}

isSameRouteLocation()中,会应用isSameLocationObject(a.query, b.query)进行检测,如果此时的query是两个嵌套的Object数据,会返回true,导致Vue Router认为是同一个路由,无奈跳转胜利,天然也无奈触发$route.fullPath扭转

function isSameRouteLocation(a, b) {    //...    return (aLastIndex > -1 &&        aLastIndex === bLastIndex &&        isSameRouteRecord(a.matched[aLastIndex], b.matched[bLastIndex]) &&        isSameLocationObject(a.params, b.params) &&        isSameLocationObject(a.query, b.query) &&        a.hash === b.hash);}function isSameLocationObject(a, b) {    if (Object.keys(a).length !== Object.keys(b).length)        return false;    for (let key in a) {        if (!isSameLocationObjectValue(a[key], b[key]))            return false;    }    return true;}function isSameLocationObjectValue(a, b) {    return Array.isArray(a)        ? isEquivalentArray(a, b)        : Array.isArray(b)            ? isEquivalentArray(b, a)            : a === b;}function isEquivalentArray(a, b) {    return Array.isArray(b)        ? a.length === b.length && a.every((value, i) => value === b[i])        : a.length === 1 && a[0] === b;}
从下面的代码能够晓得,如果咱们的a.query是一个嵌套Object,最终会触发isSameLocationObjectValue()a===b的比拟,最终应该会返回false才对,那为什么会返回true呢?

那是因为在调用isSameRouteLocation()之前会进行resolve(to)操作,而在这个办法中,咱们会进行normalizeQuery(rawLocation.query),无论rawLocation.query是嵌套多少层的ObjectnormalizeQuery()中的'' + value都会变成'[object Object]',因而导致了a===b的比拟理论就是'[object Object]'==='[object Object]',返回true

function createRouter(options) {    function resolve(rawLocation, currentLocation) {        return assign({            fullPath,            hash,            query: normalizeQuery(rawLocation.query),        }, matchedRoute, {            redirectedFrom: undefined,            href: routerHistory.base + fullPath,        });    }    function push(to) {        return pushWithRedirect(to);    }    function pushWithRedirect(to, redirectedFrom) {        const targetLocation = (pendingLocation = resolve(to));        //...        if (!force && isSameRouteLocation(from, targetLocation)) {            failure = createRouterError(4 /* NAVIGATION_DUPLICATED */, { to: toLocation, from });        }    }}function normalizeQuery(query) {    const normalizedQuery = {};    for (let key in query) {        let value = query[key];        if (value !== undefined) {            normalizedQuery[key] = Array.isArray(value)                ? value.map(v => (v == null ? null : '' + v))                : value == null                    ? value                    : '' + value;        }    }    return normalizedQuery;}

issues/328修复剖析

初始化可传入stringifyQuery,外部对query不进行解决

因为query可能是简单的构造,因而修复该问题第一思考的点就是放开给开发者本人解析

开发者能够在初始化createRouter()传入自定义的parseQuery()stringifyQuery()办法,开发者自行解析和转化目前的query参数

  • 如果开发者不传入自定义的stringifyQuery()办法,那么stringifyQuery就会等于originalStringifyQuery(一个Vue Router内置的stringifyQuery办法),这个时候query就会应用normalizeQuery(rawLocation.query)进行数据的整顿,最终返回的还是一个Object对象
  • 如果开发者传入自定义的stringifyQuery()办法,那么就不会触发任何解决,还是应用rawLocation.query,在下面示例中就是一个嵌套的Object对象,防止应用normalizeQuery()'' + value变成'[object Object]'的状况

isSameRouteLocation比拟query时,应用它们stringify解决后的字符串进行比拟

开发者能够自定义传入stringifyQuery()进行简单构造的解决,而后返回字符串进行比拟
如果不传入stringifyQuery(),则应用默认的办法进行stringify,而后依据返回的字符串进行比拟

默认办法的stringify不会思考简单的数据结构,只会当做一般对象进行stringify

issues/1124剖析

function insertMatcher(matcher) {    let i = 0;    while (i < matchers.length &&        comparePathParserScore(matcher, matchers[i]) >= 0 &&        // Adding children with empty path should still appear before the parent        // https://github.com/vuejs/router/issues/1124        (matcher.record.path !== matchers[i].record.path ||            !isRecordChildOf(matcher, matchers[i])))        i++;    matchers.splice(i, 0, matcher);    // only add the original record to the name map    if (matcher.record.name && !isAliasRecord(matcher))        matcherMap.set(matcher.record.name, matcher);}

issues/1124问题形容

Vue Router 4.0.11

  • 应用动静增加路由addRoute()为以后的name="Root"路由增加子路由后

    • 咱们想要拜访Component:B,应用了router.push("/"),然而无奈渲染出Component:B,它渲染的是它的上一级门路Component: Root
    • 而如果咱们应用router.push({name: 'Home'})时,就能失常拜访Component:B
  • 如果咱们不应用动静增加路由,间接在初始化的时候,如上面正文children那样,间接增加Component:B,当咱们应用router.push("/"),能够失常渲染出Component:B
const routes = [    {        path: '/',        name: 'Root',        component: Root,        // Work with non dynamic add and empty path        /*children: [          {            path: '',            component: B          }        ]*/    }];// Doesn't work with empty path and dynamic addingrouter.addRoute('Root', {    path: '',    name: 'Home',    component: B});

issues/1124问题产生的起因

当动态增加路由时,因为是递归调用addRoute(),即父addRoute()->子addRoute->子insertMatcher()->父addRoute(),因而子matcher是排在父matcher后面的,因为从《Vue3相干源码-Vue Router源码解析(一)》文章中计算路由权重的逻辑能够晓得,门路雷同分数则雷同,comparePathParserScore()的值为0,因而先调用insertMatcher(),地位就越靠前,因而子路由地位靠前

function addRoute(record, parent, originalRecord) {    if ('children' in mainNormalizedRecord) {        const children = mainNormalizedRecord.children        for (let i = 0; i < children.length; i++) {            addRoute(                children[i],                matcher,                originalRecord && originalRecord.children[i]            )        }    }    //...    insertMatcher(matcher);}function insertMatcher(matcher) {    let i = 0;    while (i < matchers.length &&        comparePathParserScore(matcher, matchers[i]) >= 0 &&        // Adding children with empty path should still appear before the parent        // https://github.com/vuejs/router/issues/1124        (matcher.record.path !== matchers[i].record.path))        i++;    matchers.splice(i, 0, matcher);    // only add the original record to the name map    if (matcher.record.name && !isAliasRecord(matcher))        matcherMap.set(matcher.record.name, matcher);}

然而动静增加路由时,同样是触发matcher.addRoute(),只不过因为是动静增加,因而要增加的新路由的parent之前曾经插入到matchers数组中去了,由下面的剖析能够晓得,路由权重雷同,因而先调用insertMatcher(),地位就越靠前,此时子路由地位靠后

function createRouter(options) {    function addRoute(parentOrRoute, route) {        let parent;        let record;        if (isRouteName(parentOrRoute)) {            parent = matcher.getRecordMatcher(parentOrRoute);            record = route;        }        else {            record = parentOrRoute;        }        return matcher.addRoute(record, parent);    }}function createRouterMatcher(routes, globalOptions) {    function addRoute(record, parent, originalRecord) {        if ('children' in mainNormalizedRecord) {            const children = mainNormalizedRecord.children            for (let i = 0; i < children.length; i++) {                addRoute(                    children[i],                    matcher,                    originalRecord && originalRecord.children[i]                )            }        }        //...        insertMatcher(matcher);    }}

在上一篇文章《Vue3相干源码-Vue Router源码解析(一)》的剖析中,咱们晓得,当咱们应用router.push("/")或者router.push({path: "/"})时,会触发正则表达式的匹配,如上面代码所示,matchers.find()会优先匹配地位靠前的路由matcher,从而产生了动静增加子路由找不到子路由(地位比拟靠后),初始化增加子路由可能渲染子路由(地位比拟靠前)的状况

path = location.path;matcher = matchers.find(m => m.re.test(path));if (matcher) {    params = matcher.parse(path);    name = matcher.record.name;}

issues/1124修复剖析

既然是增加程序导致的问题,那么只有让子路由动静增加时,遇到它的parent不要i++即可,如上面修复提交代码所示,当子路由动静增加时,检测目前比照的是不是它的parent,如果是它的parent,则阻止i++,子路由地位就能够顺利地放在它的parent后面

issues/916剖析

很长的一段navigte()失败之后的解决.....简化下
function setupListeners() {    removeHistoryListener = routerHistory.listen((to, _from, info) => {        //...        const toLocation = resolve(to);        const from = currentRoute.value;        navigate(toLocation, from)            .catch((error) => {                if (isNavigationFailure(error, 4 /* ErrorTypes.NAVIGATION_ABORTED */ | 8 /* ErrorTypes.NAVIGATION_CANCELLED */)) {                    return error;                }                if (isNavigationFailure(error, 2 /* ErrorTypes.NAVIGATION_GUARD_REDIRECT */)) {                    pushWithRedirect(error.to, toLocation)                        .then(failure => {                            // manual change in hash history #916 ending up in the URL not                            // changing, but it was changed by the manual url change, so we                            // need to manually change it ourselves                            if (isNavigationFailure(failure, 4 /* ErrorTypes.NAVIGATION_ABORTED */ |                                16 /* ErrorTypes.NAVIGATION_DUPLICATED */) &&                                !info.delta &&                                info.type === NavigationType.pop) {                                routerHistory.go(-1, false);                            }                        })                        .catch(noop);                    // avoid the then branch                    return Promise.reject();                }                // do not restore history on unknown direction                if (info.delta) {                    routerHistory.go(-info.delta, false);                }                // unrecognized error, transfer to the global handler                return triggerError(error, toLocation, from);            })            .then((failure) => {                failure =                    failure ||                    finalizeNavigation(                        // after navigation, all matched components are resolved                        toLocation, from, false);                // revert the navigation                if (failure) {                    if (info.delta &&                        // a new navigation has been triggered, so we do not want to revert, that will change the current history                        // entry while a different route is displayed                        !isNavigationFailure(failure, 8 /* ErrorTypes.NAVIGATION_CANCELLED */)) {                        routerHistory.go(-info.delta, false);                    } else if (info.type === NavigationType.pop &&                        isNavigationFailure(failure, 4 /* ErrorTypes.NAVIGATION_ABORTED */ | 16 /* ErrorTypes.NAVIGATION_DUPLICATED */)) {                        // manual change in hash history #916                        // it's like a push but lacks the information of the direction                        routerHistory.go(-1, false);                    }                }                triggerAfterEach(toLocation, from, failure);            })            .catch(noop);    });}
function setupListeners() {    removeHistoryListener = routerHistory.listen((to, _from, info) => {        //...        const toLocation = resolve(to);        const from = currentRoute.value;        navigate(toLocation, from)            .catch((error) => {                // error是 NAVIGATION_ABORTED/NAVIGATION_CANCELLED                return error;                // error是 NAVIGATION_GUARD_REDIRECT                pushWithRedirect(error.to, toLocation)                    .then(failure => {                        if (isNavigationFailure(failure, 4 /* ErrorTypes.NAVIGATION_ABORTED */ |                            16 /* ErrorTypes.NAVIGATION_DUPLICATED */) &&                            !info.delta &&                            info.type === NavigationType.pop) {                            routerHistory.go(-1, false);                        }                    })            })            .then((failure) => {                if (failure) {                    if (info.type === NavigationType.pop &&                        isNavigationFailure(failure, 4 /* ErrorTypes.NAVIGATION_ABORTED */ |                         16 /* ErrorTypes.NAVIGATION_DUPLICATED */)) {                        routerHistory.go(-1, false);                    }                }            })    });}

issues/916问题形容

目前有两个路由,/login路由和/about路由,它们配置了一个全局的导航守卫,当遇到/about路由时,会重定向到/login路由

router.beforeEach((to, from) => {    if (to.path.includes('/login')) {        return true;    } else {        return {            path: '/login'        }    }})

目前问题是:

  • 目前是/login路由,各方面失常,手动在浏览器地址更改/login->/about
  • 冀望行为是:因为router.beforeEach配置了重定向跳转,浏览器地址会从新变为/login,页面也还是保留在Login组件
  • 理论体现是:浏览器地址是/about,页面保留在Login组件,造成浏览器地址跟理论映射组件不合乎的bug

issues/916问题产生的起因

issues/916修复剖析

如果导航守卫next()返回的是路由数据,会触发popStateHandler()->navigate()->pushWithRedirect(),而后返回NAVIGATION_DUPLICATED,因而咱们要做的就是回退一个路由,并且不触发组件更新,即routerHistory.go(-1, false)

因为NAVIGATION_DUPLICATED就意味着要重定向的这个新路由(/login)跟启动重定向路由(/about)之前的路由(/login)是反复的,那么这个启动重定向路由(/about)就得回退,因为它(/about)势必会不胜利

除了在pushWithRedirect()上修复谬误之外,还在navigate().then()上也进行同种状态的判断

这是为什么呢?除了next("/login")之外,还有状况导致谬误吗?

这是因为除了next("/login"),还有一种可能就是next(false)也会导致谬误,即

router.beforeEach((to, from) => {    if (to.path.includes('/login')) {        return true;    } else {        return false;        // return {        //     path: '/login'        // }    }})

当重定向路由改为false时,如上面代码块所示,navigate()会返回NAVIGATION_ABORTED的谬误,从而触发navigate().catch(()=> error)
Promise.catch()中返回值,这个值也会包裹触发下一个then(),也就是说NAVIGATION_ABORTED会传递给navigate().catch().then()中,因而还须要在then()外面进行routerHistory.go(-1, false)

NAVIGATION_ABORTED意味着这种routerHistory.listen传递的to路由因为next()返回false而勾销,因而须要回退一个路由,因为这个to路由曾经扭转浏览器的记录了!
function setupListeners() {    removeHistoryListener = routerHistory.listen((to, _from, info) => {        //...        const toLocation = resolve(to);        const from = currentRoute.value;        navigate(toLocation, from)            .catch((error) => {                // error是 NAVIGATION_ABORTED/NAVIGATION_CANCELLED                return error;            })            .then((failure) => {                if (failure) {                    if (info.type === NavigationType.pop &&                        isNavigationFailure(failure, 4 /* ErrorTypes.NAVIGATION_ABORTED */ |                         16 /* ErrorTypes.NAVIGATION_DUPLICATED */)) {                        routerHistory.go(-1, false);                    }                }            })    });}

总结

1. 内部定义的路由,是如何在Vue Router外部建立联系的?

Vue Router反对多种门路写法,动态门路、一般的动静门路以及动静门路和正则表达式的联合
初始化时会对routes进行解析,依据多种门路的不同状态解析出对应的正则表达式、路由权重和路由其它数据,包含组件名称、组件等等

2. Vue Router是如何实现pushreplacepop操作的?

  • push/replace

    • 通过resolve()整顿出跳转数据的对象,该对象包含找到一开始初始化routes对应的matched以及跳转的残缺门路
    • 而后通过navigate()进行一系列导航守卫的调用
    • 而后通过changeLocation(),也就是window.history.pushState/replaceState()实现的以后浏览器门路替换
    • 更新目前的currentRoute,从而触发<route-view>中的injectedRoute->routeToDisplay等响应式数据发生变化,从而触发<route-view>setup函数从新渲染currentRoute的matched携带的Component,实现路由更新性能
  • pop

    • 初始化会进行setupListeners()进行routerHistory.listen事件的监听
    • 监听popstate事件,后退事件触发时,触发初始化监听的routerHistory.listen事件
    • 通过resolve()整顿出跳转数据的对象,该对象包含找到一开始初始化routes对应的matched以及跳转的残缺门路
    • 而后通过navigate()进行一系列导航守卫的调用
    • 而后通过changeLocation(),也就是window.history.pushState/replaceState()实现的以后浏览器门路替换
    • 更新目前的currentRoute,从而触发<route-view>中的injectedRoute->routeToDisplay等响应式数据发生变化,从而触发<route-view>setup函数从新渲染currentRoute的matched携带的Component,实现路由更新性能

3. Vue Router是如何命中多层嵌套路由,比方/parent/child/child1须要加载多个组件,是如何实现的?

在每次路由跳转的过程中,会解析出以后路由对应的matcher对象,并且将它所有的parent matcher都退出到matched数组中
在实现pushreplacepop操作时,每一个路由都会在router-view中计算出目前对应的嵌套深度,而后依据嵌套深度,拿到下面matched对应的item,实现路由组件的渲染

4. Vue Router有什么导航守卫?触发的流程是怎么的?

通过Promise链式顺序调用多个导航守卫,在每次路由跳转时,会触发push/replace()->navigate()->finalizeNavigation(),导航守卫就是在navigate()中进行链式调用,能够在其中一个导航守卫中中断流程,从而中断整个路由的跳转

参考官网文档的材料,咱们能够推断出/child1->/child2导航守卫的调用程序为
  • 【组件守卫】在失活的组件里调用 beforeRouteLeave 守卫
  • 【全局守卫】beforeEach
  • 【路由守卫】beforeEnter
  • 解析异步路由组件
  • 【组件守卫】在被激活的组件里调用 beforeRouteEnter(无法访问this,实例未创立)
  • 【全局守卫】beforeResolve
  • 导航被确认
  • 【全局守卫】afterEach
  • 【vue生命周期】beforeCreatecreatedbeforeMount
  • 调用 beforeRouteEnter 守卫中传给 next 的回调函数,创立好的组件实例会作为回调函数的参数传入
  • 【vue生命周期】mounted

    参考官网文档的材料,咱们能够推断前途由/user/:id/user/a->/user/b导航守卫的调用程序为
  • 【全局守卫】beforeEach
  • 【组件守卫】在重用的组件里调用 beforeRouteUpdate 守卫(2.2+)
  • 【全局守卫】beforeResolve
  • 【全局守卫】afterEach
  • 【vue生命周期】beforeUpdateupdated

5. Vue Router的导航守卫是如何做到链式调用的?

navigate()的源码中,咱们截取beforeEachbeforeRouteUpdate的片段进行剖析,次要波及有几个点:

  • runGuardQueue(guards)实质就是链式不停地调用promise.then(),而后执行guards[x](),最终实现某一个阶段,比方beforeEach阶段之后,再应用guards收集下一个阶段的function数组,而后再启用runGuardQueue(guards)应用promise.then()一直执行guards外面的办法
  • guards.push()增加的是一个Promise,传递的是在内部注册的办法guardtofrom三个参数
function navigate(to, from) {    return (runGuardQueue(guards)        .then(() => {            // check global guards beforeEach            guards = [];            for (const guard of beforeGuards.list()) {                guards.push(guardToPromiseFn(guard, to, from));            }            guards.push(canceledNavigationCheck);            return runGuardQueue(guards);        })        .then(() => {            // check in components beforeRouteUpdate            guards = extractComponentsGuards(updatingRecords, 'beforeRouteUpdate', to, from);            for (const record of updatingRecords) {                record.updateGuards.forEach(guard => {                    guards.push(guardToPromiseFn(guard, to, from));                });            }            guards.push(canceledNavigationCheck);            // run the queue of per route beforeEnter guards            return runGuardQueue(guards);        })        //....    )}function runGuardQueue(guards) {    return guards.reduce(        (promise, guard) => promise.then(() => guard()),        Promise.resolve());}

对于guards.push()增加的Promise,会判断内部function(也就是guard)有多少个参数,而后调用不同的条件逻辑,最终依据内部注册的办法,比方beforeEach()返回的值,进行Promise()的返回,从而造成链式调用

Function: length就是有多少个parameters参数,guard.length次要区别在于传不传next参数,能够看上面代码块的正文局部
如果你在内部的办法,比方beforeEach携带了next参数,你就必须调用它,不然会报错
//router.beforeEach((to, from)=> {return false;}//router.beforeEach((to, from, next)=> {next(false);}function guardToPromiseFn(guard, to, from, record, name) {    // keep a reference to the enterCallbackArray to prevent pushing callbacks if a new navigation took place    const enterCallbackArray = record &&        // name is defined if record is because of the function overload        (record.enterCallbacks[name] = record.enterCallbacks[name] || []);    return () => new Promise((resolve, reject) => {        const next = (valid) => {            resolve();        };        const guardReturn = guard.call(record && record.instances[name], to, from, canOnlyBeCalledOnce(next, to, from));        let guardCall = Promise.resolve(guardReturn);        if (guard.length < 3)            guardCall = guardCall.then(next);        if (guard.length > 2) {            //...解决有next()的状况        }        guardCall.catch(err => reject(err));    });}function canOnlyBeCalledOnce(next, to, from) {    let called = 0;    return function () {        next._called = true;        if (called === 1)            next.apply(null, arguments);    };}

6. Vue RouterbeforeRouteEnterbeforeRouteUpdate的触发机会

如果复用一个路由,比方/user/:id会导致不同的path会应用同一个路由,那么就不会调用beforeRouteEnter,因而咱们须要在beforeRouteUpdate获取数据

export default {  data() {    return {      post: null,      error: null,    }  },  beforeRouteEnter(to, from, next) {    getPost(to.params.id, (err, post) => {      next(vm => vm.setData(err, post))    })  },  // 路由扭转前,组件就曾经渲染完了  // 逻辑稍稍不同  async beforeRouteUpdate(to, from) {    this.post = null    try {      this.post = await getPost(to.params.id)    } catch (error) {      this.error = error.toString()    }  },

7. Vue Routerrouterouter的区别

router是目前Vue应用Vue Router示例,具备多个办法和多个对象数据,蕴含currentRoute
route是以后路由的响应式对象,内容实质就是currentRoute

8. hash模式跟h5 history模式在Vue Router中有什么区别?

hash模式:监听浏览器地址hash值变动,应用pushState/replaceState进行路由地址的扭转,从而触发组件渲染扭转,不须要服务器配合配置对应的地址
history模式:扭转浏览器url地址,从而触发浏览器向服务器发送申请,须要服务器配合配置对应的地址

9. Vue Routerhash模式重定向后还会保留浏览记录吗?比方重定向后再应用router.go(-1)会返回重定向之前的页面吗?

Vue Routerhash模式中,如果产生重定向,从push()->pushWithRedirect()的源码能够晓得,会在navigate()之前就进行重定向的跳转,因而不会触发finalizeNavigation()pushState()办法往浏览器中留下记录,因而不会在Vue Routerhash模式中,不会保留浏览器history state记录

function pushWithRedirect(to, redirectedFrom) {    const targetLocation = (pendingLocation = resolve(to));    const from = currentRoute.value;    const data = to.state;    const force = to.force;    // to could be a string where `replace` is a function    const replace = to.replace === true;    const shouldRedirect = handleRedirectRecord(targetLocation);    if (shouldRedirect) {        //...解决重定向的逻辑        return pushWithRedirect(...)    }    // if it was a redirect we already called `pushWithRedirect` above    const toLocation = targetLocation;    toLocation.redirectedFrom = redirectedFrom;    //...解决SameRouteLocation的状况    // ...去除failure的解决,默认都胜利    return navigate(toLocation, from)        .then((failure) => {            failure = finalizeNavigation(toLocation, from, true, replace, data);            triggerAfterEach(toLocation, from, failure);            return failure;        });}

10. Vue Routerhash模式什么中央最容易导致路由切换失败?

在之前的剖析中,咱们能够晓得,咱们能够应用go(-1, false)进行pauseListeners()的调用

function go(delta, triggerListeners = true) {    if (!triggerListeners)        historyListeners.pauseListeners();    history.go(delta);}function listen(callback) {    // set up the listener and prepare teardown callbacks    listeners.push(callback);    const teardown = () => {        const index = listeners.indexOf(callback);        if (index > -1)            listeners.splice(index, 1);    };    teardowns.push(teardown);    return teardown;}function pauseListeners() {    pauseState = currentLocation.value;}

从下面的代码中,咱们能够总结出几个要害的问题:

  1. pauseListeners()是如何暂停监听办法执行的?pauseState什么时候应用?
  2. 什么时候触发listen()注册监听办法?
  3. listen()注册的监听办法有什么用途?
  4. 为什么要应用pauseListeners()暂停监听?

pauseListeners()是如何暂停监听办法执行的?

pauseState是如何做到暂停监听办法执行的?

当触发后退事件时,会检测pauseState是否存在以及是否等于后退之前的路由,如果是的话,则间接return,阻止后续的listeners的循环调用,达到暂停listeners的目标

const popStateHandler = ({ state, }) => {    const from = currentLocation.value;    //....    let delta = 0;    if (state) {        //....        // ignore the popstate and reset the pauseState        if (pauseState && pauseState === from) {            pauseState = null;            return;        }        delta = fromState ? state.position - fromState.position : 0;    } else {        replace(to);    }    listeners.forEach(listener => {      //...    });};window.addEventListener('popstate', popStateHandler);

什么时候触发listen()注册监听办法?

在上一篇文章的setupListeners()注册pop操作相干监听办法的剖析中,咱们能够晓得,初始化app.use(router)会触发router.install(app),而后进行一次push()操作,此时就是初始化阶段!readypush()->navigate()->finalizeNavigation(),而后触发listen()注册监听办法

function finalizeNavigation(toLocation, from, isPush, replace, data) {    markAsReady();}function markAsReady(err) {    if (!ready) {        ready = !err;        setupListeners();    }    return err;}function setupListeners() {    removeHistoryListener = routerHistory.listen((to, _from, info) => {       //...navigate()    });}

listen()监听办法有什么用途?

在上一篇文章的setupListeners()注册pop操作相干监听办法的剖析中,咱们能够晓得,在原生popstate后退事件触发时,会触发对应的listeners监听办法执行,将以后的路由作为参数传递过来,因而listen()监听办法实质的性能就是监听后退事件,执行一系列导航守卫,实现路由切换相干逻辑

pop()事件的监听实质跟push()执行的逻辑是统一的,都是切换路由映射的组件
function useHistoryListeners(base, historyState, currentLocation, replace) {    const popStateHandler = ({ state, }) => {        const to = createCurrentLocation(base, location);        if (state) {              currentLocation.value = to;        }        listeners.forEach(listener => {            listener(currentLocation.value, from, {                delta,                type: NavigationType.pop,                direction: delta                    ? delta > 0                        ? NavigationDirection.forward                        : NavigationDirection.back                    : NavigationDirection.unknown,            });        });    };    window.addEventListener('popstate', popStateHandler);    return {        pauseListeners,        listen,        destroy,    };}

listen()监听办法触发后,具体做了什么?

当后退事件触发,listen()监听办法触发,实质就是执行了navigate()导航,进行了对应的组件切换性能

手动模仿了被动触发路由切换,然而不会进行pushState/replaceState扭转以后的路由地址,只是扭转currentRoute,触发<router-view>的组件从新渲染
function setupListeners() {    removeHistoryListener = routerHistory.listen((to, _from, info) => {        navigate(toLocation, from).then(() => {            // false代表不会触发pushState/replaceState            finalizeNavigation(toLocation, from, false);        });    });}function finalizeNavigation(toLocation, from, isPush, replace, data) {    if (isPush) {        if (replace || isFirstNavigation)            routerHistory.replace(toLocation.fullPath, assign({                scroll: isFirstNavigation && state && state.scroll,            }, data));        else            routerHistory.push(toLocation.fullPath, data);    }    currentRoute.value = toLocation;}

什么时候调用pauseListeners()pauseListeners()的作用是什么?

go()办法中,如果咱们应用router.go(-1, false),那么咱们就会触发pauseListeners()->pauseState = currentLocation.value
路由后退会触发popStateHandler(),此时咱们曾经注册pauseState = currentLocation.value,因而在popStateHandler()中会阻止后续的listeners的循环调用,达到暂停listeners的目标

function go(delta, triggerListeners = true) {    if (!triggerListeners)        historyListeners.pauseListeners();    history.go(delta);}
咱们什么时候调用go(-xxxx, false)?第二个参数跟popStateHandler()有什么分割?

Vue Router 4.1.6的源码中,咱们能够发现,所有波及到go(-xxxx, false)都集中在后退事件对应的监听办法中,如上面代码块所示,在后退事件触发,进行组件的切换过程中,Vue Router可能会产生多种不同类型的路由切换失败,比方下面剖析的issues/916一样,当咱们手动更改路由,新路由重定向的路由跟目前路由反复时,咱们就须要被动后退一个路由,然而咱们不心愿从新渲染组件,只是单纯想要回进路由记录,那么咱们就能够调用routerHistory.go(-1, false)触发pauseListeners(),从而暂停listeners执行,从而阻止navigate()函数的调用,阻止导航守卫的产生和scroll滚动地位的复原等一系列逻辑

function setupListeners() {    removeHistoryListener = routerHistory.listen((to, _from, info) => {        navigate(toLocation, from)            .catch((error) => {               if (isNavigationFailure(error, 2 /* ErrorTypes.NAVIGATION_GUARD_REDIRECT */)) {                    pushWithRedirect(error.to, toLocation)                        .then(failure => {                            if (isNavigationFailure(failure, 4 /* ErrorTypes.NAVIGATION_ABORTED */ |                                16 /* ErrorTypes.NAVIGATION_DUPLICATED */) &&                                !info.delta &&                                info.type === NavigationType.pop) {                                routerHistory.go(-1, false);                            }                        });                } else {                    if (info.delta) {                        routerHistory.go(-info.delta, false);                    }                }            })            .then((failure) => {                // false代表不会触发pushState/replaceState                failure = failure || finalizeNavigation(toLocation, from, false);                if (failure) {                    if (info.delta &&                        // a new navigation has been triggered, so we do not want to revert, that will change the current history                        // entry while a different route is displayed                        !isNavigationFailure(failure, 8 /* ErrorTypes.NAVIGATION_CANCELLED */)) {                        routerHistory.go(-info.delta, false);                    }                    else if (info.type === NavigationType.pop &&                        isNavigationFailure(failure, 4 /* ErrorTypes.NAVIGATION_ABORTED */ | 16 /* ErrorTypes.NAVIGATION_DUPLICATED */)) {                        // manual change in hash history #916                        // it's like a push but lacks the information of the direction                        routerHistory.go(-1, false);                    }                }            })    });}

参考文章

  1. 7张图,从零实现一个简易版Vue-Router,太通俗易懂了!
  2. VueRouter4路由权重

Vue系列其它文章

  1. Vue2源码-响应式原理浅析
  2. Vue2源码-整体流程浅析
  3. Vue2源码-双端比拟diff算法 patchVNode流程浅析
  4. Vue3源码-响应式零碎-依赖收集和派发更新流程浅析
  5. Vue3源码-响应式零碎-Object、Array数据响应式总结
  6. Vue3源码-响应式零碎-Set、Map数据响应式总结
  7. Vue3源码-响应式零碎-ref、shallow、readonly相干浅析
  8. Vue3源码-整体流程浅析
  9. Vue3源码-diff算法-patchKeyChildren流程浅析