创建自己的AngularJS - 作用域和Digest(五)

作用域

第一章 作用域和Digest(五)

销毁监控

当你注册一个监控,很多时候你想让它和scope一样保持活跃的状态,所以不必显示的删除他。然而,有些情况下,你需要销毁一个特定的监控,但是仍然保持作用域可操作。意思就是,我们需要给监控增加一个删除操作。

Angular实现这个的方式特别聪明:Angular中的$watch函数有一个返回值。他是一个函数,但其被调用的时候,即删除了其注册的监控。如果想要能够移除一个监控,只要存储注册监控函数时返回的函数,然后当不需要监控的时候调用它即可:

test/scope_spec.js

it("allows destroying a $watch with a removal function", function(){
    scope.aValue = ‘abc‘;
    scope.counter = 0;

    var destroyWatch = scope.$watch(
        function(scope) { return scope.aValue; },
        function(newValue, oldValue, scope){
            scope.counter ++;
        }
    );

    scope.$digest();
    expect(scope.counter).toBe(1);

    scope.aValue = ‘def‘;
    scope.$digest();
    expect(scope.counter).toBe(2);

    scope.aValue = ‘ghi‘;
    destroyWatch();
    scope.$digest();
    expect(scope.counter).toBe(2);

});

为了实现该功能,我们需要在$watch中返回一个函数,并将其在$$watchers数组中删除:

src/scope.js

Scope.prototype.$watch = function(watchFn, listenerFn, valueEq){
    var self = this;
    var watcher = {
        watchFn: watchFn,
        listenerFn: listenerFn || function(){},
        valueEq: !!valueEq,
        last: initWatchVal
    };
    this.$$watchers.push(watcher);
	this.$$lastDirtyWatch = null;
    return function(){
        var index = self.$$watchers.indexOf(watcher);
		if(index >= 0){
			self.$$watchers.splice(index, 1);
        }
    }
};

在监听函数能够删除他本身的之前,我们还有一些测试案例需要去处理,来保证我们有一个健壮的实现。他们在digest循环中删除监控。

首先,一个监控可能在他自己的监控函数或者监听函数中删除他自己。这不应该影响到其他监控:

test/scope_spec.js

it("allows destroying a $watch during digest", function(){
    scope.aValue = ‘abc‘;

    var watchCalls = [];

    scope.$watch(
        function(scope) {
            watchCalls.push("first");
            return scope.aValue;
        }
    );

    var destroyWatch = scope.$watch(
        function(scope) {
            watchCalls.push(‘second‘);
            destroyWatch();
        }
    );

    scope.$watch(
        function(scope) {
            watchCalls.push(‘third‘);
            return scope.aValue;
        }
    );

    scope.$digest();
    expect(watchCalls).toEqual([‘first‘, ‘second‘, ‘third‘, ‘first‘, ‘third‘]);
});

在这个测试中我们有三个监控。中间的监控当它第一次被调用的时候他删除了他自己,只留下了第一个和第三个。我们验证监控以正确的顺序被遍历:在第一次遍历时,每个监控被执行一次。然后,因为digest是脏的,每个监控再次被执行,但是现在第二个监控已经不存在了。

然而,事实上,当第二个监控移除掉他自己时,监控集合中第二个右边的内容左移一位,导致$$digestOnce在本次循环中跳过了第三个监控。

译者注:对于_.forEach函数,在第二个被移除后,第三个监控的数组下标变为了1,然后进入循环进入第三次,即访问$$watchers[2],该内容已不存在,为undefined,从而导致了原来数组的第三个监控,即现在的第二个,没有被遍历。

解决这个问题的窍门就是要反转$$watches数组,所有新的监控被加到数组的前面,然后从后面向前遍历数组。当一个监控被移除掉,被移动的部分已经在digest遍历中被处理了,它不会影响剩余的部分。

当我们添加一个监控时,我们要使用Array.unshift来代替Array.push

src/scope.js

Scope.prototype.$watch = function(watchFn, listenerFn, valueEq){
    var self = this;
    var watcher = {
        watchFn: watchFn,
        listenerFn: listenerFn || function(){},
        valueEq: !!valueEq,
        last: initWatchVal
    };
    this.$$watchers.unshift(watcher);
	this.$$lastDirtyWatch = null;
    return function(){
        var index = self.$$watchers.indexOf(watcher);
		if(index >= 0){
			self.$$watchers.splice(index, 1);
        }
    };
};

