首页 > 技术文章 > angularjs 面试(二)

YangqinCao 2016-08-18 22:11 原文

1、ng-if  ng-show对比

ng-show、ng-hide、ng-if指令都可以用来控制dom元素的显示或隐藏。

1)实现上:ng-show和ng-hide根据所给表达式的值来显示或隐藏HTML元素。当赋值给ng-show指令的值为false时元素会被隐藏,值为true时元素会显示。ng-hide功能类似,使用方式相反。元素的显示或隐藏是通过改变CSS的display属性值来实现的。

ng-if指令可以根据表达式的值在DOM中生成或移除一个元素。如果赋值给ng-if的表达式的值是false,那对应的元素将会从DOM中移除,否则生成一个新的元素插入DOM中。ng-if同no-show和ng-hide指令最本质的区别是,它不是通过CSS显示或隐藏DOM节点,而是删除或者新增结点。

2)表现:ng-if重新创建元素时用的是它们编译后的状态。如果ng-if内部的代码加载之后被jQuery修改过(例如用.addClass),那么当ng-if的表达式值为false时,这个DOM元素会被移除,表达式再次成为true时这个元素及其内部的子元素会被重新插入DOM,此时这些元素的状态会是它们的原始状态,而不是它们上次被移除时的状态。也就是说无论用jQuery的.addClass添加了什么类都不会存在了。而ng-show和ng-hide则可以保留dom元素上次修改后的状态。
3)作用域:当一个元素被ng-if从DOM中移除,同它关联的作用域也会被销毁。而且当它重新加入DOM中时,会通过原型继承从它的父作用域生成一个新的作用域。也就是说ng-if会新建作用域,而ng-show和ng-hide则不会
4)性能
ng-show和ng-hide通过CSS样式来控制显示和隐藏,而这部分显隐的内容中也会有很多数据绑定,在页面渲染的时候就会影响性能。比如通常angular建议一个页面的数据绑定不超过2000个,假如现在有一个页面直接绑定了2000个model,然后你加载,会发现非常卡.如果你将每100的model设置为ng-show,默认情况下不显示,你会发现还是很卡.

    然后你将所有的ng-show换成ng-if,你会发现性能瞬间快的像两个应用.原因在ng-show还是会执行其中的所有绑定,ng-if则会在等于true,也就是显示的时候再去执行其中的绑定.这样一来性能就有很大的提高。

5)使用:ng-if与ng-model一起使用时,会觉得ng-model失效了,其实是因为ng-model交给子作用域托管了。
<div ng-if='true'>
  <input type='text' ng-model='a'>
</div>
<span>{{a}}</span>

 在输入框输入a也不会改变span中显示的值。如果a没有在父scope中初始化,那么scope就没有办法继承,这样子导致的问题就是父scope修改了a后并没有得到相应的效果。如果在ng-if之前先把参数都初始化就可以避免失去了控制权。

对于ng-if中ng-model的解决办法就是使用对象,或者使用$parent

<div ng-if='true'>
  <input type='text' ng-model='a.id'>
</div>
<span>{{a}}</span>

 

.controller('myCtrl',function($scope){
  $scope.a={};
})

 

span中会同步显示{'id':输入的值}

或者使用$parent

<div ng-if='true'>
  <input type='text' ng-model='$parent.a'>
</div>
<span>{{a}}</span>

这个不需要初始化

 6)ng-if中作用域分析

作用域:每个 Angular 应用默认有一个根作用域 $rootScope, 根作用域位于最顶层,从它往下挂着各级作用域。通常情况下,页面中 ng-model 绑定的变量都是在对应的 Controller 中定义的。如果一个变量未在当前作用域中定义,JavaScript 会通过当前 Controller 的 prototype 向上查找,也就是作用域的继承。

这又分两种情况。

基本类型变量:

<div ng-controller="OuterCtrl">
<p>{{x}}</p>
<div ng-controller="InnerCtrl">
<input type="text" ng-model="x">
</div>
</div>



运行后会发现跟文章开头一样的问题,里面输入框变了,外面的没跟着变。原因在于,InnerCtrl 中并未定义 x 这个变量,取值的时候,会沿着原型链向上找,找到了 OuterCtrl 中定义的 x ,然后赋值给自己,在 InnerCtrl 的输入框输入值时,改变的是 InnerCtrl 中的 x ,而对 OuterCtrl 中的 x 无影响。此时,两个 x 是独立的。

不过,如果你不嫌麻烦的话,用 $scope.$parent 可以绑定并影响上一层作用域中的基本变量:

1
<input type="text" ng-model="$parent.x">

引用类型的变量:在这种情况下,两者的 data 是同一个引用,对这个对象上面的属性修改,是可以反映到两级对象上的。Angular的实现机制其实也就是把这两个控制器中的$scope作了关联,外层的作用域实例成为了内层作用域的原型。也就是inner(内层作用域实例)-->inner.prototype-->outer(外层作用域实例),因此inner,outer引用了同一个对象,对这个对象的修改会影响到inner,outer

并不是只有 Controller 可以创建作用域,ng-if 等指令也会(隐式地)产生新作用域。

总结下来就是,ng-if、 ng-switch 、 ng-include 等会动态创建一块界面的东西,都是自带一级作用域。

因此,在开发过程中,为了避免模板中的变量歧义,应当尽可能使用命名限定,比如 data.x,出现歧义的可能性就比单独的 x 要少得多。

 2、ng-repeat迭代数组的时候,如果数组中有相同值,会有什么问题,如何解决?

会提示 Duplicates in a repeater are not allowed. 加 track by $index可解决。当然,也可以 trace by 任何一个普通的值,只要能唯一性标识数组中的每一项即可(建立 dom 和数据之间的关联)。

ng-repeat当数组中有相同的项时会出现这个问题。

 <p ng-repeat='item in array'>{{item}}</p>
<script>
    $scope.array=['array','2','array'];
</script>

 

因为array中有两个array所以会报错,加上item in array track by $index可以解决 

<p ng-repeat='item in array'>{{item.id}}</p>
<script>
    $scope.array=[{id:1,similar:'array'},{id:1,similar:'array'},{id:2}];
</script>

 

 这里不会报错,item指向的是对象,这两个对象不同

 3、ng-click 中写的表达式,能使用 JS 原生对象上的方法吗?

不止是 ng-click 中的表达式,只要是在页面中,都不能直接调用原生的 JS 方法,因为这些并不存在于与页面对应的 Controller 的 $scope 中。

举个栗子:

<p>{{parseInt(55.66)}}<p>

会发现,什么也没有显示。

但如果在 $scope 中添加了这个函数:

$scope.parseInt = function(x){
    return parseInt(x);
}

这样自然是没什么问题了。

对于这种需求,使用一个 filter 或许是不错的选择:

<p>{{13.14 | parseIntFilter}}</p>

app.filter('parseIntFilter', function(){
    return function(item){
        return parseInt(item);
    }
})

 4、{{now | 'yyyy-MM-dd'}} 这种表达式里面,竖线和后面的参数通过什么方式可以自定义?

filter,格式化数据,接收一个输入,按某规则处理,返回处理结果。

内置 filter

ng 内置的 filter 有九种:

  • date(日期)

  • currency(货币)

  • limitTo(限制数组或字符串长度)

  • orderBy(排序)

  • lowercase(小写)

  • uppercase(大写)

  • number(格式化数字,加上千位分隔符,并接收参数限定小数点位数)

  • filter(处理一个数组,过滤出含有某个子串的元素)

  • json(格式化 json 对象)

filter 有两种使用方法,一种是直接在页面里:

<p>{{now | date : 'yyyy-MM-dd'}}</p>

另一种是在 js 里面用:

// $filter('过滤器名称')(需要过滤的对象, 参数1, 参数2,...)
$filter('date')(now, 'yyyy-MM-dd hh:mm:ss');

自定义 filter

// 形式
app.filter('过滤器名称',function(){
    return function(需要过滤的对象,过滤器参数1,过滤器参数2,...){
        //...做一些事情  
        return 处理后的对象;
    }
});  

// 栗子
app.filter('timesFilter', function(){
    return function(item, times){
        var result = '';
        for(var i = 0; i < times; i++){
            result += item;
        }
        return result;
    }
})

5、factory、service 和 provider 是什么关系?

 查看http://www.oschina.net/translate/angularjs-factory-vs-service-vs-provider

