《stateman》是波神的一個超級輕量的單頁路由,拜讀之後寫寫自己的小總結。
stateman的github地址 github.com/leeluolee/s…
簡單使用
以下文章全部以該Demo作為例子講解。
Html:
<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:
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工作原理。
例項化路由:new StateMan()
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.title
state樹
stateman根據stateName的"."確定父子關係,整個路由的模組最終是上圖右邊的樹狀結構。
構建state樹程式碼分析
StateMan.prototype.state
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);
}
複製程式碼
程式碼做了兩件事:
- stateName的替換
- "~": 代表當前所處的active狀態;
- "^": 代表active狀態的父狀態; 例如:
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
}
})
複製程式碼
- 使用State.prototype.state函式來找到或者建立state
stateFn.apply(this, arguments);
複製程式碼
State.prototype.state
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
state生成url:State.prototype.configUrl
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)\/?$/
}
複製程式碼
啟動路由:StateMan.prototype.start
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
- 監聽history的change事件
- 啟動history
這裡監聽了history的change事件這個動作,是連線stateman和history的橋樑。
history工作流程
history這邊的程式碼邏輯比較清晰,所以不講解太多程式碼,主要講解流程。
主要的工作原理分為了3個路線:
- onhashchange:利用onhashchange事件來檢測路由變化
- onpopstate:這個是html5新API,在我們點選瀏覽器前進後退時觸發,也就是說hash改變的時候並不會出發這個事件,所有點選a標籤的時候需要進行檢測,點選a標籤,阻止預設跳轉,呼叫pushState來增加一條歷史,然後路由觸發跳轉。
- iframe hack:在舊版本IE,IE8以下並不支援以上兩個事件,這裡設定了一個定時器,定時去檢視路徑是不是發生了變化,如果發生了變化,就觸發路由跳轉
生命週期:單頁不同state之間的跳轉
當路由跳轉時,state樹會按照以下順序進行一系列的生命週期:
- 找到兩個state節點的共同父節點
permission階段:
- 從當前state節點往上到共同父節點進行canLeave
- 從共同父節點往下到目標節點進行canEnter
navigation階段:
- 從當前state節點往上到共同父節點進行leave
- 從共同父節點往上到根節點進行update
- 從共同父節點往下到目標節點進行enter
流程分析
在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是支援非同步的。
permission階段返回Promise
在_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函式,從而繼續生命週期的迴圈。
在不支援Promise的環境的非同步
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) 表示終止路由跳轉
....
}
複製程式碼