前言
本文是vue-router 2.x源码分析的第二篇,主要看matcher和history的处理过程!
实例代码
同上节
1、matcher
看下createMatcher函数
function createMatcher (routes,router) {
var ref = createRouteMap(routes);
var pathList = ref.pathList;
var pathMap = ref.pathMap;
var nameMap = ref.nameMap;
function addRoutes (routes) {
createRouteMap(routes, pathList, pathMap, nameMap);
}
function match (raw,currentRoute,redirectedFrom) {
...
}
function redirect (record,location) {
...
}
function alias (record,location,matchAs) {
...
}
function _createRoute (record,location,redirectedFrom) {
...
}
return {
match: match,
addRoutes: addRoutes
}
}
该函数返回了一个包含match和addRoutes属性的对象,这里主要看下createRouteMap函数:
function createRouteMap (routes,oldPathList,oldPathMap,oldNameMap) {
//pathList是用来控制path匹配优先级的
var pathList = oldPathList || [];
var pathMap = oldPathMap || Object.create(null);
var nameMap = oldNameMap || Object.create(null);
//循环调用addRouteRecord函数完善pathList, pathMap, nameMap
routes.forEach(function (route) {
addRouteRecord(pathList, pathMap, nameMap, route);
});
// 确保通配符路径总是在pathList数组末尾
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
}
}
该函数将routes转化成这样的对象:
ref:{
nameMap:Object //name路由
pathList:Array(3)
pathMap:Object //path路由
__proto__:Object
}
//本实例中是path路由,生成的pathMap如下:
pathMap:{
"":Object
/bar:Object
/foo:Object
}
//其中第一个Object如下,该对象即是路由记录record:
{
beforeEnter:undefined
components:Object
instances:Object
matchAs:undefined
meta:Object
name:undefined
parent:undefined
path:""
props:Object
redirect:undefined
regex:/^(?:\/(?=$))?$/i
__proto__:Object
}
可以看到createRouteMap主要调用了addRouteRecord函数,该函数如下:
function addRouteRecord (pathList,pathMap,nameMap,route,parent,matchAs) {
var path = route.path;
var name = route.name;
//略过错误处理部分
...
//修正path
var normalizedPath = normalizePath(path, parent);
//根据传入的route构造路由记录record
var record = {
path: normalizedPath,
regex: compileRouteRegex(normalizedPath),
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) {
// Warn if route is named and has a default child route.
// If users navigate to this route by name, the default child will
// not be rendered (GH Issue #629)
{
if (route.name && route.children.some(function (child) { return /^\/?$/.test(child.path); })) {
warn(
false,
"Named Route '" + (route.name) + "' has a default child route. " +
"When navigating to this named route (:to=\"{name: '" + (route.name) + "'\"), " +
"the default child route will not be rendered. Remove the name from " +
"this route and use the name of the default child route for named " +
"links instead."
);
}
}
route.children.forEach(function (child) {
var childMatchAs = matchAs
? cleanPath((matchAs + "/" + (child.path)))
: undefined;
addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs);
});
}
//处理路由别名
if (route.alias !== undefined) {
//alias是数组
if (Array.isArray(route.alias)) {
route.alias.forEach(function (alias) {
var aliasRoute = {
path: alias,
children: route.children
};
addRouteRecord(pathList, pathMap, nameMap, aliasRoute, parent, record.path);
});
//alias是字符串
} else {
var aliasRoute = {
path: route.alias,
children: route.children
};
addRouteRecord(pathList, pathMap, nameMap, aliasRoute, parent, record.path);
}
}
//填充pathList,pathMap,nameMap
if (!pathMap[record.path]) {
pathList.push(record.path);
pathMap[record.path] = record;
}
if (name) {
if (!nameMap[name]) {
nameMap[name] = record;
} else if ("development" !== 'production' && !matchAs) {
warn(
false,
"Duplicate named routes definition: " +
"{ name: \"" + name + "\", path: \"" + (record.path) + "\" }"
);
}
}
}
2、History
- 先看HashHistory
function HashHistory (router, base, fallback) {
History.call(this, router, base);
// check history fallback deeplinking
if (fallback && checkFallback(this.base)) {
return
}
ensureSlash();
}
History长这样
var History = function History (router, base) {
this.router = router;
//base最佳写法:'/base',以斜杠开头,不以斜杠结尾
this.base = normalizeBase(base);
// start with a route object that stands for "nowhere"
this.current = START;
this.pending = null;
this.ready = false;
this.readyCbs = [];
this.readyErrorCbs = [];
this.errorCbs = [];
};
History.prototype上有这些方法
History.prototype={
listen:function(){...},
onReady:function(){...},
onError:function(){...},
transitionTo:function(){...},
confirmTransition:function(){...},
updateRoute:function(){...}
}
//以下7.10新增
还记得上篇中router的初始化时关于history的处理吗,
if (history instanceof HTML5History) {
history.transitionTo(history.getCurrentLocation());
} else if (history instanceof HashHistory) {
var setupHashListener = function () {
history.setupListeners();
};
history.transitionTo(
history.getCurrentLocation(),
setupHashListener,
setupHashListener
);
}
//监听route,一旦route发生改变就赋值给app._route从而触发页面
//更新,达到特定route绘制特定组件的目的
history.listen(function (route) {
this$1.apps.forEach(function (app) {
app._route = route;
});
});
我们以hash模式为主来分析,可以看到执行了history.transitionTo方法,该方法接受了三个参数history.getCurrentLocation(),setupHashListener和setupHashListener。
先看getCurrentLocation方法,返回当前hash值
HashHistory.prototype.getCurrentLocation = function getCurrentLocation () {
return getHash()
};
//getHash函数如下:
function getHash () {
// We can't use window.location.hash here because it's not
// consistent across browsers - Firefox will pre-decode it!
var href = window.location.href;
var index = href.indexOf('#');
return index === -1 ? '' : href.slice(index + 1)
}
再看transitionTo方法
History.prototype.transitionTo = function transitionTo (location, onComplete, onAbort) {
var this$1 = this;
//调用match方法取得匹配到的route
var route = this.router.match(location, this.current);
//调用confirmTransition方法
this.confirmTransition(route, function () {
this$1.updateRoute(route);
onComplete && onComplete(route);
this$1.ensureURL();
// 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); });
}
});
};
看看match方法
VueRouter.prototype.match = function match (raw,current,redirectedFrom) {
return this.matcher.match(raw, current, redirectedFrom)
};
//this.matcher.match如下,该函数经过层层调用最终返回了一个route对象,注意跟路由记录record对象的区别
function match (raw, currentRoute,redirectedFrom) {
var location = normalizeLocation(raw, currentRoute, false, router);
var name = location.name;
if (name) {
var record = nameMap[name];
{
warn(record, ("Route with name '" + name + "' does not exist"));
}
var paramNames = record.regex.keys
.filter(function (key) { return !key.optional; })
.map(function (key) { return key.name; });
if (typeof location.params !== 'object') {
location.params = {};
}
if (currentRoute && typeof currentRoute.params === 'object') {
for (var key in currentRoute.params) {
if (!(key in location.params) && paramNames.indexOf(key) > -1) {
location.params[key] = currentRoute.params[key];
}
}
}
if (record) {
location.path = fillParams(record.path, location.params, ("named route \"" + name + "\""));
return _createRoute(record, location, redirectedFrom)
}
} else if (location.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)
}
未完待续。。