• 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,一定能看到很多眼熟的东西。

  • 相关阅读:
    docker 部署 netcore3.1项目,使用nginx反向代理,swagger 不能访问的问题 SwaggerUIBundle is not defined
    windows 下使用SCP命令传递文件/文件夹到Liunx系统
    dockerfile 中的CMD 和 ENTRYPOINT 的区别
    netcore3.1 dockerfile 生成镜像 并 部署到 linux 系统
    docker 自定义网络 不使用 link 也能实现容器间网络互通
    CentOS 7中安装Nginx 并设置自启动
    微信小程序实现相册展示及图片预览
    netcore 3.1 项目部署到docker 及遇到的各种坑 和 对应解决方案
    netcore3.1 webApi项目发布Docker后,通过容器互联,访问redis 【部署redis到docker】
    docker 安装 mongodb 容器 及 挂载 配置文件、数据存放目录,日志存放目录,备份目录等
  • 原文地址:https://www.cnblogs.com/tarol/p/4616826.html
Copyright © 2020-2023  润新知