前言
在正式进入
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
中路由守卫的实现.