然后,当遍历监控时,我们应该使用_.forEachRight来代替_.forEach,用来反转数组顺序:

src/scope.js

Scope.prototype.$$digestOnce = function(){
	var self = this;
	var newValue, oldValue, dirty;
	_.forEachRight(this.$$watchers, function(watcher){
		try{
			newValue = watcher.watchFn(self);
			oldValue = watcher.last;
			if(!(self.$$areEqual(newValue, oldValue, watcher.valueEq))){

                self.$$lastDirtyWatch = watcher;

                watcher.last = watcher.valueEq ? _.cloneDeep(newValue) : newValue;
                watcher.listenerFn(newValue,
                    (oldValue === initWatchVal ? newValue: oldValue),
                    self);
                dirty = true;
            }else if(self.$$lastDirtyWatch === watcher){
                return false;
            }
    } catch (e){
        console.error(e);
    }
    });
    return dirty;
};

下一个案例是移除另一个监控。观察下面的测试案例:

test/scope_spec.js

it("allows a $watch to destroy another during digest", function() {
    scope.aValue = ‘abc‘;
    scope.counter = 0;

    scope.$watch(
        function(scope) {
            return scope.aValue;
        },
        function(newValue, oldValue, scope) {
            destroyWatch();
        }
    );

    var destroyWatch = scope.$watch(
        function(scope) {},
        function(newValue, oldValue, scope) {}
    );

    scope.$watch(
        function(scope) { return scope.aValue; },
        function(newValue, oldValue, scope){
            scope.counter ++;
        }
    );

    scope.$digest();
    expect(scope.counter).toBe(1);
});

测试失败了。罪魁祸首是我们的短路优化。回忆一下,在$$digestOnce中,如果当前的监控是上一次遍历中那个脏的,并且现在干净了,我们会结束掉digest。在这个测试案例中发生的步骤如下:

  1. 第一个监控被执行了。它现在是脏的,它被存储在$$lastDirtyWatch中,并且它的监听被执行了。该监听销毁了第二个监控。
  2. 第一个监控再次被执行,因为在监控数组中它被向前移动了一个位置。这次他是干净的,并且因为他还和$$lastDirtyWatch相等,digest结束了。我们永远没有进入第三个监控。

在有监控被移除的情况下,我们应该取消短路优化,让这种情况不会发生。

src/scope.js

Scope.prototype.$watch = function(watchFn, listenerFn, valueEq){
    var self = this;
    var watcher = {
        watchFn: watchFn,
        listenerFn: listenerFn || function(){},
        valueEq: !!valueEq,
        last: initWatchVal
    };
    this.$$watchers.unshift(watcher);
	this.$$lastDirtyWatch = null;
    return function(){
        var index = self.$$watchers.indexOf(watcher);
		if(index >= 0){
			self.$$watchers.splice(index, 1);
			self.$$lastDirtyWatch = null;
        }
    };
};

最后一个测试考虑当在一个监控中同时移除多个监控的情况:

test/scope_spec.js

it("allows destroying several $watches during digest", function(){
    scope.aValue = ‘abc‘;
    scope.counter = 0;

    var destroyWatch1 = scope.$watch(
        function(scope){
            destroyWatch1();
            destroyWatch2();
        });

    var destroyWatch2 = scope.$watch(
        function(scope){ return scope.aValue; },
        function(newValue, oldValue, scope){
            scope.counter++;
        }
    );

    scope.$digest();
    expect(scope.counter).toBe(0);
});

第一个监控不仅销毁了他本身,而且销毁了即将执行的第二个监控。既然我们不希望第二个监控执行,我们也不希望他抛出异常,也就是他现在正在发生的情况。

我们能做的就是在迭代时检查当前的监控是否存在:

src/scope.js

Scope.prototype.$$digestOnce = function(){
	var self = this;
	var newValue, oldValue, dirty;
	_.forEachRight(this.$$watchers, function(watcher){
		try{
			if(watcher){
				newValue = watcher.watchFn(self);
				oldValue = watcher.last;
				if(!(self.$$areEqual(newValue, oldValue, watcher.valueEq))){

                    self.$$lastDirtyWatch = watcher;

                    watcher.last = watcher.valueEq ? _.cloneDeep(newValue) : newValue;
                    watcher.listenerFn(newValue,
                        (oldValue === initWatchVal ? newValue: oldValue),
                        self);
                    dirty = true;
                }else if(self.$$lastDirtyWatch === watcher){
                    return false;
                }
            }
    } catch (e){
        console.error(e);
    }
    });
    return dirty;
};

