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

57次阅读

共计 49193 个字符,预计需要花费 123 分钟才能阅读完成。

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

正文完
 0