ui.router路由实现原理

前端构建SPA必不可少应该就是Router了,不同的MV*对路由的实现各有不同,对于angular这种大而全的框架,它自然也有内置的路由模块ngRoute。

不过,ngRoute功能有限,甚至不能实现路由嵌套。
相比较而言ui-router可以很好的完成一个页面可以嵌套多个视图、多个视图去控制某一个视图的工作。

router的基本原理

我们应该都知道在访问锚点的时候,页面并不会进行刷新,比如在下面的结构中:

<a href="#view">go</a>
<pre id="view">

点击了a标签之后,会找对对应锚点的位置,并且在地址栏中的地址后加上了#view

所以可以通过对#之后部分的解析来获取数据,以此来实现页面不刷新而重新渲染部分内容的效果。

那么如何获取#view呢?

https://www.abc.com:8080/test/index.html?id=10#name为例,我们通过控制台打印出window.location,可以得出其地址结构分为:

protocol: https
host: www.baidu.com:8080
hostname: www.baidu.com
port: 8080
pathname: /aaa/1.html
search: ?id=10
hash: #name

接下来可以通过window.location.hash来拿到对应的hash。hash的值改变会触发hashchange事件,所以我们可以有如下实现:

window.addEventListener('hashchange', function(e) {
        var hash = window.location.hash;
        switch (hash) {
          case '#/index/':
            console.log('index');
            break;
          case '#/a/':
            console.log('a');
            break;
          case '#/b/':
            console.log('b');
            break;
        }

ui-router实现原理

首先,看一个简单的例子:

$stateProvider
    .state('home', {
        url: '/abc',
        template: 'hello world'
    });

上面,我们通过调用$stateProvider.state()方法,创建了一个简单路由规则,通过参数,可以容易理解到:

规则名:’home’
匹配的url:’/abc’
对应的模板:’hello world’
意思就是说:当我们访问http://xxxx#/abc的时候,这个路由规则被匹配到,对应的模板会被填到某个div[ui-view]中。

那么$stateProvider.state()方法,它做了些什么工作?

首先,创建并存储一个state对象,里面包含着该路由规则的所有配置信息。
然后,调用$urlRouterProvider.when()方法,进行路由的注册(之前是路由的创建),源码里面是这样写的:

$urlRouterProvider.when(state.url, ['$match', '$stateParams', function ($match, $stateParams) {
  // 判断是否是同一个state || 当前匹配参数是否相同
  if ($state.$current.navigable != state || !equalForKeys($match, $stateParams)) {
    $state.transitionTo(state, $match, { inherit: true, location: false });
  }
}]);

上述代码的意思是:当hash值与state.url相匹配时,就执行后面那段回调,回调函数里面进行了两个条件判断之后,决定是否需要跳转到该state.

这里就插入了一个话题:为什么说 “跳转到该state,而不是该url”?

其实这个问题跟大家一直说的:“ui.router是基于state(状态)的,而不是url”是同一个问题。

我的理解是这样的:之前就说过,路由存在着明确的父子关系,每一个路由可以理解为一个state,

当程序匹配到某一个子路由时,我们就认为这个子路由state被激活,同时,它对应的父路由state也将被激活。
我们还可以手动的激活某一个state,就像上面写的那样,$state.transitionTo(state, ...);,这样的话,它的父state会被激活(如果还没有激活的话),它的子state会被销毁(如果已经激活的话)。
ok,回到之前的路由注册,调用了$urlRouterProvider.when()方法,它做了什么呢?

它创建了一个rule,并存储在rules集合里面,之后的,每次hash值变化,路由重新查找匹配都是通过遍历这个rules集合进行的。

ui-router查找匹配

angular 在刚开始的$digest时,$rootScope会触发$locationChangeSuccess事件(angular在每次浏览器hashchange的时候也会触发$locationChangeSuccess事件);
ui.router 监听了$locationChangeSuccess事件,于是开始通过遍历一系列rules,进行路由查找匹配。
当匹配到路由后,就通过$state.transitionTo(state,...),跳转激活对应的state。
最后,完成数据请求和模板的渲染。
可以从下面这段源代码看到,看到查找匹配的起始和过程:

function update(evt) {
  // ...省略
  function check(rule) {
    var handled = rule($injector, $location);
    // handled可以是返回:
    // 1. 新的的url,用于重定向
    // 2. false,不匹配
    // 3. true,匹配
    if (!handled) return false;

    if (isString(handled)) $location.replace().url(handled);
    return true;
  }

  var n = rules.length, i;

  // 渲染遍历rules,匹配到路由,就停止循环
  for (i = 0; i < n; i++) {
    if (check(rules[i])) return;
  }
  // 如果都匹配不到路由,使用otherwise路由(如果设置了的话)
  if (otherwise) check(otherwise);
}

function listen() {
  // 监听$locationChangeSuccess,开始路由的查找匹配
  listener = listener || $rootScope.$on('$locationChangeSuccess', update);
  return listener;
}

if (!interceptDeferred) listen();

每次路由变化(hash变化),由于监听了$locationChangeSuccess事件,都要进行rules的遍历来查找匹配路由,然后跳转到对应的state。

我们之所以要循环遍历rules,是因为要查找匹配到对应的路由(state),然后跳转过去。但是如果不循环,也能直接找到对应的state,如何实现呢?

在用ui.router在创建路由时,会实例化一个对应的state对象,并存储起来(states集合里面)
每一个state对象都有一个state.name进行唯一标识(如:’home’)
根据以上两点,于是ui.router提供了另一个指令叫做:ui-sref指令,来解决这个问题,比如这样:

<a ui-sref="home">通过ui-sref跳转到home state</a>

当点击这个a标签时,会直接跳转到home state,而并不需要循环遍历rules,ui.router是这样做到的(这里简单说一下):

首先,ui-sref="home"指令会给对应的dom添加click事件,然后根据state.name,直接跳转到对应的state,代码像这样:

element.bind("click", function(e) {
    // ..省略若干代码
    var transition = $timeout(function() {
      // 手动跳转到指定的state
      $state.go(ref.state, params, options);
    });
});

跳转到对应的state之后,ui.router会做一个善后处理,就是改变hash,所以理所当然,会触发$locationChangeSuccess事件,然后执行回调,但是在回调中可以通过一个判断代码规避循环rules,像这样:

function update(evt) {
  var ignoreUpdate = lastPushedUrl && $location.url() === lastPushedUrl;

  // 手动调用$state.go(...)时,直接return避免下面的循环
  if (ignoreUpdate) return true;

  // 省略下面的循环ruls代码
}

说了那么多,其实就是想说,我们不建议直接使用href=”#/xxx”来改变hash,然后跳转到对应state(虽然也是可以的),因为这样做会多了一步rules循环遍历,浪费性能,就像下面这样:

<a href="#/abc">通过href跳转到home state</a>


参考:
ui.router源码解析

我是王浩然,15年毕业于合肥工业大学,现就职于趣分期。</br>乐于分享,喜欢折腾。