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

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

本文重点剖析Vue RouterWebHashHistory模式,不会对WebHistoryMemoryHistory模式过多剖析

文章内容

Vue Router的初始化代码动手,逐渐剖析对应的代码流程和波及到的操作方法(pushreplacepop

本文将着重于:

  • Vue Router是如何利用routes数组建设路由映射,路由权重是如何初始化
  • Vue Routerpushreplacepop流程具体执行了什么?是如何找到对应的路由数据
  • Vue Router提供的RouterViewRouterLink源码剖析
  • Vue Router提供的多个组合式API的源码剖析,包含onBeforeRouteLeaveonBeforeRouteLeaveuseRouteruseRouteuseLink
  • Vue Router源码正文中波及到的issues的问题解说和对应的修复代码剖析

本文的最初将基于多个问题进行Vue Router源码的系统性总结

前置常识

Vue Router介绍

摘录于Vue Router官网文档

Vue Router 是 Vue.js 的官网路由。它与 Vue.js 外围深度集成,让用 Vue.js 构建单页利用变得轻而易举。性能包含:

  • 嵌套路由映射
  • 动静路由抉择
  • 模块化、基于组件的路由配置
  • 路由参数、查问、通配符
  • 展现由 Vue.js 的过渡零碎提供的过渡成果
  • 粗疏的导航管制
  • 主动激活 CSS 类的链接
  • HTML5 history 模式或 hash 模式
  • 可定制的滚动行为
  • URL 的正确编码

window.location属性

属性 形容
href https://test.example.com:8090/vue/router#test?search=1
protocol https:
host/hostname test.example.com
port 8090
pathname /vue/router
search ?search=1
hash #test

<base>:文档根 URL 元素

摘录于https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base

  • HTML <base> 元素 指定用于一个文档中蕴含的所有绝对 URL 的根 URL
  • 只能有一个 <base> 元素
  • 一个文档的根本 URL,能够通过应用 document.baseURI(en-US) 的 JS 脚本查问,如果文档不蕴含 <base> 元素,baseURI 默认为 document.location.href

前置问题

  • 内部定义的路由,是如何在Vue Router外部建立联系的
  • Vue Router是如何实现pushreplacepop操作的
  • Vue Router是如何命中多层嵌套路由,比方/parent/child/child1须要加载多个组件,是如何实现的
  • Vue Router有什么导航?触发的流程是怎么的
  • Vue Router的导航守卫是如何做到链式调用的
  • Vue RouterbeforeRouteEnterbeforeRouteUpdate的触发机会
  • Vue Routerrouterouter的区别
  • hash模式跟h5 history模式在Vue Router中有什么区别
  • Vue Routerhash模式重定向后还会保留浏览记录吗?比方重定向后再应用router.go(-1)会返回重定向之前的页面吗
  • Vue Routerhash模式什么中央最容易导致路由切换失败

下面所有问题将在第二篇文章Vue3相干源码-Vue Router源码解析(二)中进行解答

示例代码

代码来自于Vue 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')

剖析的外围示例代码

const router = VueRouter.createRouter({
  history: VueRouter.createWebHashHistory(),
  routes
})
const app = Vue.createApp({})
app.use(router)

上面将依照创立history、初始化router、Vue应用router的程序进行源码剖析

createWebHashHistory创立history

hash模式是用createWebHashHistory() 创立的历史记录模式

从上面代码能够晓得,解决了base数据:如果没有传入url,则拼接location.pathname + location.search,而后加上#
最初还是调用了createWebHistory(base)的办法进行history的创立

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);
}

间接看下面的代码会有点懵,间接看官网文档会比拟好了解点,上面内容参考自https://router.vuejs.org/zh/api/#createwebhashhistory,是createWebHashHistory(base)的参数阐明

Parameter Type Description
base string 提供可选的 base。默认是 location.pathname + location.search。如果 head 中有一个 <base>,它的值将被疏忽,而采纳这个参数。但请留神它会影响所有的 history.pushState() 调用,这意味着如果你应用一个 <base> 标签,它的 href 值必须与这个参数相匹配 (请疏忽 # 前面的所有内容)
// at https://example.com/folder
createWebHashHistory() // 给出的网址为 `https://example.com/folder#`
createWebHashHistory('/folder/') // 给出的网址为 `https://example.com/folder/#`
// 如果在 base 中提供了 `#`,则它不会被 `createWebHashHistory` 增加
createWebHashHistory('/folder/#/app/') // 给出的网址为 `https://example.com/folder/#/app/`
// 你应该防止这样做,因为它会更改原始 url 并打断正在复制的 url
createWebHashHistory('/other-folder/') // 给出的网址为 `https://example.com/other-folder/#`

// at file:///usr/etc/folder/index.html
// 对于没有 `host` 的地位,base被疏忽
createWebHashHistory('/iAmIgnored') // 给出的网址为 `file:///usr/etc/folder/index.html#`

从下面例子能够看出,如果传入base字符串,会以传入base优先级最高去拼接,而后才是location.pathname + location.search
同时会检测传入base是否含有"#",如果没有,则在前面增加"#"

当然,也会存在传入baselocation.pathname不一样的状况,如下面例子/other-folder/所示,会间接更改网址去除location.pathname,改为base

createWebHistory创立history

整体概述

routerHistory对标的是原生的history对象,routerHistory在原生的history对象的API根底上,削减了一些逻辑解决,实现Vue的路由切换、路由映射组件、组件切换的性能

function createWebHistory(base) {
    // 步骤1: normalizeBase整顿url
    base = normalizeBase(base);
    // 步骤2: useHistoryStateNavigation
    const historyNavigation = useHistoryStateNavigation(base);
    // 步骤3: useHistoryListeners
    const historyListeners = useHistoryListeners(base, 
      historyNavigation.state, historyNavigation.location, historyNavigation.replace);
    // 步骤4: 合并historyNavigation和historyListeners,整合为routerHistory
    const routerHistory = assign({
        // it's overridden right after
        location: '',
        base,
        go,
        createHref: createHref.bind(null, base),
    }, historyNavigation, historyListeners);
    Object.defineProperty(routerHistory, 'location', {
        enumerable: true,
        get: () => historyNavigation.location.value,
    });
    Object.defineProperty(routerHistory, 'state', {
        enumerable: true,
        get: () => historyNavigation.state.value,
    });
    return routerHistory;
}
function go(delta, triggerListeners = true) {
    if (!triggerListeners)
        historyListeners.pauseListeners();
    history.go(delta);
}

步骤1: normalizeBase

从下面剖析能够晓得,如果有传入base 或者 自身存在location.pathname,那么达到normalizeBase(base)base就不可能为空

然而也有可能存在

  • 没有传入base+自身不存在location.pathname
  • 没有传入base+自身不存在location.host(file://xxx)

当没有base时,咱们就会检测是否具备<base>标签,<base>标签的含意能够参考https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base,如果没有<base>标签,则应用location.href作为根底的链接,如果有<base>标签,则应用该标签作为根底链接

For example, given <base href=”https://example.com/“> and this link: To anchor. The link points to https://example.com/#anchor

而后替换域名局部的字符串,最终返回base="/xxxx"或者"#xxxxx"

function normalizeBase(base) {
    if (!base) {
        if (isBrowser) {
            // respect <base> tag
            const baseEl = document.querySelector('base');
            base = (baseEl && baseEl.getAttribute('href')) || '/';
            // 剔除https://xx.xxxx.com这部分的字母
            base = base.replace(/^\w+:\/\/[^\/]+/, '');
        } else {
            base = '/';
        }
    }
    if (base[0] !== '/' && base[0] !== '#')
        base = '/' + base;
    // 删除尾部的斜杠
    return removeTrailingSlash(base);
}
const TRAILING_SLASH_RE = /\/$/;
const removeTrailingSlash = (path) => path.replace(TRAILING_SLASH_RE, '');

个别状况下,通过normalizeBase()失去的base的构造为location.pathname+location.search+"#",具体构造如上面所示:

上面链接为测试链接,仅仅示意不思考非凡状况下的base构造,没有其它含意

base = "/Frontend-Articles/vue3-debugger/router/vue-router.html?_ijt=q70k46ba94eddqp8fu39ta28fh&_ij_reload#"

步骤2: useHistoryStateNavigation

整体概述

该办法初始化的historyNavigation蕴含了以后的路由、以后的堆栈信息以及对应的push加操作和replace笼罩操作,同时检测history.state是否为空,如果为空,须要压入一个初始化的currentLocation

除了pushreplace等惯例操作,咱们晓得一个路由还须要具备路由后退的监听,Vue Router将后退操作的监听放在下一节要剖析的useHistoryListeners

// const historyNavigation = useHistoryStateNavigation(base);
function useHistoryStateNavigation(base) {
    const { history, location } = window;
    // currentLocation.value=除去"#"后的字符串
    const currentLocation = {
        value: createCurrentLocation(base, location),
    };
    const historyState = { value: history.state };
    // build current history entry as this is a fresh navigation
    if (!historyState.value) {
        changeLocation(currentLocation.value, {
            back: null,
            current: currentLocation.value,
            forward: null,
            // the length is off by one, we need to decrease it
            position: history.length - 1,
            replaced: true,
            // don't add a scroll as the user may have an anchor, and we want
            // scrollBehavior to be triggered without a saved position
            scroll: null,
        }, true);
    }

    function changeLocation(to, state, replace) {...}
    function replace(to, data) {...}
    function push(to, data) {...}

    return {
        location: currentLocation,
        state: historyState,
        push,
        replace,
    };
}

createCurrentLocation()

window.location对象中创立一个规范化的数据作为以后的门路,能够简略认为最初失去的门路就是下面注册的routes中的其中一个路由门路

具体逻辑剖析请看上面的例子剖析

function createCurrentLocation(base, location) {
  const { pathname, search, hash } = location;
  // allows hash bases like #, /#, #/, #!, #!/, /#!/, or even /folder#end
  const hashPos = base.indexOf('#');
  if (hashPos > -1) {
    let slicePos = hash.includes(base.slice(hashPos))
      ? base.slice(hashPos).length
      : 1;
    let pathFromHash = hash.slice(slicePos);
    // prepend the starting slash to hash so the url starts with /#
    if (pathFromHash[0] !== '/')
      pathFromHash = '/' + pathFromHash;
    return stripBase(pathFromHash, '');
  }
  const path = stripBase(pathname, base);
  return path + search + hash;
}
function stripBase(pathname, base) {
    // no base or base is not found at the beginning
    if (!base || !pathname.toLowerCase().startsWith(base.toLowerCase()))
        return pathname;
    return pathname.slice(base.length) || '/';
}

连续下面例子所用的文件门路,咱们能够晓得:

location.href = "http://localhost:63342/Frontend-Articles/vue3-debugger/router/vue-router.html?_ijt=q70k46ba94eddqp8fu39ta28fh&_ij_reload#/about";
base = "/Frontend-Articles/vue3-debugger/router/vue-router.html?_ijt=q70k46ba94eddqp8fu39ta28fh&_ij_reload#";
location.hash = "#/about";

location.pathname = "/Frontend-Articles/vue3-debugger/router/vue-router.html";

通过hash.slice(slicePos)失去的pathFromHash

pathFromHash = "/about"

最终通过stripBase(pathFromHash, ''),间接返回pathFromHash = "/about"

!!!!下面的举例是比拟失常一点的门路,然而事实中必定会存在各种各样的门路=_=前面遇到奇怪的门路再回来补充例子,临时跳过这部分规范化门路逻辑的思考

最终失去目前的路由门路为

const currentLocation = {
    value: "/about"
};

changeLocation()

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;
    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) {
        // Force the navigation, this also resets the call count
        location[replace ? 'replace' : 'assign'](url);
    }
}

