博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
前端单页路由《stateman》源码解析
阅读量:5789 次
发布时间:2019-06-18

本文共 7296 字,大约阅读时间需要 24 分钟。

《stateman》是波神的一个超级轻量的单页路由,拜读之后写写自己的小总结。

stateman的github地址

简单使用

以下文章全部以该Demo作为例子讲解。

Html:

复制代码

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树会按照以下顺序进行一系列的生命周期:

  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函数

核心关键在于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) 表示终止路由跳转    ....}   复制代码

转载地址:http://dhmyx.baihongyu.com/

你可能感兴趣的文章
vim
查看>>
MVVM计算器(下)
查看>>
C++中指针和引用的区别
查看>>
簡單分稀 iptables 記錄 udp 微軟 138 端口
查看>>
Java重写equals方法和hashCode方法
查看>>
Spark API编程动手实战-07-join操作深入实战
查看>>
H3C-路由策略
查看>>
centos 修改字符界面分辨率
查看>>
LNMP之Mysql主从复制(四)
查看>>
阅读Spring源代码(1)
查看>>
nagios一键安装脚本,nagios监控被监控主机上的应用服务mysql数据库
查看>>
grep 命令
查看>>
JS二维数组的声明和使用
查看>>
v$archive_gap dg dataguard 断档处理 scn恢复
查看>>
问责IT风险管理:CIO需关注两个重点
查看>>
Winform打包发布图解
查看>>
PDF文件怎么编辑,超简单的方法
查看>>
EasyUI基础入门之Easyloader(载入器)
查看>>
Uva 839 Not so Mobile
查看>>
30款超酷的HTTP 404页面未找到错误设计
查看>>