zoukankan      html  css  js  c++  java
  • 构建自己的AngularJS

    作用域

    第一章 作用域和Digest(三)

    $eval - 在当前作用域的上下文中运行代码

    Angular有多种方式让你在当前作用域的上下文中运行代码。最简单的是$eval。传入一个函数当做其參数。然后将当前的作用域作为參数传给该函数,并运行它。然后它返回该函数的运行结果。$eval还有第二个可选的參数。它不过被传递给将要运行的函数。

    有几个单元測试展示了我们怎样使用$eval

    test/scope_spec.js

    it("execute $eval'ed function and return the result", function(){
        scope.aValue = 42;
    
        var result = scope.$eval(function(scope){
            return scope.aValue;
        })
    
        expect(result).toBe(42);
    });
    
    it("pass the second $eval argument straight through", function(){
        scope.aValue = 42;
        var result = scope.$eval(function(scope, arg){
            return scope.aValue + arg;
        }, 2);
    
        expect(result).toBe(44);
    });

    实现$eval非常easy:

    src/scope.js

    Scope.prototype.$eval = function(expr, locals){
        return expr(this, locals);
    };

    使用这样的迂回的方式去触发一个函数有什么目的呢?有人觉得:$eval只能够让一些处理作用域的代码调用起来略微清楚一点。我们即将看到。$eval$apply的基石。

    然而,使用$eval最有趣的地方我们使用表达式取代原函数。

    $watch一样,你能够给$eval一个字符串表达式。

    它会编译该表达式。在作用域的上下文中运行。我们会在该书的第二部分实现。

    $apply - 整合使用Digest循环的外部代码

    可能作用域中最广为人知的函数就是$apply了。他被觉得是外部类库集成到Angular中最标准的方法。原因是:

    $apply使用函数作为其參数。其使用$eval运行该函数,然后启动通过调用$digest启动digest循环。有几个測试案比例如以下:

    test/scope_spec.js

    it("execute $apply'ed function and starts the digest", function(){
        scope.aValue = 'someValue';
        scope.counter = 0;
    
        scope.$watch(
            function(scope){ return scope.aValue; },
            function(newValue, oldValue, scope){ 
                scope.counter ++;
            });
    
        scope.$digest();
        expect(scope.counter).toBe(1);
    
        scope.$apply(function(scope){
            scope.aValue = 'someOtherValue';
        });
    
        expect(scope.counter).toBe(2);
    });

    我们有一个监控函数监控scope.aValue,而且让计数器自增。当$apply触发的时候我们測试该监控函数是否被运行。

    以下是让该測试案例通过的一个简单的实现:

    src/scope.js

    Scope.prototype.$apply = function(expr){
        try{
            return this.$eval(expr);
        }finally{
            this.$digest();
        }
    };

    $digest在finally块中被调用。来确保即使提供的函数抛出异常digest循环一定会发生。

    $apply的最大思想是我们能够运行一些Angular没有意识到的代码。而这些代码可能改变作用域上的内容,只要我们用$apply包装了这些代码,我们
    能够确保该作用域上的任一个监控都能接受到这些变化。

    当人们谈论在“Angular生命循环”中使用$apply集成代码时,这基本上就是他们要表达的意思。

    $evalAsync - 推迟运行

    在Javascript中,推迟运行一块代码非常寻常 - 推迟其运行到未来的某个时间点,直到当前的运行完毕。

    一般是通过调用setTimeout()传入0(或者一个非常小)的延迟的參数来实现的。

    这样的模式也能够应用到Angular应用上,虽然比較推荐的方法是使用$timeout服务。当中。在digest中使用$apply集成延迟函数

    可是在Angular中有第二种方法推迟代码的运行,那就是Scope中的$evalAsync函数。

    $evalAsync提供一个函数作为參数,推迟调度其运行。可是仍在其当前正在运行的digest中。比如,你能够,在一个监听函数中推迟一段代码的运行,了解到虽然这些代码被推迟了,可是在当前的digest遍历中仍然会被触发。

    相比于$timeout$evalAsync更可取的原因和浏览器的事件循环有关。

    当你使用$timeout去调度你的工作,你把你的控制权给了浏览器。让浏览器决定什么时候去运行你的调度。

    在你的工作到达限制时间之前。浏览器可能选择其它工作先去运行。比如,渲染页面,运行点击事件,或者处理Ajax响应。与之不同的是,$evalAsync在运行工作方面更加的严格。

    由于他在当前正在运行的digest中运行,能够保证在其一定在浏览器运行其它事情之前运行。$timeout$evalAsync的差别在你想要阻止不必要的渲染的时候更加明显:为什么要让浏览器渲染即将被覆盖掉的DOM变化呢?

    以下是关于$evalAsync的单元測试:

    test/scope_spec.js

    it("execute $evalAsync'ed function later in the same cycle", function(){
        scope.aValue = [1, 2, 3];
        scope.asyncEvaluated = false;
        scope.asyncEvaluatedImmediately = false;
    
        scope.$watch(
            function(scope) { return scope.aValue; },
            function(newValue, oldValue, scope){
                scope.$evalAsync(function(){
                    scope.asyncEvaluated = true;
                });
    
                scope.asyncEvaluatedImmediately = scope.asyncEvaluated;
            });
    
        scope.$digest();
        expect(scope.asyncEvaluated).toBe(true);
        expect(scope.asyncEvaluatedImmediately).toBe(false);
    });

    我们在监听函数中调用了$evalAsync。然后检查该函数时候在同一个digest中最后被运行。

    首先,我们须要去存储被调度的$evalAsync任务。

    我们想要使用数组来存储。在Scope的构造函数中初始化:

    src/scope.js

    function Scope(){
        this.$$watchers = [];
    	this.$$lastDirtyWatch = null;
        this.$$asyncQueue = [];
    }

    然后我们来定义$evalAsync。让其将要运行的函数增加该队列中:

    src/scope.js

    Scope.prototype.$evalAsync = function(expr){
        this.$$asyncQueue.push({scope: this, expression: expr});
    };

    我们明白地在当前队列的对象中存储当前的作用域,是和作用域的继承有关的。我们将在下一章讨论这个问题。

    对于将要被运行的函数。我们先将他们记录下来,其实。我们还须要去运行他们。那将是在$digest中发生:首先在$digest中我们要消耗该队列中的全部内容。然后通过使用$eval来触发全部被延迟的函数:

    src/scope.js

    Scope.prototype.$digest = function(){
        var tt1 = 10;
        var dirty;
        this.$$lastDirtyWatch = null;
    	do {
    		while (this.$$asyncQueue.length){
    			var asyncTask = this.$$asyncQueue.shift();
    			asyncTask.scope.$eval(asyncTask.expression);
    		}
    		dirty = this.$$digestOnce();
            if(dirty && !(tt1 --)) {
                throw '10 digest iterations reached';
            }
        } while (dirty);
    };

    该实现保证了当scope是脏的情况下推迟函数的运行,之后才回触发函数。可是仍然能在同一个digest中。

    这满足了我们的单元測试。

    在监控函数中使用$evalAsync

    在上一节中,我们看到了在监听函数中使用$evalAsync调度函数仍在同一个digest循环中延迟运行。可是假设你在监控函数中使用$evalAsync会发生什么呢?假定有一件事情你不须要去做,由于监控函数应该是没有副作用的。可是他仍然可能去做,所以我们应该确定这不能在digest中造成破坏。

    我们思考一个场景,在监控函数中使用一次$evalAsync。每件事看起来都是有序的。

    在我们当前的视线中。以下的測试案例应该都能通过:

    test/scope_spec.js

    it("executes $evalAsync'ed functions added by watch functions", function(){
        scope.aValue = [1, 2, 3];
        scope.asyncEvaluated = false;
    
        scope.$watch(
            function(scope){
                if(!scope.asyncEvaluated){
                    scope.$evalAsync(function(){
                        scope.asyncEvaluated = true;
                    })
                }
                return scope.aValue;
            },
            function(newValue, oldValue, scope) {} );
    
        scope.$digest();
        expect(scope.asyncEvaluated).toBe(true);
    });

    那么问题是什么呢?正如我们看到的,我们在最后一个监控是脏的情况下保持digest循环继续。在上面測试案例中,这样的情况就发生在第一次遍历中,当我们在监控函数中返回scope.aValue。这引起了digest进入下一次遍历,在这次遍历中。他调用了我们使用$evalAsync调度的函数。

    可是当没有监控是脏的情况下。我们调度$evalAsync呢?

    test/scope_spec.js

    it("executes $evalAsync'ed functions even when not dirty", function(){
        scope.aValue = [1, 2, 3];
        scope.asyncEvaluatedTimes = 0;
    
        scope.$watch(
            function(scope){
                if(scope.asyncEvaluatedTimes < 2){
                    scope.$evalAsync(function(scope){
                        scope.asyncEvaluatedTimes ++;
                    });
                }
                return scope.aValue;
            },
            function(newValue, oldValue, scope) {});
    
        scope.$digest();
        expect(scope.asyncEvaluatedTimes).toBe(2);
    
    });

    这个版本号做了两次$evalAsync。在第二次中。监控函数不是脏的,由于scope.aValue没有发生变化。这意味着$evalAsync没有运行,由于$digest已经停止了。

    虽然他会在下一次digest中运行,可是我们希望他这次中运行。

    同一时候意味着我们须要调整$digest的结束条件,查看在异步队列中是否有内容须要去运行:

    src/scope.js

    Scope.prototype.$digest = function(){
        var tt1 = 10;
        var dirty;
        this.$$lastDirtyWatch = null;
    	do {
    		while (this.$$asyncQueue.length){
    			var asyncTask = this.$$asyncQueue.shift();
    			asyncTask.scope.$eval(asyncTask.expression);
    		}
    		dirty = this.$$digestOnce();
    		if(dirty && !(tt1 --)) {
    			throw '10 digest iterations reached';
    		}
    	} while (dirty || this.$$asyncQueue.length);
    };

    測试案例通过了。可是如今我们引入了一个问题。假设一个监控函数一直使用$evalAsync去调度一些事情呢?我们可能希望引起循环达到最大值。实际上。并没有:

    test/scope_spec.js

    it("eventually halts $evalAsyncs added by watches", function(){
        scope.aValue = [1, 2, 3];
    
        scope.$watch(
            function(scope){
                scope.$evalAsync(function(scope){});
                return scope.aValue;
            },
            function(newValue, oldValue, scope) {} 
        );
    
        expect(function(){ scope.$digest()}).toThrow();
    });

    该測试会一直运行下去。由于$digest中的循环一直不会结束。

    我们须要做的是在TTL检查中也检查异步队列的状态:

    src/scope.js

    Scope.prototype.$digest = function(){
        var tt1 = 10;
        var dirty;
        this.$$lastDirtyWatch = null;
    	do {
    		while (this.$$asyncQueue.length){
    			var asyncTask = this.$$asyncQueue.shift();
    			asyncTask.scope.$eval(asyncTask.expression);
    		}
    		dirty = this.$$digestOnce();
    		if((dirty || this.$$asyncQueue.length) && !(tt1 --)) {
    			throw '10 digest iterations reached';
    		}
    	} while (dirty || this.$$asyncQueue.length);
    };

    不论digest由于是脏的情况下运行。还是由于在队列中有内容的情况下运行,如今我们都能够确定其会结束。

    作用域相位

    $evalAsync还有一个功能是调度一个没有准备运行的$digest去运行。也就是。不论当你什么时候调用$evalAsync,你都能够确定你正在推迟的函数非常快会被触发。而不是等到有些内容去触发一个digest之后。

    虽然$evalAsync不会调度一个$digest,比較好的方式是使用$applyAsync去异步运行一个有digest的代码。让我们開始下个章节吧。

    这里为了这样的情况能够工作。对于$evalAsync来说须要有方法去检查$digest是否正在运行。由于那种情况下。他不能够去打搅一个正在运行的代码。由于这个原因,Angular作用域实现了名叫相位(phase)属性。是作用域上的一个简单字符型的属性。存储着当前正在运行的一些信息。

    作为一个单元測试,让我们给这个名叫$$phase的字段设置一个期望。在digest过程中,值应该是“digestapply”。其它情况为null:

    test/scope_spec.js

    it("has a $$phase field whose value is the current digest phase", function(){
        scope.aValue = [1, 2, 3];
        scope.phaseInWatchFunction = undefined;
        scope.phaseInListenerFunction = undefined;
        scope.phaseInApplyFunction = undefined;
    
        scope.$watch(
            function(scope){
                scope.phaseInWatchFunction = scope.$$phase;
    		},
    		function(newValue, oldValue, scope){
    			scope.phaseInListenerFunction = scope.$$phase;
            }
        );
    
        scope.$apply(function(scope){
            scope.phaseInApplyFunction = scope.$$phase;
        });
    
        expect(scope.phaseInWatchFunction).toBe("$digest");
        expect(scope.phaseInListenerFunction).toBe("$digest");
        expect(scope.phaseInApplyFunction).toBe("$apply");
    });

    在此我们不须要显示的调用$digest,由于$apply已经帮我们做了。

    在Scope的构造函数中。让我们增加$$phase字段,并初始化为null。

    src/scope.js

    function Scope(){
        this.$$watchers = [];
    	this.$$lastDirtyWatch = null;
        this.$$asyncQueue = [];
    	this.$$phase = null;
    }

    以下。让我们定义一组函数用来控制相位:一个用来设置他,一个用来清除他。同一时候我们在设置函数增加一个校验,来确保当其正在活动时,我们没有试图去设置他。

    src/scope.js

    Scope.prototype.$beginPhase = function(phase){
        if(this.$$phase){
    		throw this.$$phase + " already in progress.";
    	}
    	this.$$phase = phase;
    };
    
    Scope.prototype.$clearPhase = function(){
        this.$$phase = null;
    };
    

    $digest中。我们在digest循环外设置phase为”$digest”:

    src/scope.js

    Scope.prototype.$digest = function(){
        var tt1 = 10;
        var dirty;
        this.$$lastDirtyWatch = null;
    	this.$beginPhase("$digest");
    	do {
    		while (this.$$asyncQueue.length){
    			var asyncTask = this.$$asyncQueue.shift();
    			asyncTask.scope.$eval(asyncTask.expression);
    		}
    		dirty = this.$$digestOnce();
    		if((dirty || this.$$asyncQueue.length) && !(tt1 --)) {
    			this.$clearPhase();
    			throw '10 digest iterations reached';
    		}
    	} while (dirty || this.$$asyncQueue.length);
    
        this.$clearPhase();
    };

    让我们也来改变$apply让其能够为他自己设置该相位:

    src/scope.js

    Scope.prototype.$apply = function(expr){
        try{
            this.$beginPhase("$apply");
            return this.$eval(expr);
        }finally{
            this.$clearPhase();
            this.$digest();
        }
    };
    

    最后,我们能够将通过$evalAsync来调用$digest。让我们先为这个需求定义一个单元測试:

    test/scope_spec.js

    it("schedules a digest in $evalAsync", function(done){
        scope.aValue = "abc";
        scope.counter = 0;
    
        scope.$watch(
            function(scope){
                return scope.aValue;
            },
            function(newValue, oldValue, scope){
                scope.counter ++;
            }
        );
    
        scope.$evalAsync(function(scope) {} );
    
        expect(scope.counter).toBe(0);
        setTimeout(function() {
            expect(scope.counter).toBe(1);
            done();
        }, 50);
    
    });

    我们检測digest确实运行了,不是在$evalAsync调用之后,而是略微在其后面。我们定义“略微后面”是指50毫秒后。

    为了让setTimeout能够在Jasmine中运行,我们使用了异步測试支持:该測试案例接受一个额外的參数作为回调參数。一旦我们调用它,他会完毕整个測试。我们已经在延迟之后这样做了。

    如今$evalAsync能够測试作用域的当前相位了,假设没有(还没有异步的任务被调度),调度digest运行。

    src/scope.js

    Scope.prototype.$evalAsync = function(expr){
        var self = this;
        if(!self.$$phase && !self.asyncQueue.length){
    		setTimeout(function(){
    			if(self.$$asyncQueue.length){
    				self.$digest();
    			}
    		}, 0);
    	}
    	this.$$asyncQueue.push({scope: this, expression: expr});
    };

    通过该实现,你能够确定当你调用$evalAsync时,digest会在不久之后运行,不论你什么时间、什么时候调用它。

    当你在digest运行过程中调用$evalAsync,你的函数会在这次digest中被计算。假设没有digest正在运行。一个digest会被启动。我们使用setTimeout在digest之前做一个略微的延迟。$evalAsync的这样的调用方式能够确保:不论digest循环当前是哪种状态,函数都会马上返回而不是异步计表达式。

    扫一扫,很多其它好文早知道

  • 相关阅读:
    扩展当easyui datagrid无数据时,显示特定值。如:没有数据
    draggable datagrid columns
    easyui combobox 带 checkbox
    LightOJ1003---Drunk(拓扑排序判环)
    算法分析---回文数推断
    Android集成百度地图SDK
    BS一机双屏的解决方式
    myeclipse中更改web项目在tomcat中部署的路径
    Linux内核调试技术——jprobe使用与实现
    【Servlet】把文件写到Respond输出流里面供用户下载
  • 原文地址:https://www.cnblogs.com/liguangsunls/p/7184054.html
Copyright © 2011-2022 走看看