不要往 controller 和 scope 里堆满不必要的逻辑。controller 这一层应该很薄;也就是说,应用里大部分的业务逻辑和持久化数据都应该放在 service 里。关于如何在 controller 里保存持久化数据。这就不是 controller 该干的事。出于内存性能的考虑,controller 只在需要的时候才会初始化,一旦不需要就会被抛弃。因此,每次当你切换或刷新页面的时候,Angular 会清空当前的 controller。与此同时,service 可以用来永久保存应用的数据,并且这些数据可以在不同的 controller 之间使用。

Angular 提供了3种方法来创建并注册我们自己的 service。

  1. Factory

  2. Service

  3. Provider

1) 用 Factory 就是创建一个对象,为它添加属性,然后把这个对象返回出来。你把 service 传进 controller 之后,在 controller 里这个对象里的属性就可以通过 factory 使用了。

FactoryExample1

2) Service 是用"new"关键字实例化的。因此,你应该给"this"添加属性,然后 service 返回"this"。你把 service 传进 controller 之后,在controller里 "this" 上的属性就可以通过 service 来使用了。

ServiceExample2

3) Providers 是唯一一种你可以传进 .config() 函数的 service。当你想要在 service 对象启用之前,先进行模块范围的配置,那就应该用 provider。

可以把Provider想象成由两部分组成。第一部分的变量和函数是可以在app.config函数中访问的,因此你可以在它们被其他地方访问到之前来修改它们。第二部分 的变量和函数是可以在任何传入了’myProvider‘的控制器中进行访问的。当你使用Provider创建一个service时,唯一的可以在你的控制器中访问的属性和方法是通过$get()函数返回内容。

ProviderExample3

 从底层实现上来看,service 调用了 factory,返回其实例;factory 调用了 provider,返回其 $get 中定义的内容。factory 和 service 功能类似,只不过 factory 是普通 function,可以返回任何东西(return 的都可以被访问);service 是构造器,可以不返回(绑定到 this 的都可以被访问);provider 是加强版 factory,返回一个可配置的 factory。

 6、angular 的数据绑定采用什么机制?详述原理

脏检查机制。

我们的浏览器一直在等待事件,比如用户交互。假如你点击一个按钮或者在输入框里输入东西,事件的回调函数就会在javascript解释器里执行,然后你就可以做任何DOM操作,等回调函数执行完毕时,浏览器就会相应地对DOM做出变化。 Angular拓展了这个事件循环,生成一个有时称为angular context的执行环境。

1)$watch队列,每次你绑定一些东西到你的UI上时你就会往$watch队列里插入一条$watch

<ul>
  <li ng-repeat="person in people">
      {{person.name}} - {{person.age}}
  </li>
</ul>

这里又生成了多少个$watch呢?每个person有两个(一个name,一个age),然后ng-repeat又有一个,因此10个person一共是(2 * 10) +1,也就是说有21个$watch。 因此,每一个绑定到了UI上的数据都会生成一个$watch。对,那这写$watch是什么时候生成的呢? 当我们的模版加载完毕时,也就是在linking阶段(Angular分为compile阶段和linking阶段---译者注),Angular解释器会寻找每个directive,然后生成每个需要的$watch

2)$digest

当浏览器接收到可以被angular context处理的事件时,$digest循环就会触发。这个循环是由两个更小的循环组合起来的。一个处理evalAsync队列,另一个处理$watch队列。 这个是处理什么的呢?$digest将会遍历我们的$watch,然后询问:

  • 嘿,$watch,你的值是什么?
    • 是9。
  • 好的,它改变过吗?
    • 没有,先生。
  • (这个变量没变过,那下一个)
  • 你呢,你的值是多少?
    • 报告,是Foo
  • 刚才改变过没?
    • 改变过,刚才是Bar
  • (很好,我们有DOM需要更新了)
  • 继续询问知道$watch队列都检查过。

这就是所谓的dirty-checking。既然所有的$watch都检查完了,那就要问了:有没有$watch更新过?如果有至少一个更新过,这个循环就会再次触发,直到所有的$watch都没有变化。这样就能够保证每个model都已经不会再变化。记住如果循环超过10次的话,它将会抛出一个异常,防止无限循环。 当$digest循环结束时,DOM相应地变化。

 比如

{{ name }}
<button ng-click="changeFoo()">Change the name</button>

这里我们有一个$watch因为ng-click不生成$watch(函数是不会变的)。

  • 我们按下按钮
  • 浏览器接收到一个事件,进入angular context
  • $digest循环开始执行,查询每个$watch是否变化。
  • 由于监视$scope.name$watch报告了变化,它会强制再执行一次$digest循环。
  • 新的$digest循环没有检测到变化。
  • 浏览器拿回控制权,更新与$scope.name新值相应部分的DOM。

3)$apply

谁决定什么事件进入angular context,而哪些又不进入呢?$apply

如果当事件触发时,你调用$apply,它会进入angular context,如果没有调用就不会进入。现在你可能会问:刚才的例子里我也没有调用$apply啊,为什么?Angular为了做了!因此你点击带有ng-click的元素时,时间就会被封装到一个$apply调用。如果你有一个ng-model="foo"的输入框,然后你敲一个f,事件就会这样调用$apply("foo = 'f';")

那么上面的过程就可以更新为:

按下按钮,浏览器接受到一个事件,这个事件被包裹在apply中,就会进入angular context

4)什么时候需要手动调用$apply

比如有一个directive和一个controller

在controller里面初始化一个数字为0

在directive里面创建了一个可点击区域,点击该区域这个数字就会自增1,然后页面还有一个按钮,有一个ng-click事件,点击按钮也会自增1

但是你在点击这个可点击区域时,控制台显示数字自增了,可是页面没显示。而按钮点击的时候控制台和页面都会自增1.

这是因为ng-click会自己触发apply,开始digest循环,检查哪些watch变化了,并更新页面,但是你自己的这个点击区域就不会了,你没有用apply,他只是变化了后台的数字,没有触发digest,不会检查有没有发生变化啊,所以你需要将这个可点击区域的点击事件包裹在一个apply里面,这样就可以实现和按钮一样的功能了。

当然上面的例子也可以直接调用无参的apply,差别就是在第一个版本中,我们是在angular context的外面更新的数据,如果有发生错误,Angular永远不知道。很明显在这个像个小玩具的例子里面不会出什么大错,但是想象一下我们如果有个alert框显示错误给用户,然后我们有个第三方的库进行一个网络调用然后失败了,如果我们不把它封装进$apply里面,Angular永远不会知道失败了,alert框就永远不会弹出来了。

因此,如果你想使用一个jQuery插件,并且要执行$digest循环来更新你的DOM的话,要确保你调用了$apply

5)$watch的使用

app.controller('MainCtrl', function($scope) {
  $scope.name = "Angular";

  $scope.updated = -1;

  $scope.$watch('name', function() {
    $scope.updated++;
  });
});

index.html

<body ng-controller="MainCtrl">
  <input ng-model="name" />
  Name updated: {{updated}} times.
</body>

这就是我们创造一个新的$watch的方法。第一个参数是一个字符串或者函数,在这里是只是一个字符串,就是我们要监视的变量的名字,在这里,$scope.name(注意我们只需要用name)。第二个参数是当$watch说我监视的表达式发生变化后要执行的。我们要知道的第一件事就是当controller执行到这个$watch时,它会立即执行一次,因此我们设置updated为-1

如果想要监视对象

$scope.$watch('user', function(newValue, oldValue) {
    if (newValue === oldValue) { return; }
    $scope.updated++;
  });

index.html

<body ng-controller="MainCtrl">
  <input ng-model="user.name" />
  Name updated: {{updated}} times.
</body>

呃?没用,为啥?因为$watch默认是比较两个对象所引用的是否相同,在例子1和2里面,每次更改$scope.name都会创建一个新的基本变量,因此$watch会执行,因为对这个变量的引用已经改变了。在上面的例子里,我们在监视$scope.user,当我们改变$scope.user.name时,对$scope.user的引用是不会改变的,我们只是每次创建了一个新的$scope.user.name,但是$scope.user永远是一样的。

$scope.$watch('user', function(newValue, oldValue) {
    if (newValue === oldValue) { return; }
    $scope.updated++;
  }, true);

现在有用了吧!因为我们对$watch加入了第三个参数,它是一个bool类型的参数,表示的是我们比较的是对象的值而不是引用。由于当我们更新$scope.user.name$scope.user也会改变,所以能够正确触发。

转自http://www.angularjs.cn/A0a6   http://huangtengfei.com/2015/09/data-bind-of-angularjs/

 7、如何理解angular中的作用域机制

