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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM