乐趣区

关于前端: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 流程浅析
退出移动版