Angular应用是分层的,主要有三个层面:视图(view),模型(model),视图模型(viewModel),即MVVM。其中,视图很好理解,就是直接可见的界面,模型就是数据,那么视图模型是一种把数据包装给视图调用的东西。所谓作用域,也就是视图模型中的一个概念。

 1) rootScope

作用域在一个Angular应用中是以树的形状体现的,根作用域位于最顶层,从它往下挂着各级作用域。每一级作用域上面挂着变量和方法,供所属的视图调用。

如果想要在代码中显式使用根作用域,可以注入$rootScope。

 2) 作用域的继承关系

<div ng-controller="OuterCtrl">
    <span>{{a}}</span>
    <div ng-controller="InnerCtrl">
        <span>{{a}}</span>
    </div>
</div>
<script>
  function OuterCtrl($scope) {
      $scope.a = 1;
  }

  function InnerCtrl($scope) {
  }
</script>

 

 我们可以看到界面显示了两个1,而我们只在OuterCtrl的作用域里定义了a变量,但界面给我们的结果是,两个a都有值。这里内层的a值显然来自外层.在Angular中,如果两个控制器所对应的视图存在上下级关系,它们的作用域就自动产生继承关系。用javascript来分析就是:

function Outer() {
    this.a = 1;
}

function Inner() {
}

var outer = new Outer();
Inner.prototype = outer;
var inner = new Inner();

Angular的实现机制其实也就是把这两个控制器中的$scope作了关联,外层的作用域实例成为了内层作用域的原型。以此类推,整个Angular应用的作用域,都存在自顶向下的继承关系,最顶层的是$rootScope,然后一级一级,沿着不同的控制器往下,形成了一棵作用域的树。

看一个例子:

<div ng-controller="OuterCtrl">
    <span>{{a}}</span>
    <div ng-controller="InnerCtrl">
        <span>{{a}}</span>
        <button ng-click="a=a+1">a++</button>
    </div>
</div>
<script>
  function OuterCtrl($scope) {
      $scope.a = 1;
  }

  function InnerCtrl($scope) {}
</script>

 

 点了按钮之后,两个a不一致了,里面的变了,外面的没变,这是为什么?原先两层不是共用一个a吗,怎么会出现两个不同的值?看这句就能明白了,相当于我们之前那个例子里,这样赋值了:

function Outer() {
    this.a = 1;
}

function Inner() {
}

var outer = new Outer();
Inner.prototype = outer;
var inner = new Inner();

inner.a = inner.a + 1;

 

 它有两个过程,取值的时候,因为inner自身上面没有,所以沿着原型往上取到了1,然后自增了之后,赋值给自己,这个赋值的时候就不同了,有a就赋值,没有a,创造一个a也要赋值。所以这么一来,inner上面就被赋值了一个新的a,outer里面的仍然保持原样,这也就导致了刚才看到的结果。

3)对象在上下级作用域之间的共享

我们就是想上下级共享变量,不创建新的,该怎么办

function Outer() {
    this.data = {
        a: 1
    };
}

function Inner() {
}

var outer = new Outer();
Inner.prototype = outer;

var inner = new Inner();

console.log(outer.data.a);
console.log(inner.data.a);

// 注意,这个时候会怎样?
inner.data.a += 1;

console.log(outer.data.a);
console.log(inner.data.a);

 

现在inner-->Inner.prototype-->outer-->Outer.prototype

所以inner和outer两者的data是同一个引用,对这个对象上面的属性修改,是可以反映到两级对象上的。所以在angular中可以这样使用

<div ng-controller="OuterCtrl">
    <span>{{data.a}}</span>
    <div ng-controller="InnerCtrl">
        <span>{{data.a}}</span>
        <button ng-click="data.a=data.a+1">increase a</button>
    </div>
</div>
<script>
  function OuterCtrl($scope) {
      $scope.data = {
          a: 1
      };
  }
  function InnerCtrl($scope) {}
</script>

 

从这个例子我们就发现了,如果想要避免变量歧义,显式指定所要使用的变量会是比较好的方式,那么如果我们确实就是要在上下级分别存在相同的变量该怎么办呢,比如说下级的点击,想要给上级的a增加1,我们可以使用$parent来指定上级作用域。

 

<div ng-controller="OuterCtrl">
    <span>{{a}}</span>
    <div ng-controller="InnerCtrl">
        <span>{{a}}</span>
        <button ng-click="$parent.a=a+1">increase a</button>
    </div>
