《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) 表示終止路由跳轉
....
}
複製代碼