本文基于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属性

属性形容
hrefhttps://test.example.com:8090/vue/router#test?search=1
protocolhttps:
host/hostnametest.example.com
port8090
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)的参数阐明
ParameterTypeDescription
basestring提供可选的 base。默认是 location.pathname + location.search。如果 head 中有一个 <base>,它的值将被疏忽,而采纳这个参数。但请留神它会影响所有的 history.pushState() 调用,这意味着如果你应用一个 <base> 标签,它的 href 值必须与这个参数相匹配 (请疏忽 # 前面的所有内容)
// at https://example.com/foldercreateWebHashHistory() // 给出的网址为 `https://example.com/folder#`createWebHashHistory('/folder/') // 给出的网址为 `https://example.com/folder/#`// 如果在 base 中提供了 `#`,则它不会被 `createWebHashHistory` 增加createWebHashHistory('/folder/#/app/') // 给出的网址为 `https://example.com/folder/#/app/`// 你应该防止这样做,因为它会更改原始 url 并打断正在复制的 urlcreateWebHashHistory('/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 segmentif (!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 objectreturn 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流程浅析