最终我们可以放心digest将会忽略被移除掉的监控而继续运行。

在一个监听中监控多个变化:$watchGroup

到目前为止我们的监控和监听只是简单的因果对:当监控发生变化,启动监听。这很不常用,然而,更常用的是监控多个状态,当他们中任一个改变时,执行同样的代码。

因为Angular监控只是普通的Javascript函数,在我们当前已经实现的监控函数下这完全可以实施:创建一个监控函数,运行多个检查,返回他们的集合的值,并且触发监听函数。

但是从Angular1.3起你不用自己手动创建这样的函数。而是可以使用Scope中已经构建好的一个特性:$watchGroup

$watchGroup函数将多个监控函数包装成一个对象,和一个监听函数。思想是:当任何数组中的一个监控函数检测到了变化,监听函数就会被调用。监听函数会接收新值和旧值包装成的数组作为参数,其顺序和原始的监听函数一样。

下面有一个测试案例,被封装在了新的describe块中:

test/scope_spec.js

describe(‘$watchGroup‘, function(){
    var scope;
    beforeEach(function(){
        scope = new Scope();
    });

    it(‘takes watches as an array and calls listener with arrays‘, function(){
        var gotNewvalues, gotOldValues;

        scope.aValue = 1;
        scope.anotherValue = 2;

        scope.$watchGroup([
            function(scope) { return scope.aValue;},
            function(scope) { return scope.anotherValue; }
        ], function(newValue, oldValue, scope){
            gotNewvalues = newValue;
            gotOldValues = oldValue;
        });
        scope.$digest();

        expect(gotNewvalues).toEqual([1, 2]);
        expect(gotOldValues).toEqual([1, 2]);

    });
});

在测试中,我们抓取了监听函数中接收的newValuesoldValues参数,检查他们的值是否是监听函数返回值组成的数组。

让我们首先尝试实现$watchGroup。我们尝试每个监控单独注册,并对其使用同一个监听函数:

src/scope.js

Scope.prototype.$watchGroup = function(watchFns, listenerFn){
    var self = this;
    _.forEach(watchFns, function(watchFn){
        self.$watch(watchFn, listenerFn);
    });
};

但是这不是很贴切。我们希望监听函数接受所有监控值组成的数组,但是现在仅仅是分别得到了分别调用的监控值。

对于每个监控,我们还需要为其定义一个单独的内部的监听函数,并且在这些内部监听的里面收集了所有的监控值组成的数组。然后我们将这些数组传递给原始的监听函数。我们使用一个数组来存储新值,一个存储旧值:

src/scope.js

Scope.prototype.$watchGroup = function(watchFns, listenerFn){
    var self = this;
    var newValues = new Array(watchFns.length);
    var oldValues = new Array(watchFns.length);
    _.forEach(watchFns, function(watchFn, i){
        self.$watch(watchFn, function(newValue, oldValue){
            newValues[i] = newValue;
            oldValues[i] = oldValue;
            listenerFn(newValues, oldValues, self);
        })
    });
};

$watchGroup永远使用引用来检测值的变化。

我们现在的实现有一个问题,他调用监听有点太早了:如果监控数组中有几次变化,监听函数就会被调用几次,我们更希望之调用他一次。更差的是,因为一旦发现了变化,我们会立即调用监听,很有可能在新值和旧值的数组中会将以前的值和现在值的混淆,从而让用户看到不一致的值的组合。

即使有多个变化,监听函数页应该之调用一次,让我们测试一下:

test/scope_spec.js

it("only calls listener once per digest", function(){
    var counter = 0;

    scope.aValue = 1;
    scope.anotherValue = 2;

    scope.$watchGroup([
        function(scope) { return scope.aValue;},
        function(scope) { return scope.anotherValue;}
    ], function(newValues, oldValues, scope){
        counter++;
    });

    scope.$digest();
    expect(counter).toBe(1);
});

我们怎样推迟监听函数的调用知道所有的监听都被检查了呢?既然在$watchGroup中,我们不负责调用digest,没有明显的地方让我们去掉用监听函数。但是,我们可以使用在之前实现的$evalAsync函数。它的目的就是推迟某些工作的执行,但是仍在同一个digest中 - 这正是我们所需要的!

