angular之scope.$watch

  某“大神”挖了个陨石坑,我于是乎似懂非懂的接手,玩了一个月angular。现在项目告一段落,暂别了繁重的重复性工作,可以开始回顾、认真的折腾下之前犹抱琵琶的angular。

  angular吸引人的特性之一就是双向绑定,model有变化view自动更新。一说到自动执行,首先浮到脑海的必须是监听和回调函数。angular也确实是这样做的,scope.$watch就是此行为的接口。一如所有的类库或框架,使用起来很简单,实现却并不容易。

  我不是一个执念于从零开始的人,喜欢站在巨人的肩上,这篇随笔取材于此:创建你自己的AngularJS

  首先,先搭建一个单元测试环境,如果嫌麻烦当然可以不用,如果不会可以在node环境下输入以下命令行:

npm install -g grunt    //安装grunt
npm install -g bower    //安装bower
npm install -g yo    //安装yeoman
npm install -g generator-angular    //安装angular生成器
yo angular    //生成angular项目文件
npm install    //安装项目依赖包
bower install    //安装前端依赖库
grunt test    //执行单元测试

  以上命令来自记忆,或有遗漏,总之如果执行成功表示环境搭建完毕。

  接下来开始编写自己的scope,我们先整理下测试环境,在angular/test/spec下建立两个文件夹,命名随意,我是define和unit。

  修改angular/test/karma.conf.js中的files字段:

files: [
      "test/spec/define/*.js",
      "test/spec/unit/*.js"
    ]

  然后可以新建Scope类了,在spec/define中新建scope.js:

‘use strict‘;

function Scope() {
}

  是的,只是一个简单的构造函数。

  然后在spec/unit中新建test.js,编写单元测试语句:

‘use strict‘;

describe("Scope", function() {
  it("can be constructed and used as an object", function() {
    var scope = new Scope();
    scope.prop = 1;
    expect(scope.prop).toBe(1);
  })
});

  如无意外,打印出来的结果是这样的:

PhantomJS 1.9.8 (Windows 8): Executed 1 of 1 SUCCESS (0 secs / 0.001 secs)

  其中PhantomJS是一个不展示用户界面的浏览器,测试的代码就是在这里面跑的。

  模型已经搭建完成,现在我们要实现的是监听scope内部prop属性的变动。要实现监听,首先内部要有监听器的队列,其次要有添加监听器的函数,最后还要有轮询检测的函数,分别在scope.js中添加这三个内容:

‘use strict‘;

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

Scope.prototype.$watch = function() {
  var watcher = {};
  this.$$watchers.unshift(watcher);
}

Scope.prototype.$digest = function() {

}

  一般类库框架,对于添加监听器的函数都支持两个参数,字符串和回调函数,比如jquery中的on。对于我们来说,字符串应该是要监听的属性。修改$watch函数如下:

Scope.prototype.$watch = function(prop, callback) {
  var watcher = {
    prop: prop,
    callback: callback
  };
  this.$$watchers.unshift(watcher);
}

  现在轮到$digest函数,轮询函数的任务在于遍历所有的监听器,比较当前属性和上一个属性是否不同,不同则执行监听器中的callback。等等,上一个属性怎么获取的,看来添加监听器的时候应该保存一个初始值,于是两个函数都要修改:

Scope.prototype.$watch = function(prop, callback) {
  var watcher = {
    prop: prop,
    callback: callback,
    last: this[prop]
  };
  this.$$watchers.unshift(watcher);
};

Scope.prototype.$digest = function() {
  var scope = this;
  scope.$$watchers.forEach(function(watcher) {
    if(scope[watcher.prop] !== watcher.last) {
      watcher.last = scope[watcher.prop];
      watcher.callback();
    }
  });
};

  现在感觉功能已经实现了,但实际上有没有用呢,要写个单元测试验证一下,修改test.js:

‘use strict‘;

describe("Scope", function() {
  it("can be constructed and used as an object", function() {
    var scope = new Scope(),
        callback = jasmine.createSpy();    //创建一个可检测是否被调用的回调函数
    scope.prop = 1;    //初始化值
    scope.$watch(‘prop‘, callback);    //添加监听函数
    scope.prop = 2;    //重新赋值
    scope.$digest();    //轮询一遍
    expect(callback).toHaveBeenCalled();    //检测回调函数是否被调用
  });
});

  执行grunt test,得到的结果如下:

PhantomJS 1.9.8 (Windows 8): Executed 1 of 1 SUCCESS (0.004 secs / 0.003 secs)

  可见是正常可用的,但是不是哪里写错了导致怎么执行都正常呢,我们注释掉重新赋值的这行:

//scope.prop = 2;

  得到的测试结果如下:

PhantomJS 1.9.8 (Windows 8): Executed 1 of 1 (1 FAILED) ERROR (0.01 secs / 0.002 secs)

  严格测试下,添加两个监听器:

‘use strict‘;

describe("Scope", function() {
  it("can be constructed and used as an object", function() {
    var scope = new Scope(),
        callback1 = jasmine.createSpy(),
        callback2 = jasmine.createSpy();
    scope.prop1 = 1;
    scope.prop2 = 1;
    scope.$watch(‘prop1‘, callback1);
    scope.$watch(‘prop2‘, callback2);
    scope.prop1 = 2;
    scope.prop2 = 2;
    scope.$digest();
    expect(callback1).toHaveBeenCalled();
    expect(callback2).toHaveBeenCalled();
  });
});

  结果也是没有问题的:

PhantomJS 1.9.8 (Windows 8): Executed 1 of 1 SUCCESS (0.004 secs / 0.002 secs)

  基本功能是没问题了,但还需要优化。

  首先,代码不够“优雅”。监听器中的prop属性唯一的用处就是在$digest中供取值,而且为此$digest需要将this重新付给scope变量导致$digest中代码偏乱。把scope.js的代码整理一下:

‘use strict‘;

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

Scope.prototype.$watch = function(prop, callback) {
  var scope = this,
      watcher = {
        get: function() {
          return scope[prop];
        },
        callback: callback,
        last: scope[prop]
      };
  scope.$$watchers.unshift(watcher);
};

Scope.prototype.$digest = function() {
  this.$$watchers.forEach(function(watcher) {
    if(watcher.get() !== watcher.last) {
      watcher.last = watcher.get();
      watcher.callback();
    }
  });
};

  这样是不是整洁多了?

  还有,回调函数现在是不传入参数的,而按照习惯来说,应该传入新值和旧值吧。所以要对$digest和test.js作出修改:

Scope.prototype.$digest = function() {
  this.$$watchers.forEach(function(watcher) {
    if(watcher.get() !== watcher.last) {
      watcher.callback(watcher.get(), watcher.last);
      watcher.last = watcher.get();
    }
  });
};
‘use strict‘;

describe("Scope", function() {
  it("can be constructed and used as an object", function() {
    var scope = new Scope(),
        newValue, oldValue,
        callback = function(newV, oldV) {
          newValue = newV;
          oldValue = oldV;
        };
    scope.prop = 1;
    scope.$watch(‘prop‘, callback);
    scope.prop = 2;
    scope.$digest();
    expect(newValue).toBe(2);
    expect(oldValue).toBe(1);
  });
});

  测试的结果是ok的。

  最后,当我们在回调函数中对设置了监听器的属性进行赋值时,会出现问题,比如修改test.js:

‘use strict‘;

describe("Scope", function() {
  it("can be constructed and used as an object", function() {
    var scope = new Scope(),
        callback1 = function(newV, oldV) {
          scope.prop2 = 2;
          scope.counter++;
        },
        callback2 = function(newV, oldV) {
          scope.prop1 = 2;
          scope.counter++;
        };
    scope.prop1 = 1;
    scope.prop2 = 1;
    scope.counter = 0;
    scope.$watch(‘prop1‘, callback1);
    scope.$watch(‘prop2‘, callback2);
    scope.prop1 = 2;  //失败
    //scope.prop2 = 2; //成功
    scope.$digest();
    expect(scope.counter).toBe(2);
  });
});

  注意注释的部分,为什么会产生这种偏差呢?因为在$watch中,我们是使用unshift插入监听器的,当在callback1中设置prop2的时候,prop2的监听器已经被轮询过了,所以不再会调用。

  那怎么解决这一问题呢?这里就要使用到dirty-checking了。在$digest中不停的对监听器进行轮询,但最少轮询一次,也就是do...while...。当有回调函数被调用时,则置整个轮询的dirty为true,需要进行下一次轮询;当一次轮询中没用任何回调函数被调用,则终止轮询。修改$digest如下:

Scope.prototype.$digest = function() {
  var dirty;
  do {
    dirty = false;
    this.$$watchers.forEach(function(watcher) {
      if(watcher.get() !== watcher.last) {
        dirty = true;
        watcher.callback(watcher.get(), watcher.last);
        watcher.last = watcher.get();
      }
    });
  } while(dirty)
};

  但这样做有一个致命的问题就是,当回调函数这样的时候:

callback1 = function(newV, oldV) {
          scope.prop2 += 1;
          scope.counter++;
        },
        callback2 = function(newV, oldV) {
          scope.prop1 += 1;
          scope.counter++;
        };

  PhantomJS表示,你根本停不下来:

WARN [PhantomJS 1.9.8 (Windows 8)]: Disconnected (1 times), because no message in 10000 ms.

Warning: Task "karma:unit" failed. Use --force to continue.

  这里就要设置一个while循环的上限值TTL(Time To Live):

Scope.prototype.$digest = function() {
  var dirty, ttl = 10;
  do {
    dirty = false;
    ttl--;
    this.$$watchers.forEach(function(watcher) {
      if(watcher.get() !== watcher.last) {
        dirty = true;
        watcher.callback(watcher.get(), watcher.last);
        watcher.last = watcher.get();
      }
    });
  } while(dirty && ttl > 0)
};

  好了,到这里就结束了。现在去看angular中scope的source code,一定能看到很多眼熟的东西。

时间: 2024-08-27 11:27:27

angular之scope.$watch的相关文章

angular directive scope

1.当directive 中不指定scope属性,则该directive 直接使用 app 的scope: 2.当directive 中指定scope属性时,scope作用域有3种方式: 2.1 .   = : 代表数据双向绑定,只要该属性发生改变 ,app(即父作用域) 中scope对应的值和 directive中对应的值将同时发生改变 : 2.2 .   @ : 代表数据单向绑定,该值的改变只会影响directive ,不会影响app(即父作用域) 其他值, 也就是孤立作用域 : 2.3 .

angular的$scope的使用

1. 可以在scope中直接使用 // 监听日期变化 $scope.$watch('vaFilter.startEffectiveDate', function(newDate, oldDate, scope){ if (!angular.isUndefined(newDate)) { $scope.fromDate = newDate; } }); 2. 可以直接监视angular以外的js变量 angular.module('myModule', []).controller('MyCtrl

angular.js_$scope

Scope(作用域) 是应用在 HTML (视图) 和 JavaScript (控制器)之间的纽带. Scope 是一个对象,有可用的方法和属性. Scope 可应用在视图和控制器上. AngularJS 应用组成如下: View(视图), 即 HTML. Model(模型), 当前视图中可用的数据. Controller(控制器), 即 JavaScript 函数,可以添加或修改属性. scope 是模型. scope 是一个 JavaScript 对象,带有属性和方法,这些属性和方法可以在视

angular中的scope

angular.element($0).scope() 什么是scope? scope是一个refer to the application model的object.它是一个expression的执行上下文context.scopes模仿DOM的层次模型也有层次关系.Scopes可以watch一个expression也可以propagate events. scope特性 scope提供API $watch来观察模型的变化 scope提供API $apply来传播angular外部世界(比如c

在angular的自定义回调中操作$scope

在angular的controller中有时候会使用到自定义的回调, 比如异步请求的回调函数 一般返回之后都需要更新页面的数据,即更新$scope.xx 但是如果直接 $scope.xx = data.info 这样根本不会有任何的影响 解决方案 this.callback = function (status) { $scope.$apply(function () { $scope.xx = status; }); }; Scope提供$watch方法监视Model的变化. Scope提供$

Angular 从DOM中获取scope

节选官方文档: 原文:https://docs.angularjs.org/guide/scope Retrieving Scopes from the DOM. Scopes are attached to the DOM as $scope data property, and can be retrieved for debugging purposes. (It is unlikely that one would need to retrieve scopes in this way

Angular this vs $scope

this vs $scope ------------------------------------------------------------------------------ 'this' vs $scope in AngularJS controllers How does this and $scope work in AngularJS controllers? Short answer: this When the controller constructor functio

野兽的Angular Api 学习、翻译及理解 - - $rootScope.Scope

野兽的ng api学习 -- $rootScope.Scope 这里讲的是一些scope的操作,如创建/注销/各种监听及scope间的通信等等. $rootScope.Scope 可以使用$injector通过$rootScope关键字检索的一个根作用域. 可以通过$new()方法创建子作用域.(大多子作用域是在HTML模板被执行编译时自动生成) 格式:$rootScope.Scope([Providers],[instanceCache]) [Providers]:当前作用域需要被提供的服务工

angular controller as syntax vs scope

今天要和大家分享的是angular从1.2版本开始带来了新语法Controller as.再次之前我们对于angular在view上的绑定都必须使用直接的scope对象,对于controller来说我们也得必须注入$scope这个service.如下: ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 angular.module("app",[]) .controller("demoController",["$scope&qu