传入要跳转的门路to,而后拼接要跳转的门路url
在下面的例子中,咱们晓得

base = "/Frontend-Articles/vue3-debugger/router/vue-router.html?_ijt=q70k46ba94eddqp8fu39ta28fh&_ij_reload#";
url = base.slice(hashIndex)) + to = "#/Frontend-Articles/vue3-debugger/router/vue-router.html?_ijt=q70k46ba94eddqp8fu39ta28fh&_ij_reload#/about"

changeLocation()实质就是拼接了跳转门路,而后

  • 调用浏览器原生API提供的window.history.pushState/window.history.replaceState办法
  • 更新historyState.value

一开始调用changeLocation()传入的state数据如下所示

{
    back: null,
    current: currentLocation.value,
    forward: null,
    // the length is off by one, we need to decrease it
    position: history.length - 1,
    replaced: true,
    // don't add a scroll as the user may have an anchor, and we want
    // scrollBehavior to be triggered without a saved position
    scroll: null,
}

changeLocation()的过程中如果产生谬误,则强制跳转,调用原生的window.location.assign(url):使窗口加载并显示指定url处的document

导航产生后,用户能够通过按“后退”按钮导航回到location.assign替换的页面,即这个办法会产生浏览器历史记录,不是间接笼罩以后页面

window.location.assign能够参考文档assign,跟window.location.replace不同点在于,replace不会保留目前要替换的页面记录

push()

代码逻辑也比拟清晰和简略,次要分为5个步骤

  • 应用forward: to记录要跳转的路由,并且放入currentState
  • 应用changeLocation()进行history.replaceState()替换以后的浏览记录
  • 应用buildState()构建出新的路由对象,造成新的路由state
  • 应用changeLocation()进行history.pushState()新增一条新的浏览记录
  • 更新多个办法都应用的变量currentLocation为以后的新的路由地址currentLocation.value=to
function push(to, data) {
    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) {
        // 如果以后没有history.state,报错提醒:
        // 如果您手动调用 history.replaceState,请确保保留现有的历史状态
    }
    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;
}
function buildState(back, current, forward, replaced = false, computeScroll = false) {
    return {
        back,
        current,
        forward,
        replaced,
        position: window.history.length,
        scroll: computeScroll ? computeScrollPosition() : null,
    };
}

replace()

构建出新的路由对象,造成新的路由state,而后应用changeLocation()进行history.replaceState()替换以后的浏览记录

changeLocation()中也进行historyState.value=state

更新多个办法都应用的变量currentLocation为以后的新的路由地址currentLocation.value=to

function replace(to, data) {
    const state = assign({}, history.state, buildState(historyState.value.back,
        // keep back and forward entries but override current position
        to, historyState.value.forward, true), data, { position: historyState.value.position });
    changeLocation(to, state, true);
    currentLocation.value = to;
}

小结

从下面的代码能够晓得,historyNavigation最终是一个对象,具备locationstatepushreplace属性,其中

  • location:代表除去"#"之后的门路内容,治理以后页面的门路数据,比方location="/about"
  • state:保留原生的window.history.state数据,比方上面的histroyState就是代表window.history.state数据

    // history.state格局如下
    const histroyState = {
      back: null,
      current: "/Frontend-Articles/vue3-debugger/router/vue-router.html?_ijt=q70k46ba94eddqp8fu39ta28fh&_ij_reload#/about",
      forward: null,
      position: 14,
      replaced: true,
      scroll: null
    }
    const state = { value: histroyState };
  • push():新增路由时调用的办法
  • replace():替换路由时调用的办法

步骤3: useHistoryListeners

整体概述

传入初始化门路以及historyNavigation保护的状态和办法,进行historyListeners的初始化

const historyListeners = useHistoryListeners(base, 
      historyNavigation.state, historyNavigation.location, historyNavigation.replace);
function useHistoryListeners(base, historyState, currentLocation, replace) {
    let listeners = [];
    let teardowns = [];
    let pauseState = null;
    const popStateHandler = ({ state, }) => {...};
    function pauseListeners() {...}
    function listen(callback) {...}
    function beforeUnloadListener() {...}
    function destroy() {...}
    
    // set up the listeners and prepare teardown callbacks
    window.addEventListener('popstate', popStateHandler);
    window.addEventListener('beforeunload', beforeUnloadListener);
    return {
        pauseListeners,
        listen,
        destroy,
    };
}

popStateHandler()

监听后退事件,这个监听办法比较复杂并且十分重要!