我们在$watchGroup中创建一个内部函数,名叫watchGroupListener。这个函数负责用两个数组来调用原来的监听函数。然后,在这个函数没有被调度的情况下,在每个单独的监听中我们调度这个函数:

src/scope.js

Scope.prototype.$watchGroup = function(watchFns, listenerFn){
    var self = this;
    var newValues = new Array(watchFns.length);
    var oldValues = new Array(watchFns.length);
    var changeReactionScheduled = false;

    function watchGroupListener(){
        listenerFn(newValues, oldValues, self);
        changeReactionScheduled = false;
    }

    _.forEach(watchFns, function(watchFn, i){
        self.$watch(watchFn, function(newValue, oldValue){
            newValues[i] = newValue;
            oldValues[i] = oldValue;
            if(!changeReactionScheduled){
                changeReactionScheduled = true;
                self.$evalAsync(watchGroupListener);
            }
        })
    });
};

这处理了基本的$watchGroup的行为,现在我们将注意力放在几个特殊的情况上。

有一个问题是和第一次调用监控函数有关的,新值和旧值应该是一样的。现在,我们的$watchGroup就是这样做的,因为他是基于$watch函数实现的这个行为。在第一次调用的情况下,新值和旧值数组一定一样。

然而,尽管这两个数组是一样的,但是他们仍然是两个独立的数组对象。这打破了使用同一个严格相等的值两次的约定。这同时意味着如果一个用户想要去比较这两个值,他们不能使用引用相等(===),而是需要迭代数组的内容,查看是否匹配。

我们想要做的更好,在第一次调用的时候,让新值和旧值严格相等:

test/scope_spec.js

it(‘uses the same array of old and new values on first run‘, function(){
    var gotNewvalues, gotOldValues;

    scope.aValue = 1;
    scope.anotherValue = 2;

    scope.$watchGroup([
        function(scope) { return scope.aValue; },
        function(scope) { return scope.anotherValue}
    ], function(newValues, oldValues, scope) {
        gotNewvalues = newValues;
        gotOldValues = oldValues;
    });

    scope.$digest();
    expect(gotNewvalues).toBe(gotOldValues);
});

在这样做的情况下,让我们同时确认我们不会打破我们现有的,通过添加一个测试来确保我们在监听函数中仍然得到的是不同的数组:

test/scope_spec.js

it("uses different arrasy for old and new values on subsequent runs", function(){
    var gotNewValues, gotOldValues;

    scope.aValue = 1;
    scope.anotherValue = 2;

    scope.$watchGroup([
        function(scope) { return scope.aValue; },
        function(scope) { return scope.anotherValue; }
    ], function(newValues, oldValues, scope){
        gotNewvalues = newValues;
        gotOldValues = oldValues;
    });

    scope.$digest();

    scope.anotherValue = 3;
    scope.$digest();

    expect(gotNewvalues).toEqual([1, 3]);
    expect(gotOldValues).toEqual([1, 2]);
});

我们可以通过检查监听函数是否是第一次被调用来实现这个需求。如果是第一次被调用,我们仅仅是将newValues数组传递给监听函数两次:

src/scope.js

Scope.prototype.$watchGroup = function(watchFns, listenerFn){
    var self = this;
    var newValues = new Array(watchFns.length);
    var oldValues = new Array(watchFns.length);
    var changeReactionScheduled = false;
    var firstRun = true;

    function watchGroupListener(){
        if (firstRun) {
            firstRun = false;
            listenerFn(newValues, newValues, self);
        }else{
            listenerFn(newValues, oldValues, self);
        }
        changeReactionScheduled = false;
    }

    _.forEach(watchFns, function(watchFn, i){
        self.$watch(watchFn, function(newValue, oldValue){
            newValues[i] = newValue;
            oldValues[i] = oldValue;
            if(!changeReactionScheduled){
                changeReactionScheduled = true;
                self.$evalAsync(watchGroupListener);
            }
        });
    });
};

我们需要$watchGroups的最后一个特性是:注销登记。我们应该可以向注销单个监控一样注销监控数组。通过使用$watchGroups返回的注销函数。

test/scope_spec.js

