关于前端:Vue3相关源码Vue-Router源码解析二

本文基于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 adding
router.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流程浅析

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理