简略示例
window.addEventListener("popstate", (event) => {
  console.log(
    `location: ${document.location}, state: ${JSON.stringify(event.state)}`
  );
});
history.pushState({ page: 1 }, "title 1", "?page=1");
history.pushState({ page: 2 }, "title 2", "?page=2");
history.replaceState({ page: 3 }, "title 3", "?page=3");
history.back(); // Logs "location: http://example.com/example.html?page=1, state: {"page":1}"
history.back(); // Logs "location: http://example.com/example.html, state: null"
history.go(2); // Logs "location: http://example.com/example.html?page=3, state: {"page":3}"
触发机会
  • history.pushState() 或者history.replaceState()不会触发popStateHandler()
  • 用户手动点击浏览器的后退按钮、history.back()history.forward()都会触发popStateHandler()
  • history.go(数字) 实质就是history.back() / history.forward()调用1次或者屡次,也会触发popStateHandler()
触发时携带的参数state

摘录于https://developer.mozilla.org/en-US/docs/Web/API/Window/popstate_event#the_history_stack

如果被激活的历史条目是通过调用 history.pushState() 创立的,或者受到调用 history.replaceState() 的影响,则 popstate 事件的状态属性蕴含历史条目状态对象的正本

换句话说,popStateHandler({state})state不为空的前提是咱们始终都有应用pushState/replaceState

源码剖析

从下面的简略示例能够晓得,携带的参数state是后退后以后的路由参数,如果存在,间接把它当作目前最新的historyState.value=state,而后依据后退前缓存的数据fromState计算出对应的delta,最终调用listeners进行监听办法的触发

const popStateHandler = ({ state, }) => {
    const to = createCurrentLocation(base, location);
    const from = currentLocation.value;
    const fromState = historyState.value;
    let delta = 0;
    if (state) {
        currentLocation.value = to;
        historyState.value = 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 => {
        listener(currentLocation.value, from, {
            delta,
            type: NavigationType.pop,
            direction: delta
                ? delta > 0
                    ? NavigationDirection.forward
                    : NavigationDirection.back
                : NavigationDirection.unknown,
        });
    });
};
window.addEventListener('popstate', popStateHandler);

如果没有携带参数state,咱们就无奈晓得目前的状态,因而会间接应用const to = createCurrentLocation(base, location)构建出新的路由对象,造成新的路由state,而后应用changeLocation()进行history.replaceState()替换以后的浏览记录

没有携带参数state时,listeners进行监听办法触发的状态为NavigationDirection.unknown

function replace(to, data) {
    const state = assign({}, history.state, buildState(historyState.value.back,
        // keep back and forward entries but override current position
        to, historyState.value.forward, true), data, { position: historyState.value.position });
    changeLocation(to, state, true);
    currentLocation.value = to;
}
listen() and pauseListeners()

提供给内部注册监听的办法,并且返回进行该监听的办法teardown

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. 什么时候触发listen()注册监听办法?
  2. listen()注册的监听办法有什么用途?
  3. 为什么要应用pauseListeners()暂停监听?
  4. pauseListeners()是如何暂停办法执行的?

这些问题将在第二篇文章Vue3相干源码-Vue Router源码解析(二)的总结Vue Router的hash模式什么中央最容易导致路由切换失败进行解答


beforeUnloadListener()注册监听

beforeunload 事件在行将来到以后页面(刷新或敞开)时触发,触发时会重置history.replaceState保留以后的scroll(来自Vue Router的代码git提交记录形容)

该事件可用于弹出对话框,提醒用户是持续浏览页面还是来到以后页面,比方“确定要来到此页吗?”

function beforeUnloadListener() {
    const { history } = window;
    if (!history.state)
        return;
    history.replaceState(assign({}, history.state, { scroll: computeScrollPosition() }), '');
}
window.addEventListener('beforeunload', beforeUnloadListener);
destroy()

革除所有listeners监听办法,移除全局注册的事件

function destroy() {
    for (const teardown of teardowns)
        teardown();
    teardowns = [];
    window.removeEventListener('popstate', popStateHandler);
    window.removeEventListener('beforeunload', beforeUnloadListener);
}

步骤4: go()和routerHistory初始化

除了上述的3个步骤,最初一个步骤就是将下面步骤失去的historyNavigationhistoryListeners以及对应的其它根底数据进行合并成为routerHistory

其中go()事件除了第一个参数回退的层级,还有第二个参数triggerListeners,阻止监听器触发执行,该办法的剖析将放在第二篇文章Vue3相干源码-Vue Router源码解析(二)的总结中进行解答

function go(delta, triggerListeners = true) {
    if (!triggerListeners)
        historyListeners.pauseListeners();
    history.go(delta);
}
const routerHistory = assign({
    // it's overridden right after
    location: '',
    base,
    go,
    createHref: createHref.bind(null, base),
}, historyNavigation, historyListeners);
Object.defineProperty(routerHistory, 'location', {
    enumerable: true,
    get: () => historyNavigation.location.value,
});
Object.defineProperty(routerHistory, 'state', {
    enumerable: true,
    get: () => historyNavigation.state.value,
});
return routerHistory;

createMemoryHistory创立history

createMemoryHistory会创立一个基于内存的history,次要目标是为了解决SSR
与后面两个办法不同的是,createMemoryHistory保护一个队列queue和一个position,来保障历史记录存储的正确性

这里不开展详细分析,请读者自行钻研

createRouter创立VueRouter对象

整体概述

createRouter()VueRouter.createWebHashHistory()routes的数据进行合并和拼接,组合成router对象

// Vue初始化Vue Router时的示例代码
const router = VueRouter.createRouter({
    // 4. 外部提供了 history 模式的实现。为了简略起见,咱们在这里应用 hash 模式。
    history: VueRouter.createWebHashHistory(),
    routes, // `routes: routes` 的缩写
})
function createRouter(options) {
    const matcher = createRouterMatcher(options.routes, options);
    //...多个办法,根本都是为上面初始化router的属性
    //比方addRoute()、removeRoute()、hasRoute()
    const router = {
        currentRoute,
        listening: true,
        addRoute,
        removeRoute,
        hasRoute,
        getRoutes,
        resolve,
        options,
        push,
        replace,
        go,
        back: () => go(-1),
        forward: () => go(1),
        beforeEach: beforeGuards.add,
        beforeResolve: beforeResolveGuards.add,
        afterEach: afterGuards.add,
        onError: errorHandlers.add,
        isReady,
        install(app) {
            //...省略,内部app.use()时调用
        },
    };
    return router;
}

这个办法的逻辑十分繁冗,本文不会具体开展讲,前面会讲一些重点的办法以及依据一些业务去剖析createRouter外面的办法,咱们只须要明确,这个办法创立的数据就是Vue Router提供给内部应用的对象即可

createRouterMatcher: 依据内部传入的options.routes初始化路由配置

整体概述

依据内部传入的options.routes初始化路由配置,建设matchers数组

留神:createRouterMatcher()外部有addRoute()办法,下面createRouter()外部也有addRoute()办法,不要搞混

// globalOptions={history, routes}
function createRouterMatcher(routes, globalOptions) {
    // normalized ordered array of matchers
    const matchers = [];
    const matcherMap = new Map();
    globalOptions = mergeOptions({ strict: false, end: true, sensitive: false }, globalOptions);
    function getRecordMatcher(name)
    function addRoute(record, parent, originalRecord) {...}
    function removeRoute(matcherRef) {...}
    function getRoutes() {...}
    function insertMatcher(matcher) {...}
    function resolve(location, currentLocation) {...}
    // add initial routes
    routes.forEach(route => addRoute(route));
    return { addRoute, resolve, removeRoute, getRoutes, getRecordMatcher };
}

从下面的代码,咱们能够晓得,除了一些办法的初始化之外,次要就执行了两个步骤:mergeOptions()routers.forEach(route=>addRoute(route))

mergeOptions

合并传递过去的参数

// partialOptions={history, routes}
// defaults={ strict: false, end: true, sensitive: false }
function mergeOptions(defaults, partialOptions) {
    const options = {};
    for (const key in defaults) {
        options[key] = key in partialOptions ? partialOptions[key] : defaults[key];
    }
    return options;
}

routers.forEach(route=>addRoute(route))

