前端單頁路由《stateman》源碼解析

《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

實例化路由: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.titlepromise

state樹

image

stateman根據stateName的"."肯定父子關係,整個路由的模塊最終是上圖右邊的樹狀結構。瀏覽器

構建state樹代碼分析

image

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);

}
複製代碼

代碼作了兩件事:app

  • 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如下並不支持以上兩個事件,這裏設置了一個定時器,定時去查看路徑是否是發生了變化,若是發生了變化,就觸發路由跳轉

image

生命週期:單頁不一樣state之間的跳轉

當路由跳轉時,state樹會按照如下順序進行一系列的生命週期:

image

  1. 找到兩個state節點的共同父節點

permission階段:

  1. 從當前state節點往上到共同父節點進行canLeave
  2. 從共同父節點往下到目標節點進行canEnter

navigation階段:

  1. 從當前state節點往上到共同父節點進行leave
  2. 從共同父節點往上到根節點進行update
  3. 從共同父節點往下到目標節點進行enter

流程分析

在stateman的start函數中有這麼一句話:

this.history.on("change", _.bind(this._afterPathChange, this));
複製代碼

上面說了,在history模塊路由變化最終會觸發change事件,因此這裏會執行this._afterPatchChange函數

image

核心關鍵在於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) 表示終止路由跳轉
    ....
}   
複製代碼
相關文章
相關標籤/搜索