it(‘can be deregistered‘, function() {
    var counter = 0;

    scope.aValue = 1;
    scope.anotherValue = 2;

    var destroyGroup = scope.$watchGroup([
        function(scope) { return scope.aValue; },
        function(scope) { return scope.anotherValue; }
    ], function(newValues, oldValues, scope){
        counter ++;
    });
    scope.$digest();

    scope.anotherValue = 3;
    destroyWatch();
    scope.$digest();
    expect(counter).toBe(1);

});

这里我们测试了一旦调用了注销函数,即使监控值发生变化,也不会引起监听函数被触发。

既然单个的监听已经返回了注销函数,我们要做的是收集他们,然后创建一个注销函数来调用所有单个注销函数:

src/scope.js

Scope.prototype.$watchGroup = function(watchFns, listenerFn){
    var self = this;
    var newValues = new Array(watchFns.length);
    var oldValues = new Array(watchFns.length);
    var changeReactionScheduled = false;
    var firstRun = true;

    function watchGroupListener(){
        if (firstRun) {
            firstRun = false;
            listenerFn(newValues, newValues, self);
        }else{
            listenerFn(newValues, oldValues, self);
        }
        changeReactionScheduled = false;
    }

    var destroyFunctions = _.map(watchFns, function(watchFn, i){
        return self.$watch(watchFn, function(newValue, oldValue){
            newValues[i] = newValue;
            oldValues[i] = oldValue;
            if(!changeReactionScheduled){
                changeReactionScheduled = true;
                self.$evalAsync(watchGroupListener);
            }
        });
    });

    return function(){
        _.forEach(destroyFunctions, function(destroyFunction) {
            destroyFunction();
        });
    };
};

我们有一个监控数组为空的特殊的测试案例,同样需要监听注销函数能够正常工作。在这种情况下,监听函数只被调用一次,但是仍然可以在第一次digest循环发生之前调用注销函数,在这种情况下单次调用应该被跳过:

test/scope_spec.js

it("does not call the zero-watch listen when deregistered first", function(){
    var counter = 0;

    var destroyGroup = scope.$watchGroup([], function(newValues, oldValues, scope){
        counter ++;
    });
    destroyGroup();
    scope.$digest();

    expect(counter).toEqual(0);

});

对于这种情况注销函数应该设置一个布尔型的标识,在调用监听函数之前先检查其值:

src/scope.js

Scope.prototype.$watchGroup = function(watchFns, listenerFn){
    var self = this;
    var newValues = new Array(watchFns.length);
    var oldValues = new Array(watchFns.length);
    var changeReactionScheduled = false;
    var firstRun = true;

    if(watchFns.length === 0){
        var shouldCall = true;
        self.$evalAsync(function() {
            if(shouldCall){
                listenerFn(newValues, oldValues, self);
            }
        });
        return function(){
            shouldCall = false;
        };
    }

    function watchGroupListener(){
        if (firstRun) {
            firstRun = false;
            listenerFn(newValues, newValues, self);
        }else{
            listenerFn(newValues, oldValues, self);
        }
        changeReactionScheduled = false;
    }

    var destroyFunctions = _.map(watchFns, function(watchFn, i){
        return self.$watch(watchFn, function(newValue, oldValue){
            newValues[i] = newValue;
            oldValues[i] = oldValue;
            if(!changeReactionScheduled){
                changeReactionScheduled = true;
                self.$evalAsync(watchGroupListener);
            }
        });
    });

    return function(){
        _.forEach(destroyFunctions, function(destroyFunction) {
            destroyFunction();
        });
    };
};

总结

到现在为止,我们终于拥有了一个完美的Angular风格的脏值检查作用域系统。

时间: 2024-08-18 09:11:52

创建自己的AngularJS - 作用域和Digest(五)的相关文章

创建自己的AngularJS - 作用域和Digest(四)

作用域 第一章 作用域和Digest(四) 联合$apply调用 - $applyAsync 不论在digest里面还是外面调用$evalAsync去延迟工作,他实际是为之前的使用案例设计的.之所以在setTimeout中调用digest是为了在digest循环外面调用$evalAsync时防止混淆. 针对在digest循环外部异步调用$apply的情况,同样有一个名为$applyAsync来处理.其使用类似于$apply - 为了集成没有意识到Angular digest循环的代码.和$app

构建自己的AngularJS - 作用域和Digest(一)

