前言
在正式进入vue-router
之前,我们先从框架的思维跳出来,仔细思考一下前端路由的实现原理,观察如下案例.
<body>
<a onclick="jump('/a')">A</a>
<a onclick="jump('/b')">B</a>
<a onclick="jump('/c')">C</a>
<div id="app">
</div>
</body>
在html
文件里新建三个a
标签,并且下面放一个空的div
.
在看js
代码之前,我们先快速回顾一遍 HTML5 history API
.(文章下面部分都将以目前主流的history API
作为路由方案讲解,至于hash
路由可自行研究)
history.pushState
可以往路由栈添加一条路由记录,从而伪装成页面的跳转.因为此API
调用仅仅只是改变了url
,并没有引起页面上的任何内容变化,浏览器也不会刷新.history.replaceState
的用法和history.pushState
,它们的区别在于history.pushState
是往路由栈添加一条路由记录,而history.replaceState
是将当前路由替换成一条新的路由.window.onpopstate
事件能监听到用户点击浏览器前进和后退按钮时触发的事件.
window.onpopstate
事件只能监听前进后退的事件,如果在代码中调用history.pushState
或history.replaceState
改变url
时,window.onpopstate
事件是不会触发的.
现在再看下面js
代码.当用户点击a
标签时触发jump
函数.在函数内部调用history.pushState({}, "",path)
,此时浏览器的url
会发生改变,但是页面的内容还没有发生任何变化.随后执行render
函数时,页面内容才开始发生真正的变化.
render
函数根据跳转路径的不同动态改变app
容器里面的内容,从而便模拟出了点击不同路径页面似乎发生了跳转的效果.
// a链接跳转
function jump(path){
history.pushState({}, "",path);
render(path);
}
//渲染内容
function render(path){
var app = document.getElementById("app");
switch(path){
case "/a":
app.innerText = "页面A";
break;
case "/b":
app.innerText = "页面B";
break;
case "/c":
app.innerText = "页面C";
break;
default:
app.innerText = "其他内容";
}
}
//监听前进后退事件
window.onpopstate = function(event) {
const path = location.pathname;
render(path);
};
效果如下:
从上面的案例中我们可以总结出前端路由的实现原理.
- 采用某种方式使
url
发生改变。这种方式可能是调用HTML5 history API
实现,也可能是点击前进后退或者改变路由hash
.但是不管采用哪种方式,它都不能造成浏览器刷新,仅仅只是单纯的url
发生变化. - 监听到
url
的变化之后,根据不同的路径获取渲染内容,再把内容填充到div
容器里.从上面案例可知,监听url
的变化一般在两个地方,第一是在window.onpopstate
包裹的回调函数里,第二是在执行history.pushState
或history.replaceState
的后面.
vue-router
的底层逻辑同样如此,比如平时中经常使用的<router-link to="home">
类似于上面案例中的a
标签,它的底层就是利用history.pushState
或history.replaceState
(当路由mode
选择'history'
时)改变url
的.而<router-view/>
相当于上方app
容器元素,监听不同路径再根据路径值拿到相应的页面组件并渲染出来.
路径解析
现有页面模板如下.router-link
和router-view
都是平时开发中使用比较多的全局组件.
<template>
<div>
<router-link :to="{ path: '/home' }">Home</router-link>
<router-view/>
</div>
</template>
router-link
相当于一个跳转链接,它的源码实现后面再讲.我们先假设该组件的内部通过获取to
里面的path
属性执行跳转操作.按照上面介绍的前端路由的原理,路由跳转操作第一步通过HTML5 history API
单纯改变url
,这个在router-link
组件内部可以做到.
第二步监听url变化后,需要根据path
拿到渲染内容.当前案例下,path
对应的路径是/home
,那么渲染的内容我们可以很容易想到在路由配置表中寻找,比如如下配置.
const routes = [
{
path: '/home',
component: Home
},
{
path: '/login',
component: () => import(/* webpackChunkName: "about" */ '../views/Login/Login.vue')
}
]
我们可以引入路由文件中的routes
数组,循环遍历判端path
和/home
是否匹配,一旦匹配成功了就能知道要渲染的内容就是Home
组件.
上面的情况属于最简单的一种,因为实际中我们存在嵌套路由和动态参数,仅仅只用上面的判断方式并不能满足日常开发需求.
我们接下来看看源码是如何处理路径匹配的.源码创建了一个VueRouter
的构造函数,在它里面编写了路由所有的实现逻辑.
//页面调用
const router = new VueRouter({
mode: 'history',
routes
})
// VueRouter构造函数
var VueRouter = function VueRouter (options) {
this.app = null;
this.apps = [];
this.options = options; //options对应上面页面调用里传入的参数
this.beforeHooks = [];
this.resolveHooks = [];
this.afterHooks = [];
this.matcher = createMatcher(options.routes || [], this);
var mode = options.mode || 'hash';
this.mode = mode;
switch (mode) {
case 'history':
this.history = new HTML5History(this, options.base);
break
case 'hash':
this.history = new HashHistory(this, options.base, this.fallback);
break
case 'abstract':
this.history = new AbstractHistory(this, options.base);
break
default:
if (process.env.NODE_ENV !== 'production') {
assert(false, ("invalid mode: " + mode));
}
}
};
在上面VueRouter
构造函数里主要做了两件事.
- 第一它会根据
mode
配置不同生成相对应的history
对象,history
对象是具体执行各种路由操作的执行者. - 第二它通过调用
createMatcher
函数生成了this.matcher
对象.这个this.matcher
正是解决我们上面遭遇的路径匹配的难题.
我们接下来看一下createMatcher
内部是怎么根据路径寻找到要渲染的组件.
createMatcher实现
假设开发者配置的路由表routes
如下,它被传入下方createMatcher
函数内部执行.
const routes = [
{
path: '/home',
component: Home, // 首页
name:"home",
children: [{
path: 'list',
name:"list",
component: List // 列表页
}]
},
{
path: '/login',
name:"login",
component: Login // 登录页
}
]
如果用户访问/home/list
,很明显匹配的路由组件应该是List
,但通过上面的数据结构并不能快速方便找出与路径相匹配的路由组件.
因此createRouteMap
函数对routes
树形结构数组作了转换,转化后能更方便找出匹配的组件.createRouteMap(routes)
执行完毕后返回三条数据,转化后的数据结构如下:
pathList = ["/home","/home/list","/login"];
pathMap = {
"/home":{
path: "/home",
name: "home",
regex: /^\/home(?:\/(?=$))?$/i,
parent: undefined,
components: {default: {…}}, //对应的页面组件Home
meta: {}
},
"/home/list":{
path: "/home/list",
meta: {}
name: "list",
parent:{ path:"/home",... }, // 对应上面的home路由
regex:/^\/home\/list(?:\/(?=$))?$/i,
components: {default: {…}}, //对应的页面组件List
},
"/login":{ ... }
}
nameMap = { // 和上面数据结构类似,只不过这里将name作为key
"home":{...},
"list":{...},
"login":{...}
}
下面的createRouteMap
做的事情就是将路由表routes
转换成上面的数据结构.我们仔细观察上面三条数据的特征,有三个属性非常关键.
parent
属性用来描述当前路由是否存在父级路由.如果每一个子路由都用parent
属性存着父路由,那么不管嵌套路由的层级有多深,通过某一级子路由的parent
属性一直往上寻找,直到找到它的所有祖先路由,这点特征将会在后面渲染嵌套路由时用到.regex
是根据当前配置路径动态生成的正则表达式.如何判断当前访问路径是否命中这个路由呢?就是拿访问路径与正则相匹配.使用正则匹配还能解决另外一个问题,比如path
配置成动态形式/detail/:id
,那么要求访问路径如果为/detail/7
也是能够匹配上该路由的,而它的正则为/^\/detail\/((?:[^\/]+?))(?:\/(?=$))?$/i
就能与访问路径匹配上.components
是一个对象,里面包含一个default
属性对应着要渲染的页面组件.
现在将静态的配置routes
转化成了上述三种数据结构,那现在根据访问路径找出匹配路由就容易多了.匹配的逻辑写在下面match
函数中.
function createMatcher (
routes,
router
) {
const { pathList,pathMap,nameMap } = createRouteMap(routes);
function addRoutes (routes) {
createRouteMap(routes, pathList, pathMap, nameMap);
}
function match (
raw,
currentRoute,
redirectedFrom
) {
var location = normalizeLocation(raw, currentRoute, false, router);
var name = location.name;
if (name) {
// 根据路由name匹配
...
} else if (location.path) { // 根据path匹配
location.params = {};
for (var i = 0; i < pathList.length; i++) {
var path = pathList[i];
var record$1 = pathMap[path];
if (matchRoute(record$1.regex, location.path, location.params)) {
return _createRoute(record$1, location, redirectedFrom)
}
}
}
// no match
return _createRoute(null, location)
}
return {
match: match,
addRoutes: addRoutes
}
我们主要看根据path
匹配路由的方式(name
匹配可下去自行研究),它会直接遍历一维数组pathList
,取出每一个path
值.
再通过path
值在pathMap
找出相应的路由配置,取出其中的正则表达式regex
与访问路径相匹配,正则如果校验成功就表示成功获取到了路由配置record$1
,再将它传入_createRoute
函数生成一个页面上即将要展现的路由.
createMatcher
函数执行完会返回两个参数.
- 第一个是
match
函数,它的作用是拿访问路径和路由配置表去一一匹配,最后返回一个正确的路由对象. - 第二个是
addRoutes
函数,这个函数可以往pathList
,pathMap
和nameMap
继续添加路由信息.官方提供的this.$router.addRoutes
动态路由出处正是在此.
回到我们最初提出的问题,怎么根据访问路径找出匹配的路由对象.最关键的地方是需要兼容开发者在编写路由表时可能采用嵌套路由和动态参数的写法,最后createMatcher
函数返回的match
函数可以解决这个问题.
在上述整个路径解析流程中,我们有两个细节没有展开讲.
- 一个是
match
函数最后命中了某个路由配置后,为什么还要使用_createRoute
函数重新创建一个新的路由,直接将命中的那个路由配置返回不行吗? - 第二个是我们现在已经知道
createRouteMap
将静态的路由表转化成了pathList
,pathMap
和nameMap
这三种数据结构,但它内部到底是怎么做到的?
_createRoute实现
通过正则校验后命中的路由对象record
在match
函数里被传入下面的createRoute
执行.该函数内重新创建了一个route
新对象,并将原来路由配置信息一一添加上去,但有一个属性matched
需要引起注意.
function createRoute (
record,
location,
redirectedFrom,
router
) {
var stringifyQuery = router && router.options.stringifyQuery;
var query = location.query || {};
try {
query = clone(query);
} catch (e) {}
var route = {
name: location.name || (record && record.name),
meta: (record && record.meta) || {},
path: location.path || '/',
hash: location.hash || '',
query: query,
params: location.params || {},
fullPath: getFullPath(location, stringifyQuery),
matched: record ? formatMatch(record) : []
};
return Object.freeze(route);//禁止route被修改
}
matched
属性里面存储着所有祖先路由的配置对象,这在后面界面渲染嵌套路由时会用到.它通过formatMatch
函数得到.
formatMatch
函数实现如下,函数内创建一个数组res
,通过循环遍历不断获取路由的父级并存储到res
中.最终返回的route
对象除了包含基础的配置信息,还在matched
属性里存储着所有祖先路由的配置对象
function formatMatch (record) {
var res = [];
while (record) {
res.unshift(record);
record = record.parent;
}
return res
}
createRouteMap实现
routes
是开发者编写的静态路由表,最后通过createRouteMap
函数处理返回pathList
、pathMap
和nameMap
.
createRouteMap
函数代码里,首先创建三个变量,再传入到addRouteRecord
执行,由此可见真正执行数据转换操作的逻辑全放在了addRouteRecord
函数里.
createRouteMap
单独对path:"*"
的情况做了处理,它将*
移到了pathList
数组的最后面.因为path:"*"
对应的路由配置里的正则表达式是/^((?:.*))(?:\/(?=$))?$/i
,它能匹配上所有的正规路径,因此要把它放到最后去匹配.只有前面路由都没匹配上时才轮的到它出场,这种场景就是常见的404
页面.
function createRouteMap (
routes,
oldPathList,
oldPathMap,
oldNameMap
) {
var pathList = oldPathList || [];
var pathMap = oldPathMap || Object.create(null);
var nameMap = oldNameMap || Object.create(null);
routes.forEach(function (route) {
addRouteRecord(pathList, pathMap, nameMap, route);
});
for (var i = 0, l = pathList.length; i < l; i++) {
if (pathList[i] === '*') {
pathList.push(pathList.splice(i, 1)[0]); //塞到数组最后面
l--;
i--;
}
}
return {
pathList: pathList,
pathMap: pathMap,
nameMap: nameMap
}
}
addRouteRecord
函数内部代码如下.record
是重新创建的路由配置,它里面有几个属性值得关注.
- path:
path
不是从原来中的route
直接获取的,而是通过normalizePath
做了中间处理.目的就是为了在存在嵌套路由的情况下,子路由的path
能够和祖先路由的path
拼接后再返回. - regex:
compileRouteRegex
函数针对每个不同path
动态生成与之匹配的正则表达式. - parent:
parent
属性会记录父级路由.下面代码里会判断route.children
存不存在,如果存在说明存在子级路由.然后将route.children
遍历循环,每一个子级路由都会递归调用addRouteRecord
函数,子级调用的时候会将record
作为parent
参数传递过去.那么递归调用时,子级创建的record
记录的parent
就会有值.
function addRouteRecord (
pathList,
pathMap,
nameMap,
route,
parent,
matchAs
) {
var path = route.path;
var name = route.name;
var pathToRegexpOptions =
route.pathToRegexpOptions || {};
var normalizedPath = normalizePath(path, parent, pathToRegexpOptions.strict);
if (typeof route.caseSensitive === 'boolean') {
pathToRegexpOptions.sensitive = route.caseSensitive;
}
var record = {
path: normalizedPath,
regex: compileRouteRegex(normalizedPath, pathToRegexpOptions),
components: route.components || { default: route.component },
instances: {},
name: name,
parent: parent,
matchAs: matchAs,
redirect: route.redirect,
beforeEnter: route.beforeEnter,
meta: route.meta || {},
props:
route.props == null
? {}
: route.components
? route.props
: { default: route.props }
};
if (route.children) {
route.children.forEach(function (child) {
var childMatchAs = matchAs
? cleanPath((matchAs + "/" + (child.path)))
: undefined;
addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs);
});
}
if (!pathMap[record.path]) {
pathList.push(record.path);
pathMap[record.path] = record;
}
if (route.alias !== undefined) {
var aliases = Array.isArray(route.alias) ? route.alias : [route.alias];
for (var i = 0; i < aliases.length; ++i) {
var alias = aliases[i];
if (process.env.NODE_ENV !== 'production' && alias === path) {
warn(
false,
("Found an alias with the same value as the path: \"" + path + "\". You have to remove that alias. It will be ignored in development.")
);
// skip in dev to make it work
continue
}
var aliasRoute = {
path: alias,
children: route.children
};
addRouteRecord(
pathList,
pathMap,
nameMap,
aliasRoute,
parent,
record.path || '/' // matchAs
);
}
}
if (name) {
if (!nameMap[name]) {
nameMap[name] = record;
} else if (process.env.NODE_ENV !== 'production' && !matchAs) {
warn(
false,
"Duplicate named routes definition: " +
"{ name: \"" + name + "\", path: \"" + (record.path) + "\" }"
);
}
}
}
页面渲染
上面已经将路径解析的流程介绍了一遍,总结起来做了这样的一件事.点击某个<router-link to="/home/list"/>
将要跳转的链接传递给VueRoute
里面matcher
对象的match
函数,它拿到链接以后,去pathList
、pathMap
和nameMap
里面寻找相匹配的路由对象,找到后返回结果.match
函数返回的路由对象除了包含一些基础配置信息:path
,name
,meta
,params
等等.另外它还包含了一个特别重要的属性matched
,装载着当前以及所有祖先路由的配置信息.
比如上面案例访问/home/list
最终返回的路由数据如下:
{
fullPath: "/home/list"
hash: ""
matched: Array(2)
0: {path: "/home", regex: /^\/home(?:\/(?=$))?$/i, components: {…}, instances: {…}, name: "home", …}
1: {path: "/home/list", regex: /^\/home\/list(?:\/(?=$))?$/i, components: {…}, instances: {…}, name: "list", …}
meta: {}
name: "list"
params: {}
path: "/home/list"
query: {}
}
我们现在再回到最初的问题,点击下面<router-link/>
已经可以拿到路由对象,对象的matched
属性装载着待渲染的组件.
<template>
<div>
<router-link :to="{ path: '/home' }">Home</router-link>
<router-view/>
</div>
</template>
现在存在一个非常棘手的问题.router-link
和router-view
是两个不同的组件,即使router-link
知道要渲染的组件是什么,那它怎么把数据传递给router-view
,另外它怎么能触发router-view
组件重新渲染.
router-link
虽然不能直接与router-view
通信,但是它们两个都可以获取Vue
的根组件实例.如果router-link
将数据传给根组件实例,并且修改根组件的响应式状态,那样所有子组件的render
函数都会重新执行一遍.
router-view
也不例外,它在执行render
函数时,可以拿到根组件上存放的路由对象,这样就可以正常渲染组件页面内容了.
现在看一下vue-router
源码如何一步步实现.我们在初始化应用创建路由对象的时候一般这样写.
//使用vue-router插件
Vue.use(VueRouter);
//创建router实例
const router = new VueRouter({
mode: 'history',
routes
})
//创建根组件实例
new Vue({
router,
render: h => h(App)
}).$mount('#app')
Vue.use(ob)
此api
是给Vue
安装插件,它内部一般都是调用ob
对象的install
来完成插件的安装.接下来我们重点看一下VueRouter
的install
方法.
install
函数的参数Vue
是Vue
的构造函数,并不是实例对象.源码中给Vue
的mixin
里面添加了一个生命周期钩子函数beforeCreate
.这将意味着使用当前Vue
构造函数创建的所有实例对象在创建过程中都会运行一遍beforeCreate
钩子函数.
var _Vue;
function install (Vue) {
if (install.installed && _Vue === Vue) { return } // 确保插件只安装一次
install.installed = true;
_Vue = Vue;
var isDef = function (v) { return v !== undefined; };
Vue.mixin({
beforeCreate: function beforeCreate () {
if (isDef(this.$options.router)) {
this._routerRoot = this;
this._router = this.$options.router;
this._router.init(this);
Vue.util.defineReactive(this, '_route', this._router.history.current);
} else {
this._routerRoot = (this.$parent && this.$parent._routerRoot) || this;
}
},
destroyed: function destroyed () {
...
}
});
Object.defineProperty(Vue.prototype, '$router', {
get: function get () { return this._routerRoot._router }
});
Object.defineProperty(Vue.prototype, '$route', {
get: function get () { return this._routerRoot._route }
});
Vue.component('RouterView', View);
Vue.component('RouterLink', Link);
}
根实例在创建的过程中也会运行beforeCreate
函数,我们在创建根实例的时候是有把router
对象传递给它的(代码如下),那么在beforeCreate
函数里,这个this
指的就是等待创建的vue
实例.
在vue
源码里创建每个vue
实例时都会把配置对象赋值给$options
.因此执行beforeCreate
函数时,如果发现this.$options.router
存在,那么说明这个this
一定是根实例.然后它将VueRouter
创建那个router
对象赋值给根实例的_router
属性,并执行了init
方法.
这里存在一句很关键的代码Vue.util.defineReactive(this, '_route', this._router.history.current)
,他在根实例上创建了一个响应式的状态_route
,这将意味着如果谁修改根实例的_route
就可以让所有子组件的render
函数重新执行.
最后Vue.prototype
原型对象上挂载了两个值分别是$router
和$route
,它们分别指向new VueRouter
创建的router
对象以及根实例上的响应式状态_route
.我们平时开发中在子组件内经常会调用this.$router.push
方法以及this.$route.params
获取参数,这些api
的出处正在于此.因为this.$router
和this.$route
都是挂载Vue
原型对象上的,所以所有Vue
实例都可以直接调用.
//创建根组件实例
new Vue({
router,
render: h => h(App)
}).$mount('#app')
VueRouter init实现
现在我们看一下this._router.init(this)
都做了哪些初始化操作.页面第一次加载时,Vue
根实例作为参数传递给init
函数执行.
VueRouter
的构造函数在上面已经介绍过,它在实例化的时候会创建一个history
对象用于路由间参数传递和跳转.
观察下面代码,获取history
对象,并判断history
对象是哪种模式创建出来的,随后调用transitionTo
函数执行跳转.
VueRouter.prototype.init = function init (app) {
var this$1 = this;
this.apps.push(app);
if (this.app) {
return
}
this.app = app;
var history = this.history;
if (history instanceof HTML5History) { // history模式
history.transitionTo(history.getCurrentLocation());
} else if (history instanceof HashHistory) { // hash模式
...
}
history.listen(function (route) {
this$1.apps.forEach(function (app) {
app._route = route;
});
});
};
transitionTo
是history
对象提供的跳转函数.在项目初始化的时候,需要对当前路径做一次跳转,如果当前页面是首页,那就相当于对/
根路径做一下跳转.
transitionTo
和普通的链接跳转不一样,它里面封装了很强大的能力,能够确保每次执行完毕后,根实例的_route
更新为最新的路由对象,从而使页面重新渲染.
在init
函数的最后面,调用了listen
函数,看上去这是一个监听函数.history.listen
代码如下,它其实是利用观察者模式将上面listen
包裹的函数存到history
对象的cb
属性上.一旦执行this.history.cb(new_route)
可想而知,listen
回调函数就会触发.this$1.apps
里面是包含根组件实例的,一旦最新的路由对象赋值给根实例的响应式状态就会引起页面重新渲染.
此时大概也能猜得出transitionTo
执行后能引起页面重新渲染,肯定是因为内部调用了this.history.cb(new_route)
.
History.prototype.listen = function listen (cb) {
this.cb = cb;
};
transitionTo实现
下面代码中出现了熟悉的this.router.match
函数,我们在上面第二节花了大量篇幅讲解的match
函数终于在这里派上用场了.match
函数根据想要跳转的路径location
找到了相适配的路由对象route
返回.
this.confirmTransition
函数传入route
参数后 ,它并没有马上执行跳转,而是做了一连串的拦截判断,这正是路由守卫发挥作用的地方.
路由守卫是vue-router
源码中的精华部分,后面会单独开一节重点研究this.confirmTransition
内部关于路由守卫的实现.
我们现在只需要理解this.confirmTransition
里面封装了大量的路由守卫的逻辑,如果通过了路由守卫的层层校验,最后就会执行this.confirmTransition
第二个参数(回调函数),这就将意味着路由守卫已经全部放行,可以对当前路由对象route
执行跳转.
History.prototype.transitionTo = function transitionTo (
location,
onComplete,
onAbort
) {
var this$1 = this;
// this.router.是new VueRouter出来的实例对象
var route = this.router.match(location, this.current); //获取最新即将要跳转的路由对象.
this.confirmTransition(
route,
function () {
this$1.updateRoute(route);
onComplete && onComplete(route);//跳转成功的回调函数
this$1.ensureURL(); //为了确保浏览器显示的url和route的path保持一致
// fire ready cbs once
if (!this$1.ready) {
this$1.ready = true;
this$1.readyCbs.forEach(function (cb) {
cb(route);
});
}
},
function (err) {
if (onAbort) {
onAbort(err); //跳转失败的回调函数
}
if (err && !this$1.ready) {
this$1.ready = true;
this$1.readyErrorCbs.forEach(function (cb) {
cb(err);
});
}
}
);
};
我们接下来看下回调函数this$1.updateRoute(route)
里面的实现.
重点来了,函数内执行了history
对象的cb
函数,正如我们在上面的分析而言,通过调用history.cb(route)
让根组件实例的响应式状态_route
得到更新,从而使所有子组件render
函数重新执行.
History.prototype.updateRoute = function updateRoute (route) {
var prev = this.current;
this.current = route;
this.cb && this.cb(route);
this.router.afterHooks.forEach(function (hook) {
hook && hook(route, prev);
});
};
现在我们可以将整个过程梳理一遍,history.transitionTo(location)
是执行跳转的函数.它首先将路径location
传给match
函数获得匹配的路由对象route
.然后穿过this.confirmTransition
层层设置的路由守卫,最后执行this$1.updateRoute(route)
,将路由对象赋值给根组件的响应式状态_route
,从而启动页面开始渲染.
我们现在来看一下<router-link />
组件的实现.它的点击事件绑定的是handler
,而handler
调用的是router
对象的push
或replace
方法.
var Link = {
name: 'RouterLink',
props: {
to: {
type: toTypes,
required: true
},
tag: {
type: String,
default: 'a'
},
event: {
type: eventTypes,
default: 'click'
}
},
render: function render (h) {
var this$1 = this;
var router = this.$router;
var current = this.$route;
var ref = router.resolve(
this.to,
current,
this.append
);
var location = ref.location;
var route = ref.route;
var href = ref.href;
...
var handler = function (e) {
if (guardEvent(e)) {
if (this$1.replace) {
router.replace(location, noop);
} else {
router.push(location, noop);
}
}
};
var on = { click: guardEvent };
if (Array.isArray(this.event)) {
this.event.forEach(function (e) {
on[e] = handler;
});
} else {
on[this.event] = handler;
}
...
return h(this.tag, data, this.$slots.default)
}
};
我们以push
方法为例,router
对象的push
方法调用的是history
对象的push
方法.而history
对象的push
方法调用的也是transitionTo
函数.
VueRouter.prototype.push = function push (location, onComplete, onAbort) {
var this$1 = this;
if (!onComplete && !onAbort && typeof Promise !== 'undefined') {
return new Promise(function (resolve, reject) {
this$1.history.push(location, resolve, reject);
})
} else {
this.history.push(location, onComplete, onAbort);
}
};
HTML5History.prototype.push = function push (location, onComplete, onAbort) {
var this$1 = this;
var ref = this;
var fromRoute = ref.current;
this.transitionTo(location, function (route) {
pushState(cleanPath(this$1.base + route.fullPath));
handleScroll(this$1.router, route, fromRoute, false);
onComplete && onComplete(route);
}, onAbort);
};
由此可见路由里面的所有的跳转操作最后运行的都是transitionTo
函数,它一方面可以更新根组件实例状态启动页面重新渲染,第二它里面包含了路由守卫的逻辑,每次执行一次跳转都要通过路由守卫的校验.
RouterView实现
transitionTo
函数执行成功后,根组件实例的状态_route
已经被赋予最新的路由对象,所有子组件的render
函数将会重新执行.
<router-view>
组件内的render
函数也会重新执行,它可以从根实例拿到最新的路由对象并开始渲染页面内容.
我们还是以跳转/home/list
为例,最终<router-view>
执行render
时,从根实例拿到的路由数据如下:
{
fullPath: "/home/list"
hash: ""
matched: Array(2)
0: {path: "/home", regex: /^\/home(?:\/(?=$))?$/i, components: {default:{...}}, instances: {…}, name: "home", …}
1: {path: "/home/list", regex: /^\/home\/list(?:\/(?=$))?$/i, components: {default:{...}}, instances: {…}, name: "list", …}
meta: {}
name: "list"
params: {}
path: "/home/list"
query: {}
}
下面RouterView
的函数里,由于设置了functional: true
,因此ref.parent
才是当前<router-view />
的虚拟dom
.
我们平时在开发多层级的嵌套路由时,<router-view />
也会写多个,它们会分布在不同组件的模板里.每一个<router-view />
只负责渲染它自己的那部分.
我们看上面路由对象的的数据结构可知,matched
里面存储着两个元素,一个是父级路由name:home
,另一个是子级路由name:list
.那么对应着页面模板里也会有两个<router-view />
,一个渲染父级元素,一个渲染子级元素.
因此下面代码会执行while (parent && parent._routerRoot !== parent)
循环,当前的虚拟dom
节点会不断往上寻找,直到找到根节点,目的就是为了确认当前这个<router-view />
位于第几个层级,对应的值depth
是多少.
一旦得到了depth
就可以调用route.matched[depth]
取出当前<router-view />
对应的路由对象,再从路由对象里面调用matched.components[name]
获取页面组件(name
默认为default
),接下来就可以顺利渲染页面组件内容了.
var View = {
name: 'RouterView',
functional: true,
props: {
name: {
type: String,
default: 'default'
}
},
render: function render (_, ref) {
var props = ref.props;
var children = ref.children;
var parent = ref.parent;
var data = ref.data;
// directly use parent context's createElement() function
// so that components rendered by router-view can resolve named slots
var h = parent.$createElement;
var name = props.name;
var route = parent.$route;
var cache = parent._routerViewCache || (parent._routerViewCache = {});
// determine current view depth, also check to see if the tree
// has been toggled inactive but kept-alive.
var depth = 0;
var inactive = false;
while (parent && parent._routerRoot !== parent) {
var vnodeData = parent.$vnode && parent.$vnode.data;
if (vnodeData) {
if (vnodeData.routerView) {
depth++;
}
if (vnodeData.keepAlive && parent._inactive) {
inactive = true;
}
}
parent = parent.$parent;
}
data.routerViewDepth = depth;
var matched = route.matched[depth];
var component = cache[name] = matched.components[name];
... //省略
return h(component, data, children)
}
};
尾言
本篇文章已经将vue-router
的核心工作流程和源码整体梳理了一遍,下一小结将会重点介绍vue-router
中路由守卫的实现.
转载:https://blog.csdn.net/brokenkay/article/details/115773964