AngularJs1.x解讀 $watch 和 $digest

weixin_34302561發表於2018-09-17

Scope object

Scope物件其實就是一個簡單的POJO(plain old JavaScript Object)。我們可以給它任意的新增屬性。

// scope.js
export default class Scope {

}
// test.js
const scope = new Scope();
scope.aProperty = 1;
expect(scope.aProperty).toBe(1);

$watch$digest

$watch$digest就像一個硬幣的兩面。他們組合在一起就是髒檢查迴圈的核心:對於資料變化的響應。

$watch:

$watch(watchExpression, listener, [objectEquality]);
  • watchExpression: 監聽的資料
  • listener:資料發生變化的時候呼叫
  • objectEquality: 後面單獨說

angularjs中將所有的 watchExpression 存放到一個叫作$$watcher的陣列中,因此我們建立一個陣列:

$$watchers = [];

$watch(watchFn, listenerFn) {

    const watcher = {
      watchFn,
      listenerFn
    };
    
    this.$$watchers.push(watcher);
} 

$digest:

它遍歷scope上的所有watchers,計算 watchExpression ,並且呼叫它對應的 listenerFn。

_.forEach(this.$$watchers, watcher => {
  watcher.listenerFn();
});

髒值檢測

目的:只有監控的值發生改變的時候我們才執行對應的listener。
思路:儲存上一次的值,和這一次值的進行比對。

$digest() {
    // 將變數宣告提取到迴圈外面
    let newValue;
    let oldValue;
    
    _.forEach(this.$$watchers, watcher => {
      
      newValue = watcher.watchFn(this);
      // 第一次獲取last的時候值為undefined
      oldValue = watcher.last;
      // 只有當新舊值不相等的時候才執行listener
      if (newValue !== oldValue) {
        watcher.last = newValue;
        watcher.listenerFn(newValue, oldValue, this);
      }
    });
}

初始化watch的值

angularjs中的初始化值為一個函式:

function initWatchVal() {}

然後將其賦值給watch的last:

const watcher = {
  watchFn,
  listenerFn,
  last: initWacthVal
};

持續監測

新增一個幫助方法,將所有的watchFn執行一次,返回一個boolean值,表示是否有更新:

$digest() {
  
    let newValue;
    let oldValue;
    // 標記是否為髒
    let dirty;
    // 上來先執行一次看是否所有值發生變化,如果有變化,則第二次執行watch
    do {
      // 初次進來設定為false
      dirty = false;
    
      _.forEach(this.$$watchers, watcher => {
      
        newValue = watcher.watchFn(this);
        // 第一次獲取last的時候值為undefined
        oldValue = watcher.last;
        // 只有當新舊值不相等的時候才執行listener
        if (newValue !== oldValue) {
          dirty = true;
          watcher.last = newValue;
          // watcher.listenerFn(newValue, oldValue, this);
          const temp = oldValue === initWacthVal ? newValue : oldValue;
          watcher.listenerFn(newValue, temp, this);
        }
      });
    
    } while (dirty);
    
}

ttl

如果watch一直為不穩定的值,我們需要停止髒檢查。angularjs中預設的ttl為10,對外暴露可修改。

// 如果為髒,並且ttl達到0的時候
if (dirty && !(ttl--)) {
    throw '10 digest iterations reached';
}

停止髒檢查

新增lastDirtyWatch去判斷,看原始碼。

  • 執行$digest的時候將 lastDirtyWatch = null
  • 當前watcer 和 lastDirtyWatch 相同的時候設定為 null
  • watch的listener裡面包含有watch的時候,重置為 null

基於值的髒檢查

$watch(watchExpression, listener, [objectEquality]);

判斷是否相等要包含陣列和物件,angularjs內建的方法為:

angular.equals(o1, o2);

然後替換原來的新舊值的判斷:

// 判斷是否相等
function areEqual(newValue, oldValue, valueEq) {
  
  if (valueEq) {
    return _.isEqual(newValue, oldValue);
  } else {
    return newValue === oldValue;
  }

}

銷燬監聽

angularjs中的銷燬是給$watch的時候返回一個方法,當你呼叫的時候,直接將它從陣列中移除。

$watch(watchFn, listenerFn, valueEq) {

    const watcher = {
      watchFn,
      listenerFn,
      valueEq, 
      last: initWacthVal
    };
    
    this.$$watchers.push(watcher);
    // 新加入一個watcher的時候將lastDirtyWatch初始化
    this.lastDirtyWatch = null;

    return () => {
      const index = _.find(this.$$watchers, watcher);
      if (index >= 0) {
        this.$$watchers.splice(index, 1);
      }
    };
}
對內簡單的分享,記錄下來。參照書名:build your own angularjs

相關文章