《stateman》是波神的一个超级轻量的单页路由,拜读以后写写本身的小总结。javascript
stateman的github地址 github.com/leeluolee/s…html
如下文章所有以该Demo做为例子讲解。html5
Html:java
<ul>
<li><a href="#/home">/home"</a></li>
<li><a href="#/contact">/contact"</a></li>
<li><a href="#/contact/list">/contact/list</a></li>
<li><a href="#/contact/2">/contact/2</a></li>
<li><a href="#/contact/2/option">/contact/2/option</a></li>
<li><a href="#/contact/2/message">/contact/2/message</a></li>
</ul>
复制代码
Javascript:git
const StateMan = require('../stateman');
let config = {
enter() {
console.log('enter: ' + this.name);
},
leave() {
console.log('leave: ' + this.name);
},
canLeave() {
console.log('canLeave: ' + this.name);
return true;
},
canEnter() {
console.log('canEnter: ' + this.name);
return true;
},
update() {
console.log('update: ' + this.name);
}
}
function create(o = {}){
o.enter= config.enter;
o.leave = config.leave;
o.canLeave = config.canLeave;
o.canEnter = config.canEnter;
o.update = config.update;
return o;
}
let stateman = new StateMan();
stateman
.state("home", config)
.state("contact", config)
.state("contact.list", config )
.state("contact.detail", create({url: ":id(\\d+)"}))
.state("contact.detail.option", config)
.state("contact.detail.message", config)
.start({});
复制代码
以上代码很简单,首先实例化StateMan,而后经过state函数来建立一个路由状态,同时传入路由的配置,最后经过start来启动,这时路由就开始工做了,如下讲解顺序会按照以上demo的代码执行顺序来说解,一步一步解析stateman工做原理。github
function StateMan(options){
if(this instanceof StateMan === false){ return new StateMan(options)}
options = options || {};
this._states = {};
this._stashCallback = [];
this.strict = options.strict;
this.current = this.active = this;
this.title = options.title;
this.on("end", function(){
var cur = this.current,title;
while( cur ){
title = cur.title;
if(title) break;
cur = cur.parent;
}
document.title = typeof title === "function"? cur.title(): String( title || baseTitle ) ;
})
}
复制代码
这里的end事件会在state跳转完成后触发,这个后面会讲到,当跳转完成后会从当前state节点一层一层往上找到title设置赋给document.titlepromise
stateman根据stateName的"."肯定父子关系,整个路由的模块最终是上图右边的树状结构。浏览器
var State = require('./state.js');
var stateFn = State.prototype.state;
...
state: function(stateName, config){
var active = this.active;
if(typeof stateName === "string" && active){
stateName = stateName.replace("~", active.name)
if(active.parent) stateName = stateName.replace("^", active.parent.name || "");
}
// ^ represent current.parent
// ~ represent current
// only
return stateFn.apply(this, arguments);
}
复制代码
代码作了两件事:app
stateman.state({
"app.user": function() {
stateman.go("~.detail") // will navigate to app.user.detail
},
"app.contact.detail": function() {
stateman.go("^.message") // will navigate to app.contact.message
}
})
复制代码
stateFn.apply(this, arguments);
复制代码
state: function(stateName, config){
if(_.typeOf(stateName) === "object"){
for(var i in stateName){
this.state(i, stateName[i]); //注意,这里的this指向stateman
}
return this;
}
var current, next, nextName, states = this._states, i = 0;
if( typeof stateName === "string" ) stateName = stateName.split(".");
var slen = stateName.length, current = this;
var stack = [];
do{
nextName = stateName[i];
next = states[nextName];
stack.push(nextName);
if(!next){
if(!config) return;
next = states[nextName] = new State();
_.extend(next, {
parent: current,
manager: current.manager || current,
name: stack.join("."),
currentName: nextName
})
current.hasNext = true;
next.configUrl();
}
current = next;
states = next._states;
}while((++i) < slen )
if(config){
next.config(config);
return this;
} else {
return current;
}
}
复制代码
这个函数就是生成state树的核心,每个state能够看做是一个节点,它的子节点由本身的_states来储存。在建立一个节点的时候,这个函数会将stateName以'.'分割,而后经过一个循环来从父节点向下检查,若是发现某一个节点不存在,就建立出来,同时配置它的url异步
configUrl: function(){
var url = "" , base = this, currentUrl;
var _watchedParam = [];
while( base ){
url = (typeof base.url === "string" ? base.url: (base.currentName || "")) + "/" + url;
// means absolute;
if(url.indexOf("^/") === 0) {
url = url.slice(1);
break;
}
base = base.parent;
}
this.pattern = _.cleanPath("/" + url);
var pathAndQuery = this.pattern.split("?");
this.pattern = pathAndQuery[0];
// some Query we need watched
_.extend(this, _.normalize(this.pattern), true);
}
复制代码
代码中以本身(当前state)为起点,向上链接父节点的url,若是url中带有^说明这是个绝对路径,这时候不会向上链接url
if(url.indexOf("^/") === 0) {
url = url.slice(1);
break;
}
复制代码
_.cleanPath(url): 把全部url的形式变成:'/some//some/' -> '/some/some'
_.normalize(path): 解析path
_.normalize('/contact/(detail)/:id/(name)');
=>
{
keys: [0, "id", 1],
matches: "/contact/(0)/(id)/(1)",
regexp: /^\/contact\/(detail)\/([\w-]+)\/(name)\/?$/
}
复制代码
start: function(options){
if( !this.history ) this.history = new Histery(options);
if( !this.history.isStart ){
this.history.on("change", _.bind(this._afterPathChange, this));
this.history.start();
}
return this;
},
复制代码
在启动路由的时候,同时作了3件事:
这里监听了history的change事件这个动做,是链接stateman和history的桥梁。
history这边的代码逻辑比较清晰,因此不讲解太多代码,主要讲解流程。
主要的工做原理分为了3个路线:
当路由跳转时,state树会按照如下顺序进行一系列的生命周期:
permission阶段:
navigation阶段:
在stateman的start函数中有这么一句话:
this.history.on("change", _.bind(this._afterPathChange, this));
复制代码
上面说了,在history模块路由变化最终会触发change事件,因此这里会执行this._afterPatchChange函数
核心关键在于walk-transit-loop之间的循环和回调的执行。
第一次walk函数时为permission阶段,第二次为navigation阶段
每次walk函数执行2次transit函数,因此transit函数共执行4次
2次为从当前节点到共同父节点的遍历(canLeave、leave)
2次为从共同父节点到目标节点的遍历(canEnter、enter)
每次的遍历都是经过loop函数来执行,
节点之间的移动经过moveOn函数来执行
每个函数我就不拿出来细讲了,没错,着必定是一篇假的源码解析。
这里提一下permission阶段的canLeave、canEnter是支持异步的。
在_moveOn里面有这么一段代码:
function done( notRejected ){
if( isDone ) return;
isPending = false;
isDone = true;
callback( notRejected );
}
...
var retValue = applied[method]? applied[method]( option ): true;
...
if( _.isPromise(retValue) ){
return this._wrapPromise(retValue, done);
}
复制代码
另外,_wrapPromise函数为:
_wrapPromise: function( promise, next ){
return promise.then( next, function(){next(false)}) ;
}
复制代码
代码不多,理解起来也容易,就是在moveOn的时候若是canLeave、canEnter函数执行返回值是一个Promise,那么moveOn函数会终止,同时经过done传入这个Promise,在Fulfilled的时候触发,done函数会执行callback,也就是loop函数,从而继续生命周期的循环。
moveOn里面提供了option.sync函数来让咱们手动中止moveOn的循环。
option.async = function(){
isPending = true;
return done;
}
...
if( !isPending ) done( retValue ) //代码的最后是这样的
复制代码
从最后一句来看,咱们若是须要异步的话,举个例子,在canLeave函数中:
canLeave: function(option) {
var done = option.sync(); // return the done function
....
省略你的业务代码,在你业务代码结束后使用:
done(true) 表示继续执行
done(false) 表示终止路由跳转
....
}
复制代码