作用域 第一章 作用域和Digest(一) Angular作用域是简单javascript对象,因此你可以像对其他对象一样添加属性.然而,他们也有一些额外的功能,用于观测数据结构的变化.这种观察能力是使用脏值检查在digest循环中运行来实现的.这就是我们这一章将要实现的内容. 作用域对象 Scope的创建是通过在Scope构造函数之前加入new关键字来创建的.这样会产生一个简单javascript对象.让我们先来创建一个单元测试.(测试驱动开发,先写测试案例) 对Scope创建一个测试文件te

创建自己的AngularJS - 作用域继承(一)

作用域 作用域继承(一) Angular作用域继承机制直接建立在Javascript原型继承基础上,并在其根部加入了一些内容.这意味着当你理解了Javascript原型链后,将对Angular作用域继承有深入了解. 根作用域 到目前为止,我们一直在和一个作用域对象打交道,该作用域使用Scope构造函数创建: var scope = new Scope(); 根作用域就是这样创建的.之所以称之为根作用域,是因为他没有父作用域,它是典型的一个由所有作用域组成的树的根. 在现实中,你永远不用用这种方式

构建自己的AngularJS - 作用域和Digest(二)

作用域 第一章 作用域和Digest(二) 放弃一个不稳定的Digest 在我们当前的实现中有一个明显的遗漏:如果发生了两个监控函数互相监控对方的变化的情况会如何?也就是,万一状态永远不能稳定呢?就像下面的测试案例展示的情况: test/scope_spec.js it("gives up on the watchers after 10 iterations", function(){ scope.counterA = 0; scope.counterB = 0; scope.$wa

构建自己的AngularJS - 作用域和Digest(三)

作用域 第一章 作用域和Digest(三) $eval - 在当前作用域的上下文中执行代码 Angular有多种方式让你在当前作用域的上下文中执行代码.最简单的是$eval.传入一个函数当做其参数,然后将当前的作用域作为参数传给该函数,并执行它.然后它返回该函数的执行结果.$eval还有第二个可选的参数,它仅仅是被传递给将要执行的函数. 有几个单元测试展示了我们如何使用$eval: test/scope_spec.js it("execute $eval'ed function and retu

构建自己的AngularJS,第一部分:作用域和digest 转摘:http://www.ituring.com.cn/article/39865

构建自己的AngularJS,第一部分:Scope和Digest 原文链接:http://teropa.info/blog/2013/11/03/make-your-own-angular-part-1-scopes-and-digest.html Angular是一个成熟和强大的JavaScript框架.它也是一个比较庞大的框架,在熟练掌握之前,需要领会它提出的很多新概念.很多Web开发人员涌向Angular,有不少人面临同样的障碍.Digest到底是怎么做的?定义一个指令(directive

AngularJS作用域

AngularJS作用域 一.概要 在AngularJS中,子作用域(child scope)基本上都要继承自父作用域(parent scope). 但,事无绝对,也有特例,那就是指令中scope设置项为对象时,即scope:{…},这将会让指令创建一个并不继承自父作用域的子作用域,我们称之为隔离作用域(isolated scope). 指令中的scope一共可以有三个值,下面我们再来温习下: 指令之scope scope: false 默认值,指令不会新建一个作用域,使用父级作用域. scop

剖析AngularJS作用域

一.概要 在AngularJS中,子作用域(child scope)基本上都要继承自父作用域(parent scope). 但,事无绝对,也有特例,那就是指令中scope设置项为对象时,即scope:{…},这将会让指令创建一个并不继承自父作用域的子作用域,我们称之为隔离作用域(isolated scope). 指令中的scope一共可以有三个值,下面我们再来温习下: 指令之scope scope: false 默认值,指令不会新建一个作用域,使用父级作用域. scope: true 指令会创建

AngularJS 作用域与数据绑定机制

AngularJS 简介 AngularJS 是由 Google 发起的一款开源的前端 MVC 脚本框架,既适合做普通 WEB 应用也可以做 SPA(单页面应用,所有的用户操作都在一个页面中完成).与同为 MVC 框架的 Dojo 的定位不同,AngularJS 在功能上更加轻量,而相比于 jQuery,AngularJS 又帮您省去了许多机械的绑定工作.在一些对开发速度要求高,功能模块不需要太丰富的非企业级 WEB 应用上,AngularJS 是一个非常好的选择.AngularJS 最为复杂同