function addRoute(record, parent, originalRecord) {
    //...
    const mainNormalizedRecord = normalizeRouteRecord(record);
    const normalizedRecords = [
        mainNormalizedRecord,
    ];
    if ('alias' in record) {
        const aliases = typeof record.alias === 'string' ? [record.alias] : record.alias;
        for (const alias of aliases) {
            // ...省略解决别名的逻辑
            normalizedRecords.push(assign({}, mainNormalizedRecord, {...}));
        }
    }
    let matcher;
    let originalMatcher;
    for (const normalizedRecord of normalizedRecords) {
        //...
        // 步骤1: 创立matcher对象
        matcher = createRouteRecordMatcher(normalizedRecord, parent, options);
        //...
        // 步骤2: 创立matcher对象和它的children-matcher之间的关系
        if (mainNormalizedRecord.children) {
            const children = mainNormalizedRecord.children;
            for (let i = 0; i < children.length; i++) {
                addRoute(children[i], matcher, originalRecord && originalRecord.children[i]);
            }
        }
        //...
        // 步骤3: 将创立好的matcher插入到matchers数组中
        insertMatcher(matcher);
    }
    return originalMatcher
        ? () => fcd
            // since other matchers are aliases, they should be removed by the original matcher
            removeRoute(originalMatcher);
        }
        : noop;
}
步骤1: createRouteRecordMatcher创立matcher对象

在路由对象record的根底上进行路由权重的计算以及正则表达式的构建,为前面门路的映射提供对应的matcher对象

function createRouteRecordMatcher(record, parent, options) {
    const parser = tokensToParser(tokenizePath(record.path), options);
    // warn against params with the same name

    const existingKeys = new Set();
    for (const key of parser.keys) {
        existingKeys.add(key.name);
    }

    const matcher = assign(parser, {
        record,
        parent,
        // these needs to be populated by the parent
        children: [],
        alias: [],
    });
    if (parent) {
        // both are aliases or both are not aliases
        // we don't want to mix them because the order is used when
        // passing originalRecord in Matcher.addRoute
        if (!matcher.record.aliasOf === !parent.record.aliasOf)
            parent.children.push(matcher);
    }
    return matcher;
}

tokenizePath()tokensToParser()解析多种模式下的路由门路,上面将着重剖析Vue Router4中的路由权重计算逻辑

多种类型的路由介绍

上面内容参考自Vue Router官网文档

动态路由

const routes = [
  { path: '/', component: Home },
  { path: '/about', component: About },
]

带参数的动静路由匹配
/users/johnny/users/jolyn 这样的 URL 都会映射到同一个路由

const routes = [
    // 动静字段以冒号开始
    { path: '/users/:id', component: User },
]

正则表达式路由
惯例参数只匹配url 片段之间的字符,用/分隔。如果咱们想匹配任意门路,咱们能够应用自定义的门路参数正则表达式,在门路参数前面的括号中退出 正则表达式

const routes = [
    // 将匹配所有内容并将其放在 `$route.params.pathMatch` 下
    { path: '/:pathMatch(.*)*', name: 'NotFound', component: NotFound }
]
const routes = [
    // 将匹配以 `/user-` 结尾的所有内容,并将其放在 `$route.params.afterUser` 下
    { path: '/user-:afterUser(.*)', component: UserGeneric }
]
const routes = [
  // /:orderId -> 仅匹配数字
  { path: '/:orderId(\\d+)' },
  // /:productName -> 匹配其余任何内容
  { path: '/:productName' },
]
const routes = [
  // /:chapters ->  匹配 /one, /one/two, /one/two/three, 等
  { path: '/:chapters+' },
  // /:chapters -> 匹配 /, /one, /one/two, /one/two/three, 等
  { path: '/:chapters*' },
]
具体例子剖析
const routes = [
    {path: '/', component: Home},
    {path: '/child', component: Child1}, // 动态路由
    {path: '/child/:id', component: Child2}, // 动静路由
    {path: '/child/:id?', component: Child3}, // 动静路由(可选)
    {path: '/:child1(\\d+)', component: Child4}, // 动静路由(限度数字)TokenizerState.ParamRegExp
    {path: '/:child2+', component: Child5}, // 动静路由(可反复)
]

具体例子应用Vue Router官网文档提供的在线测试链接Path Ranker进行测试,后果为下图所示

上面将剖析如何失去下面图中的分数以及对应的路由匹配逻辑

tokenizePath()

该办法就是间接依据不同的const char=path[i++]进行不同状态的赋值,而后造成对应的数据
上面就是Vue Router源码中标记的几种状态

const enum TokenizerState {
  Static, // 静态数据,比方/child这种类型中的"child"
  Param, // 动静路由以及常见的正则表达式,比方"/:child1(\\d+)"
  ParamRegExp, // custom re for a param
  ParamRegExpEnd, // check if there is any ? + *
  EscapeNext,
}

间接应用debugger断点调试该办法即可清晰明确整个流程

取一个比较复杂的正则表达式例子为:path: "/c\\hil\\d3/new:c\\hild1(\\d+)?",这个例子中有静态数据/child3,有动态数据:child1,也有一些正则表达式\d+以及?,还有一些两头乱入的\\
tokenizePath()外围办法为上面代码块,依据判断目前item"/",还是":",还是"(",而后进行不同的状态的赋值

function tokenizePath(path) {
    while (i < path.length) {
        char = path[i++];
        if (char === '\\' && state !== 2 /* TokenizerState.ParamRegExp */) {
            previousState = state;
            state = 4 /* TokenizerState.EscapeNext */;
            continue;
        }
        switch (state) {
            case 0 /* TokenizerState.Static */:
                break;
            case 4 /* TokenizerState.EscapeNext */:
                break;
            case 1 /* TokenizerState.Param */:
                break;
            case 2 /* TokenizerState.ParamRegExp */:
                break;
            case 3 /* TokenizerState.ParamRegExpEnd */:
                break;
            default:
                crash('Unknown state');
                break;
        }
    }
}
  • TokenizerState.Static: 遇到"/",进入该状态,解决动态门路,比方"/child3"
  • TokenizerState.Param: 遇到":",进入该状态,解决动静门路,比方":child1"
  • TokenizerState.ParamRegExp: 遇到"(",进入该状态,开始解决正则表达式
  • TokenizerState.ParamRegExpEnd: 遇到")",进入该状态,完结解决正则表达式,而后从新回到TokenizerState.Static状态
  • TokenizerState.EscapeNext: 在不是TokenizerState.ParamRegExp(解决正则表达式)的状态下遇到了"\\",间接跳过"\\",比方/c\\hil\\d3->/child3

最终造成segment数组为:

segment.push({
    type: 1 /* TokenType.Param */,
    value: buffer, // path的局部内容
    regexp: customRe, // 正则表达式的内容
    repeatable: char === '*' || char === '+', // 是否容许反复,代表"+"或者"*"这些示意反复的正则表达式
    optional: char === '*' || char === '?', // 是否可选,代表"?"或者"*"这些示意可选的正则表达式
});

segment代表每一个片段的值,在path: "/c\\hil\\d3/new:c\\hild1(\\d+)?"中,能够分为2个片段

  • c\\hil\\d3
  • new:c\\hild1(\\d+)?

最终要依据segment再造成一个总体的数组tokens

function finalizeSegment() {
    if (segment)
        tokens.push(segment);
    segment = [];
}

path: "/c\\hil\\d3/new:c\\hild1(\\d+)?"造成tokens如下所示

  • c\\hil\\d3: 动态门路
  • new:c\\hild1(\\d+)?: 动态门路+动静门路(蕴含正则表达式+可选)
[
  [
    {
      "type": 0,
      "value": "child3"
    }
  ],
  [
    {
      "type": 0,
      "value": "new"
    },
    {
      "type": 1,
      "value": "child1",
      "regexp": "\\d+",
      "repeatable": false,
      "optional": true
    }
  ]
]
tokensToParser()

遍历tokenizePath()拿到的tokens进行权重得分、正则表达式的计算

function tokensToParser(segments, extraOptions) {
    //...
    for (const segment of segments) {
        //...
    }
    return {
        re,
        score,
        keys,
        parse,
        stringify,
    };
}

上面剖析正则表达式re和权重分数score的构建流程

正则表达式re的构建

TokenType.Static: 动态门路,比方path="/child",应用原来的门路值,会进行一些特殊字符的转译,最终造成re=/^\/child$/i

const REGEX_CHARS_RE = /[.+*?^${}()[\]/\\]/g;
if (!tokenIndex)
    pattern += '/';
pattern += token.value.replace(REGEX_CHARS_RE, '\\$&');

TokenType.Param: 动静匹配门路,蕴含各种正则表达式
当动静匹配门路不蕴含正则表达式时,间接应用BASE_PARAM_PATTERN = '[^/]+?'作为最终re的值
比方path="/child/:id",最终造成re=/^\/child\/([^/]+?)$/i