</div>
<script>
  function OuterCtrl($scope) {
      $scope.a = 1;
  }
  function InnerCtrl($scope) {}
</script>

 

 4)控制器实例别名

在最近版本的AngularJS中,已经可以不显式注入$scope了,语法是这样:

<div ng-controller="CtrlB as instanceB">
    <div>{{instanceB.a}}</div>
    <button ng-click="instanceB.foo()">click me</button>
</div>
function CtrlB() {
    this.a = 1;
    this.foo = function() {
    };
}
app.controller('CtrlB',CtrlB);

as语法,给CtrlB的实例取了一个别名叫做instanceB,这样,它下属的各级视图都可以显式使用这个名称来调用其属性和方法,不易引起歧义。

在开发过程中,为了避免模板中的变量歧义,应当尽可能使用命名限定,比如a.b,出现歧义的可能性就比单独的b要少得多。

5)手动创建作用域

var newScope = scope.$new();

6) 作用域上的事件

我们刚才提到使用$parent来处理上下级的通讯,但其实这不是一种好的方式,尤其是在不同控制器之间,这会增加它们的耦合,对组件复用很不利。那怎样才能更好地解耦呢?我们可以使用事件。

比如A1要传播一个事件给B1

  • 沿着父作用域一路往上到达双方共同的祖先作用域,
    $scope.$emit("someEvent", {});
  • 从祖先作用域一级一级往下进行广播,直到到达需要的地方
    $scope.$broadcast("someEvent", {});

事件的传递

 使用事件的主要作用是消除模块间的耦合,发送方是不需要知道接收方的状况的,接收方也不需要知道发送方的状况,双方只需要传送必要的业务数据即可。

 7)事件总线

我们能不能这样:搞一个专门负责通讯的机构,大家的消息都发给它,然后由它发给相关人员,其他人员在理念上都是平级关系。

这就是一个很典型的订阅发布模式,接收方在这里订阅消息,发布方在这里发布消息。这个过程可以用这样的图形来表示:

应用内的事件总线

代码写起来也很简单,把它做成一个公共模块,就可以被各种业务方调用了:

 

app.factory("EventBus", function() {
    var eventMap = {};

    var EventBus = {
        on : function(eventType, handler) {
            //multiple event listener
            if (!eventMap[eventType]) {
                eventMap[eventType] = [];  // 如果没有该类型的事件,创造这个属性,并初始化为一个空数组
            }
            eventMap[eventType].push(handler);
        },

        off : function(eventType, handler) {
            for (var i = 0; i < eventMap[eventType].length; i++) {
                if (eventMap[eventType][i] === handler) {
                    eventMap[eventType].splice(i, 1);   // 解除事件,将该类型事件中的处理函数删除
                    break;
                }
            }
        },

        fire : function(event) {
            var eventType = event.type;
            if (eventMap && eventMap[eventType]) {
                for (var i = 0; i < eventMap[eventType].length; i++) {
                    eventMap[eventType][i](event);   // 将eventMap对象中该类型事件的数组中的每一个事件都触发,并传入参数
                }
            }
        }
    };
    return EventBus;
});

 

事件订阅

EventBus.on("someEvent", function(event) {
    // 这里处理事件
    var c = event.data.a + event.data.b;
});

 

事件发布

EventBus.fire({
    type: "someEvent",
    data: {
        aaa: 1,
        bbb: 2
    }
});

 举个例子,现在有个A页面要向B,C页面传递事件

在A页面中:

EventBus.fire(dataA);
dataA={
    type: "click",
    data: {
        a: 1,
        b: 2
    }
}

 

在B页面中

EventBus.on("click",handlerB);
 function handlerB(event) {
    // 这里处理事件
    var c = event.data.a + event.data.b;
}

 

在C页面中

EventBus.on("click", handlerC);
function handlerC(event) {
    // 这里处理事件
   alert(event.data.a);
}

因为factory是个单例对象,那么现在eventMap应该是这样的

var eventMap = {
     click:[handlerB,handlerC]
};

 那么A页面调用的fire实际上就是:handlerB(dataA),handlerC(dataA)

转自https://github.com/xufei/blog/issues/18 

 

 

 

 

 

 

 

 

 

 

推荐阅读