因为path蕴含动态门路+动静门路,因而re= 动态门路+动静门路 = ^\/child\/ + ([^/]+?)+i

当动静匹配门路蕴含正则表达式时,间接门路中的正则表达式,即segment.regexp去构建最终的re的值
比方path="/:child1(\\d+)?",失去的segment.regexp="\\d+",最终造成re=/^(?:\/(\d+))?$/i

const BASE_PARAM_PATTERN = '[^/]+?';
const re = regexp ? regexp : BASE_PARAM_PATTERN;
let subPattern = repeatable ? `((?:${re})(?:/(?:${re}))*)` : `(${re})`;
// prepend the slash if we are starting a new segment
if (!tokenIndex)
    subPattern =
        // avoid an optional / if there are more segments e.g. /:p?-static
        // or /:p?-:p2
        optional && segment.length < 2
            ? `(?:/${subPattern})`
            : '/' + subPattern;
if (optional)
    subPattern += '?';

如果path中蕴含可反复的正则表达式,比方path="/:child1(\\d+)+",失去的segment.regexp="\\d+",因为最初有个+,因而repeatable=true,最终造成re=/^\/((?:\d+)(?:\/(?:\d+))*)$/i


权重分数score的构建

const enum PathScore {
  _multiplier = 10,
  Root = 9 * _multiplier, // just /
  Segment = 4 * _multiplier, // /a-segment
  SubSegment = 3 * _multiplier, // /multiple-:things-in-one-:segment
  Static = 4 * _multiplier, // /static
  Dynamic = 2 * _multiplier, // /:someId
  BonusCustomRegExp = 1 * _multiplier, // /:someId(\\d+)
  BonusWildcard = -4 * _multiplier - BonusCustomRegExp, // /:namedWildcard(.*) we remove the bonus added by the custom regexp
  BonusRepeatable = -2 * _multiplier, // /:w+ or /:w*
  BonusOptional = -0.8 * _multiplier, // /:w? or /:w*
  // these two have to be under 0.1 so a strict /:page is still lower than /:a-:b
  BonusStrict = 0.07 * _multiplier, // when options strict: true is passed, as the regex omits \/?
  BonusCaseSensitive = 0.025 * _multiplier, // when options strict: true is passed, as the regex omits \/?
}

原始分数为PathScore.Segment=40

  • 如果对大小写敏感(options.sensitive=true),则减少分数PathScore.BonusCaseSensitive=0.25,即便40+0.25=40.25
  • 如果最初一个segment设置了options.strict=true,额定失去PathScore.BonusStrict0.7

下面是初始化的分数,上面将针对各种状态进行剖析

TokenType.Static

动态门路会额定失去PathScore.Static+40分,如/child的分数是40+40+0.7=80.7

TokenType.Param

动静匹配门路会额定失去PathScore.Dynamic+20分,如/:child的分数是40+20+0.7=60.7

  • 如果蕴含正则表达式,正则表达式会额定失去PathScore.BonusCustomRegExp+10分,如/:child1(\\d+)的分数是60+10+0.7=70.7
  • 如果蕴含可选optional符号,额定失去PathScore.BonusOptional-10分,如/:child1?的分数是60-10+0.7=60.7
  • 如果蕴含匹配所有字符.*符号,额定失去PathScore.BonusWildcard-50分,如/:child1(.*)的分数是60+10-50+0.7=20.7
  • 如果蕴含反复repeatable符号,额定失去PathScore.BonusRepeatable-20分,如/:child1+的分数是60-20+0.7=40.7

/:child1+的正则表达式为空,即TokenizePath [[{"type":1,"value":"child1","regexp":"","repeatable":true,"optional":false}]]

/:child1?同理,正则表达式也为空,因而没有PathScore.BonusCustomRegExp+10分


如果存在多段segment

  • /child/:child1,分数为child+:child1=[[80], [60.7]]
  • /child/pre-:child1,分数为child+pre-+:child1=[[80],[80,60.7]]

小结

通过tokenizePath()拿到routes解析后的路由数据

{
  "type": 1,
  "value": "child1",
  "regexp": "\\d+",
  "repeatable": false,
  "optional": true
}

依据tokenizePath()解析后的数据,进行路由权重的计算,通过tokensToParser()拿到权重以及拼接对应的正则表达式,造成一个更加欠缺的路由数据

parse()stringify()会在应用RouterMatcher匹配门路时用到

{
  re, //正则表达式
  score, //权重分数
  keys, //{name,repeatable,optional}
  parse, //动静路由匹配,匹配出动静路由对应那个动态门路的办法
  stringify //格式化params失去path的办法
}

最终拼接所有数据造成matchercreateRouteRecordMatcher()返回数据如下所示

function createRouteRecordMatcher(record, parent, options) {
    const parser = tokensToParser(tokenizePath(record.path), options);
    const matcher = assign(parser, {
        record,
        parent,
        // these needs to be populated by the parent
        children: [],
        alias: [],
    });
    return matcher;
}
步骤2: 创立matcher对象和它的children-matcher之间的关系

遍历目前routechildren,将以后route创立的matcher作为parent传入addRoute(),递归调用addRoute()创立新的matcher

if (mainNormalizedRecord.children) {
    const children = mainNormalizedRecord.children;
    for (let i = 0; i < children.length; i++) {
        addRoute(children[i], matcher, originalRecord && originalRecord.children[i]);
    }
}
步骤3: insertMatcher()将创立好的matcher插入到matchers数组和matcherMap对象中
  • 应用comparePathParserScore()matchers进行排序,每次从头开始遍历let i=0,如果目前matchers[i]权重较大,则i++,否则间接调用matchers.splice(i, 0, matcher)插入matcher进去
  • matcherMap以路由对象record的名称作为keymatcher作为value

在应用matchers查找门路时,会应用matcher=matchers.find(m => m.re.test(path)),权重越大的元素放在越后面,会最先被找到,因而查找门路时会先找到权重最大的那个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 ||
            !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);
}

而权重的比拟也比较简单,不是依照总分计算权重,而是依据数组中的每一项从头到尾进行比拟,如果a<b,则返回大于0的数字

  • 如果雷同index比拟能出后果,间接返回后果

    • 长度雷同时,间接比拟雷同index,间接返回差值
    • 如果a的长度只有1,并且a[0]=PathScore.Static+PathScore.Segment,示意a是一个动态门路,并且只有一个元素,那么a的权重较大,返回-1,反之返回1
    • 如果b的长度只有1,并且b[0]=PathScore.Static+PathScore.Segment,示意b是一个动态门路,并且只有一个元素,那么b的权重较大,返回-1,反之返回1
    • 如果ab的长度雷同,每一个值比拟都等于0,证实a===b,返回0,持续上面流程
  • 如果雷同index的后果都一样,并且长度相差1,则比拟最初一位是否为正数,哪个为正数,就比拟小
  • 如果雷同index的后果都一样,并且长度不止1,则比拟谁的长度大,谁大权重就大
function comparePathParserScore(a, b) {
    let i = 0;
    const aScore = a.score;
    const bScore = b.score;
    while (i < aScore.length && i < bScore.length) {
        const comp = compareScoreArray(aScore[i], bScore[i]);
        // do not return if both are equal
        if (comp)
            return comp;
        i++;
    }
    if (Math.abs(bScore.length - aScore.length) === 1) {
        if (isLastScoreNegative(aScore))
            return 1;
        if (isLastScoreNegative(bScore))
            return -1;
    }
    return bScore.length - aScore.length;
}
function compareScoreArray(a, b) {
    let i = 0;
    while (i < a.length && i < b.length) {
        const diff = b[i] - a[i];
        // only keep going if diff === 0
        if (diff)
            return diff;
        i++;
    }
    if (a.length < b.length) {
        return a.length === 1 && a[0] === 40 /* PathScore.Static */ + 40 /* PathScore.Segment */
            ? -1
            : 1;
    }else if (a.length > b.length) {
        return b.length === 1 && b[0] === 40 /* PathScore.Static */ + 40 /* PathScore.Segment */
            ? 1
            : -1;
    }
    return 0;
}

应用RouterMatcher匹配路由

在下面createRouterMatcher()中,咱们晓得了怎么初始化匹配路由,在这个大节中,咱们将剖析如何利用matcher进行路由的匹配

// 只保留matcher匹配的代码逻辑
function createRouter(options) {
    const matcher = createRouterMatcher(options.routes, options);
    //...多个办法,根本都是为上面初始化router的属性
    //比方addRoute()、removeRoute()、hasRoute()
    const router = {
        currentRoute,
        listening: true,
        addRoute,
        removeRoute,
        hasRoute,
        getRoutes,
        resolve,
        options,
        push,
        replace,
        go,
        back: () => go(-1),
        forward: () => go(1),
        beforeEach: beforeGuards.add,
        beforeResolve: beforeResolveGuards.add,
        afterEach: afterGuards.add,
        onError: errorHandlers.add,
        isReady,
        install(app) {
            //...省略,内部app.use()时调用
        },
    };
    return router;
}

push(routerHistory.location)整体概述

这个push()办法是在createRouter()创立的部分办法,不是下面useHistoryStateNavigation()创立的router.push()办法

当内部调用router.push()跳转到新的路由时,理论调用的是pushWithRedirect(),而在pushWithRedirect()中第一行代码,应用resolve(to)进行以后要跳转的路由的计算

function push(to) {
    return pushWithRedirect(to);
}
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;
        });
}
resolve(rawLocation, currentLocation)解析跳转门路

传递的rawLocation次要分为2种状况进行剖析

  • rawLocation为字符串,比方rawLocation="./child1",而后调用
    parseURL()->matcher.resolve()->routerHistory.createHref()
  • rawLocationObject数据,解决它携带的pathparams,而后调用matcher.resolve()
    ->routerHistory.createHref()

上面将先对parseURL()matcher.resolve()routerHistory.createHref()开展剖析,而后再剖析resolve()的整体流程

parseURL()解析门路拿到fullPathpathqueryhash

传入参数

  • parseQuery()是一个办法,能够解析链接中?A=xx&B=xx的局部,返回一个key-value数据,开发者可在初始化传入自定义的解析办法
  • location代表行将要跳转的路由门路
  • currentLocation代表目前的路由门路

在这个办法中,咱们通过"#"以及"?"拿到对应的字符串片段,塞入到hashquery字段中,将删除掉"#""?"的局部塞入到path字段中

function parseURL(parseQuery, location, currentLocation = '/') {
    let path, query = {}, searchString = '', hash = '';
    // Could use URL and URLSearchParams but IE 11 doesn't support it
    // TODO: move to new URL()
    const hashPos = location.indexOf('#');
    let searchPos = location.indexOf('?');
    // the hash appears before the search, so it's not part of the search string
    if (hashPos < searchPos && hashPos >= 0) {
        searchPos = -1;
    }
    if (searchPos > -1) {
        path = location.slice(0, searchPos);
        searchString = location.slice(searchPos + 1, hashPos > -1 ? hashPos : location.length);
        query = parseQuery(searchString);
    }
    if (hashPos > -1) {
        path = path || location.slice(0, hashPos);
        // keep the # character
        hash = location.slice(hashPos, location.length);
    }
    // no search and no query
    path = resolveRelativePath(path != null ? path : location, currentLocation);
    // empty path means a relative query or hash `?foo=f`, `#thing`
    return {
        fullPath: path + (searchString && '?') + searchString + hash,
        path,
        query,
        hash,
    };
}

而后触发resolveRelativePath()办法,代码逻辑也非常简单,就是判断toPosition是否是相对路径

  • 如果是"/"结尾,不解决间接返回
  • 如果是"."结尾,则代表跟fromSegments同级目录,不进行position的解决
  • 如果是".."结尾,代表是fromSegments的上一级目录,进行position--

最终拼接绝对路径进行返回

function resolveRelativePath(to, from) {
    if (to.startsWith('/'))
        return to;
    if (!from.startsWith('/')) {
        warn(`Cannot resolve a relative location without an absolute path. Trying to resolve "${to}" from "${from}". It should look like "/${from}".`);
        return to;
    }
    if (!to)
        return from;
    const fromSegments = from.split('/');
    const toSegments = to.split('/');
    let position = fromSegments.length - 1;
    let toPosition;
    let segment;
    for (toPosition = 0; toPosition < toSegments.length; toPosition++) {
        segment = toSegments[toPosition];
        // we stay on the same position
        if (segment === '.')
            continue;
        // go up in the from array
        if (segment === '..') {
            // we can't go below zero, but we still need to increment toPosition
            if (position > 1)
                position--;
            // continue
        }
        // we reached a non-relative path, we stop here
        else
            break;
    }
    return (fromSegments.slice(0, position).join('/') +
        '/' +
        toSegments
            // ensure we use at least the last element in the toSegments
            .slice(toPosition - (toPosition === toSegments.length ? 1 : 0))
            .join('/'));
}

最终返回绝对路径下的全门路fullPath(包含queryhash)以及对应的pathqueryhash

function parseURL(parseQuery, location, currentLocation = '/') {
    //...
    return {
        fullPath: path + (searchString && '?') + searchString + hash,
        path,
        query,
        hash,
    };
}
matcher.resolve(location, currentLocation)拿到门路上所有对应的matcher数组

留神,matchercreateRouterMatcher()返回的对象,具备多个办法属性
createRouterMatcher()外部会进行addRoute()创立对应的matcherMap对象,提供给matcher.resolve()应用

// globalOptions={history, routes}
function createRouterMatcher(routes, globalOptions) {
    //...
    function resolve(location, currentLocation) {...}
    return { addRoute, resolve, removeRoute, getRoutes, getRecordMatcher };
}

matcher.resolve()办法,次要分为3个条件进行查找

function resolve(location, currentLocation) {
    let matcher;
    let params = {};
    let path;
    let name;
    if ('name' in location && location.name) {
        //...传递name查找路由
    } else if ('path' in location) {
       //...传递path查找路由
    } else {
       //...其它
    }
    const matched = [];
    let parentMatcher = matcher;
    while (parentMatcher) {
        matched.unshift(parentMatcher.record);
        parentMatcher = parentMatcher.parent;
    }
    return {
        name,
        path,
        params,
        matched,
        meta: mergeMetaFields(matched),
    };
}

function insertMatcher(matcher) {
    //...
    if (matcher.record.name && !isAliasRecord(matcher))
        matcherMap.set(matcher.record.name, matcher);
}

上面将针对下面这个代码进行合成

传递name查找对应的路由

router.push({ name: 'user', params: { username: 'eduardo' } })

间接依据name找到对应的matcher,而后进行params的合并,这里间接利用location.params笼罩currentLocation.params反复的key

paramsFromLocation(): 筛选第一个传入Object,筛选出key存在于第二个参数,即第二个参数存在的key,能力保留下来第一个参数对应的key-value

matcher = matcherMap.get(location.name);
name = matcher.record.name;
params = assign(
    paramsFromLocation(currentLocation.params,
        matcher.keys.filter(k => !k.optional).map(k => k.name)),
    location.params &&
    paramsFromLocation(location.params, matcher.keys.map(k => k.name)));
path = matcher.stringify(params);

而后依据params进行对应门路的拼接,如果是动态门路,间接拼接动态门路的值,如果是动静门路,则拼接传递的params

如果动静匹配路由没有传递对应的参数,并且是不可选optional=false,则会报错

function stringify(params) {
    let path = '';
    let avoidDuplicatedSlash = false;
    for (const segment of segments) {
        if (!avoidDuplicatedSlash || !path.endsWith('/'))
            path += '/';
        avoidDuplicatedSlash = false;
        for (const token of segment) {
            if (token.type === 0 /* TokenType.Static */) {
                path += token.value;
            } else if (token.type === 1 /* TokenType.Param */) {
                const { value, repeatable, optional } = token;
                const param = value in params ? params[value] : '';
                const text = isArray(param)
                    ? param.join('/')
                    : param;
                if (!text) {
                    if (optional) {
                        // 可选条件下,如果path.endsWith('/'),则去掉最初面的'/'
                    } else { throw new Error(`Missing required param "${value}"`); }
                }
                path += text;
            }
        }
    }
    return path || '/';
}

传递path查找对应的路由

通过正则表达式匹配门路找到对应的matcher,通过matcher.parse()解析门路,拿到路由动静匹配的字符串
路由配置为: /:child1->router.push("/test")->params: ["child1": ["test"]]

path = location.path;
matcher = matchers.find(m => m.re.test(path));
if (matcher) {
    //params: ["child1": ["test"]]
    params = matcher.parse(path);
    name = matcher.record.name;
}

没有传递name也没有传递path

获取以后路由的matcher,而后合并传递的params,应用params造成新的门路

matcher = currentLocation.name
    ? matcherMap.get(currentLocation.name)
    : matchers.find(m => m.re.test(currentLocation.path));
name = matcher.record.name;
params = assign({}, currentLocation.params, location.params);
path = matcher.stringify(params);

寻找到路由后,遍历这条路由所有segment,拿到所有的matcher,波及门路上的多个Component

咱们加载子Component的同时也会加载父Component

const matched = [];
let parentMatcher = matcher;
while (parentMatcher) {
    matched.unshift(parentMatcher.record);
    parentMatcher = parentMatcher.parent;
}
return {
    name,
    path,
    params,
    matched,
    meta: mergeMetaFields(matched),
};
routerHistory.createHref()

base(除去域名后的残余门路),应用正则表达式替换为"#",比方"/router/vue-router-path-ranker.html?a=1#test#xixi"->"#test#xixi"

此时location为目前的门路,比方/one/two/three,那么组合起来就是"#test#xixi/one/two/three"

一般来说应该是"#/one/two/three",不会呈现两个#

const href = routerHistory.createHref(fullPath);
const BEFORE_HASH_RE = /^[^#]+#/;
function createHref(base, location) {
    return base.replace(BEFORE_HASH_RE, '#') + location;
}
resolve()详细分析

parseURL(): 解析失去fullPathpathqueryhash
matcher.resolve(): 解析失去matchedRouted,初始化传入routes失去的数组对象,蕴含该路由所映射的组件,对应的参数以及一系列的路由守卫,具体的数据结构如上面代码块所示
routerHistory.createHref(): 解析失去href

// matcher.resolve()
function resolve(location, currentLocation) {
    return {
        name,
        path,
        params,
        matched,
        meta: mergeMetaFields(matched),
    };
}
// matched: RouteRecordNormalized[]
interface RouteRecordNormalized {
    path: _RouteRecordBase['path']
    redirect: _RouteRecordBase['redirect'] | undefined
    name: _RouteRecordBase['name']
    components: RouteRecordMultipleViews['components'] | null | undefined
    children: RouteRecordRaw[]
    meta: Exclude<_RouteRecordBase['meta'], void>
    props: Record<string, _RouteRecordProps>
    beforeEnter: _RouteRecordBase['beforeEnter']
    leaveGuards: Set<NavigationGuard>
    updateGuards: Set<NavigationGuard>
    enterCallbacks: Record<string, NavigationGuardNextCallback[]>
    instances: Record<string, ComponentPublicInstance | undefined | null>
    aliasOf: RouteRecordNormalized | undefined
}

从下面的剖析,咱们能够晓得parseURL()matcher.resolve()routerHistory.createHref()的返回值,当初咱们能够对resolve()办法进行具体的剖析

function resolve(rawLocation, currentLocation) {
    currentLocation = assign({}, currentLocation || currentRoute.value);
    if (typeof rawLocation === 'string') {
        const locationNormalized = parseURL(parseQuery$1, rawLocation, currentLocation.path);
        const matchedRoute = matcher.resolve({ path: locationNormalized.path }, currentLocation);
        const href = routerHistory.createHref(locationNormalized.fullPath);
        return assign(locationNormalized, matchedRoute, {
            params: decodeParams(matchedRoute.params),
            hash: decode(locationNormalized.hash),
            redirectedFrom: undefined,
            href,
        });
    } else {
        let matcherLocation;
        if ('path' in rawLocation) {
            matcherLocation = assign({}, rawLocation, {
                path: parseURL(parseQuery$1, rawLocation.path, currentLocation.path).path,
            });
        } else {
            const targetParams = assign({}, rawLocation.params);
            for (const key in targetParams) {
                if (targetParams[key] == null) {
                    delete targetParams[key];
                }
            }
            matcherLocation = assign({}, rawLocation, {
                params: encodeParams(rawLocation.params),
            });
            currentLocation.params = encodeParams(currentLocation.params);
        }
        const matchedRoute = matcher.resolve(matcherLocation, currentLocation);
        const hash = rawLocation.hash || '';
        matchedRoute.params = normalizeParams(decodeParams(matchedRoute.params));
        const fullPath = stringifyURL(stringifyQuery$1, assign({}, rawLocation, {
            hash: encodeHash(hash),
            path: matchedRoute.path,
        }));
        const href = routerHistory.createHref(fullPath);
        return assign({
            fullPath,
            hash,
            query: stringifyQuery$1 === stringifyQuery
                ? normalizeQuery(rawLocation.query)
                : (rawLocation.query || {}),
        }, matchedRoute, {
            redirectedFrom: undefined,
            href,
        });
    }
}

parseURL(): 解析失去fullPathpathqueryhash

matcher.resolve(): 解析失去matchedRouted,初始化传入routes失去的Array,蕴含该路由所映射的组件,对应的参数以及一系列的路由守卫

routerHistory.createHref(): 解析失去href

typeof rawLocation === 'string'时,字符串代表路由path,通过matcher.resolve({path: xxx})获取对应的路由对象

const locationNormalized = parseURL(parseQuery$1, rawLocation, currentLocation.path);
const matchedRoute = matcher.resolve({ path: locationNormalized.path }, currentLocation);
const href = routerHistory.createHref(locationNormalized.fullPath);
// locationNormalized is always a new object
return assign(locationNormalized, matchedRoute, {
    params: decodeParams(matchedRoute.params),
    hash: decode(locationNormalized.hash),
    redirectedFrom: undefined,
    href,
});

typeof rawLocation !== 'string'时,也是同样的逻辑

  • 如果有path属性,则跟下面的逻辑统一,通过matcher.resolve({path: xxx})获取对应的路由对象
  • 如果没有path属性,解决params数据(从path中提取的已解码参数字典),而后再通过matcher.resolve({path: xxx})获取对应的路由对象

fullPath: path + (searchString && '?') + searchString + hash

let matcherLocation;
//======== 第1局部:整顿参数 ========
if ('path' in rawLocation) {
    matcherLocation = assign({}, rawLocation, {
        path: parseURL(parseQuery$1, rawLocation.path, currentLocation.path).path,
    });
} else {
    //...
    matcherLocation = assign({}, rawLocation, {
        params: encodeParams(rawLocation.params),
    });
    currentLocation.params = encodeParams(currentLocation.params);
}
//======== 第2局部:matcher.resolve ========
const matchedRoute = matcher.resolve(matcherLocation, currentLocation);
const hash = rawLocation.hash || '';
matchedRoute.params = normalizeParams(decodeParams(matchedRoute.params));
const fullPath = stringifyURL(stringifyQuery$1, assign({}, rawLocation, {
    hash: encodeHash(hash),
    path: matchedRoute.path,
}));
//======== 第3局部:routerHistory.createHref ========
const href = routerHistory.createHref(fullPath);
// ======== 第4局部:返回对象数据 ========
return assign({
    fullPath,
    hash,
    query: stringifyQuery$1 === stringifyQuery
        ? normalizeQuery(rawLocation.query)
        : (rawLocation.query || {}),
}, matchedRoute, {
    redirectedFrom: undefined,
    href,
});

resolve()最终返回值对象也就是下文中targetLocation的值和toLocation的值


通过解说下面的一系列办法,当初咱们能够开始解析push()->pushWithRedirect()

pushWithRedirect()真正push逻辑

咱们通过resolve(to)拿到了目前匹配的门路对象,而后解决重定向的逻辑,而后雷同Route对象逻辑,而后触发navigate()办法

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;
        });
}

navigate(toLocation, from)

function navigate(to, from) {
    let guards;
    // 将旧的路由的beforerRouteLeave守卫函数放入guards
  
    return (runGuardQueue(guards)// 先执行beforerRouteLeave守卫函数
        .then(() => {
            // 解决全局的beforeEach守卫函数
        })
        .then(() => {
            // 解决该路由的beforeRouteUpdate守卫函数
        })
        .then(() => {
            // 解决该路由的beforeEnter守卫函数
        }).then(() => {
            // 解决该路由的beforeRouteEnter守卫函数
        }).then(() => {
            // 解决全局的beforeResolve守卫函数
        })
        // catch any navigation canceled
        .catch(err => isNavigationFailure(err, 8 /* ErrorTypes.NAVIGATION_CANCELLED */)
            ? err
            : Promise.reject(err)));
}
function runGuardQueue(guards) {
    return guards.reduce((promise, guard) => promise.then(() => guard()), Promise.resolve());
}
路由切换失败状况剖析

除了执行一系列的导航守卫须要关注外,咱们还须要关注下error产生时的解决状况,次要的谬误状况分为

export const enum ErrorTypes {
  // they must be literals to be used as values, so we can't write
  // 1 << 2
  MATCHER_NOT_FOUND = 1,
  NAVIGATION_GUARD_REDIRECT = 2,
  NAVIGATION_ABORTED = 4,
  NAVIGATION_CANCELLED = 8,
  NAVIGATION_DUPLICATED = 16,
}

ErrorTypes.MATCHER_NOT_FOUND

当应用matcher.solve()寻找对应的matched数据时,如果传入的参数是路由的name,然而咱们却无奈依据name找到对应的matcher时,咱们返回ErrorTypes.MATCHER_NOT_FOUND的谬误

因为对于一个路由,name是惟一的标识,如果传入name,会依据matcherMap去找对应存储过的matcher

function createRouterMatcher(routes, globalOptions) {
    function resolve(location, currentLocation) {
        let matcher;
        let params = {};
        let path;
        let name;
        if ('name' in location && location.name) {
            matcher = matcherMap.get(location.name);
            if (!matcher)
                throw createRouterError(1 /* ErrorTypes.MATCHER_NOT_FOUND */, {
                    location,
                });
            //...
        }

        return {
            name,
            path,
            params,
            matched,
            meta: mergeMetaFields(matched),
        };
    }
}

ErrorTypes.NAVIGATION_ABORTED

如上面代码块所示,咱们导航守卫会传递对应的next()提供给内部应用,比方开发者在内部的next()返回false,则触发NAVIGATION_ABORTED,示意勾销导航

ErrorTypes.NAVIGATION_GUARD_REDIRECT

如上面代码块所示,咱们导航守卫会传递对应的next()提供给内部应用,比方开发者在内部的next()返回"/login"或者{"name": "login"},则isRouteLocation=true,从而返回NAVIGATION_GUARD_REDIRECT,示意导航重定向到其它路由

// guards.push(guardToPromiseFn(guard, to, from));
function guardToPromiseFn(guard, to, from, record, name) {
    return () => new Promise((resolve, reject) => {
        const next = (valid) => {
            if (valid === false) {
                reject(createRouterError(4 /* ErrorTypes.NAVIGATION_ABORTED */, {
                    from,
                    to,
                }));
            } else if (valid instanceof Error) {
                reject(valid);
            } else if (isRouteLocation(valid)) {
                reject(createRouterError(2 /* ErrorTypes.NAVIGATION_GUARD_REDIRECT */, {
                    from: to,
                    to: valid,
                }));
            } else {
                resolve();
            }
        };
    }
}

ErrorTypes.NAVIGATION_CANCELLED

如上面代码块所示,当应用push()->pushWithRedirect()->navigate()->finalizeNavigation()时,会进行checkCanceledNavigation()的检测,如果以后要跳转的路由跟pushWithRedirect()的路由不同时,阐明又有新的导航曾经产生,之前的导航勾销

ErrorTypes.NAVIGATION_DUPLICATED

如上面代码块所示,如果没有应用force,当检测到雷同路由时,会产生NAVIGATION_DUPLICATED谬误,阻止持续调用navigate()->finalizeNavigation()

isSameRouteLocation: matchedparamsqueryhash都雷同

function pushWithRedirect(to, redirectedFrom) {
    const targetLocation = (pendingLocation = resolve(to));
    if (!force && isSameRouteLocation(stringifyQuery$1, from, targetLocation)) {
        failure = createRouterError(16 /* ErrorTypes.NAVIGATION_DUPLICATED */, { to: toLocation, from });
    }
      if(failure) return Promise.resolve(failure);
    return navigate(toLocation, from).then((failure) => {
        if (!failure) {
            failure = finalizeNavigation(toLocation, from, true, replace, data);
        }
    });
}
function finalizeNavigation(toLocation, from, isPush, replace, data) {
    // a more recent navigation took place
    const error = checkCanceledNavigation(toLocation, from);
    if (error)
        return error;
}
function checkCanceledNavigation(to, from) {
    if (pendingLocation !== to) {
        return createRouterError(8 /* ErrorTypes.NAVIGATION_CANCELLED */, {
            from,
            to,
        });
    }
}

finalizeNavigation()

从上面代码块能够晓得,finalizeNavigation()的步骤为:

  • 触发routerHistory.replace/routerHistory.push更新
  • 更新currentRoute.value为目前的路由门路
  • 检测是否是初始化阶段,如果是初始化阶段,则触发setupListeners()办法
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);
    }
    // accept current navigation
    currentRoute.value = toLocation;
    handleScroll(toLocation, from, isPush, isFirstNavigation);
    markAsReady();
}
function markAsReady(err) {
    if (!ready) {
        // still not ready if an error happened
        ready = !err;
        setupListeners();
        readyHandlers
            .list()
            .forEach(([resolve, reject]) => (err ? reject(err) : resolve()));
        readyHandlers.reset();
    }
    return err;
}

setupListeners()注册pop操作相干监听办法

初始化会进行一次push()操作,此时就是初始化阶段

!readypush()->navigate()->finalizeNavigation()

初始化阶段会进行routerHistory.listen()的办法注册

function finalizeNavigation(toLocation, from, isPush, replace, data) {
    markAsReady();
}
function markAsReady(err) {
    if (!ready) {
        ready = !err;
        setupListeners();
    }
    return err;
}
function setupListeners() {
    // avoid setting up listeners twice due to an invalid first navigation
    if (removeHistoryListener)
        return;
    removeHistoryListener = routerHistory.listen((to, _from, info) => {
        if (!router.listening)
            return;
        // cannot be a redirect route because it was in history
        const toLocation = resolve(to);
        //...解决重定向的逻辑

        pendingLocation = toLocation;
        const from = currentRoute.value;
        // TODO: should be moved to web history?
        if (isBrowser) {
            saveScrollPosition(getScrollKey(from.fullPath, info.delta), computeScrollPosition());
        }
        //...去除错误处理
        navigate(toLocation, from)
            .then((failure) => {
                finalizeNavigation(
                    // after navigation, all matched components are resolved
                    toLocation, from, false);
                triggerAfterEach(toLocation, from, failure);
            })
            .catch(noop);
    });
}

那么初始化就注册的监听在什么时候会触发呢?这个监听又有什么作用呢?

在下面咱们说到useHistoryListeners()初始化的时候,咱们提供了listen()办法进行事件的注册,而后在popStateHandler()触发时,进行listeners注册办法的调用

因为咱们能够晓得,这个监听事件实质就是为了在用户进行浏览器后退按钮点击时,可能失常监听到路由变动并且主动实现Component切换

function useHistoryListeners(base, historyState, currentLocation, replace) {
    let listeners = [];
    let teardowns = [];
    // TODO: should it be a stack? a Dict. Check if the popstate listener
    // can trigger twice
    let pauseState = null;
    const popStateHandler = ({ state, }) => {...};
    function pauseListeners() {...}
    function listen(callback) {...}
    function beforeUnloadListener() {...}
    function destroy() {...}
    
    // set up the listeners and prepare teardown callbacks
    window.addEventListener('popstate', popStateHandler);
    window.addEventListener('beforeunload', beforeUnloadListener);
    return {
        pauseListeners,
        listen,
        destroy,
    };
}
const popStateHandler = ({ state, }) => {
    //...
    listeners.forEach(listener => {
        listener(currentLocation.value, from, {
            delta,
            type: NavigationType.pop,
            direction: delta
                ? delta > 0
                    ? NavigationDirection.forward
                    : NavigationDirection.back
                : NavigationDirection.unknown,
        });
    });
};

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()时调用
    },
};

因为篇幅起因,接下来的剖析请看下一篇文章Vue3相干源码-Vue Router源码解析(二)

参考文章

  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 来减少垃圾评论。了解你的评论数据如何被处理