論如何監聽一個對象某個屬性的變化

往期

前言

本文分爲入門和進階兩部分,建議有經驗的讀者直接閱讀進階部分。vue

本文主要參考了vue這一開源庫,若讀者閱讀過它的源碼能夠直接跳過本文 :)瀏覽器

入門

關於訪問對象的某個屬性

既然是入門,仍是先提一下Vue.prototype.$watch的幾種用法app

const vm = Vue({
  data() {
    return {
      b: true,
      o: { name: 'obj' },
      a: ['a', 'b', 'c'],
      odeep: {
        path: {
          name: 'obj deep',
          value: [],
        },
      },
    };
  },

  watch: {
    // 若是b的值改變了,打印改變前與改變後的值
    b(val, oldVal) {
      console.warn(val, oldVal);
    },
    // 若是o.name的值改變了,打印改變前與改變後的值
    'o.name': {
      handler(val, oldVal) {
        console.warn(val, oldVal);
      },
    },
  },

  created() {
    // 深度監聽: 若是odeep.path.name/odeep.path.value的值改變了,打印odeep.path改變前與改變後的值
    this.$watch('odeep.path', (val, oldVal) => {
      console.warn(val, oldVal);
    }, { deep: true });
  },
});
複製代碼

如何去經過諸如o.name的字符串訪問到vm.o.name呢? vm['o.name']固然是不行的,須要寫成vm['o']['name']這樣的形式。函數

function parsePath(path) {
  if (!/[\w.]$/.test(path)) {
    // 爲何要返回一個帶參數的函數呢? 提早告訴你,是爲了觸發被監聽對象的get方法(還記得上一篇文章的內容嗎)
    return function(obj) {};
  }

  const segs = path.split('.');
  // 想知道這裏爲何不用forEach嗎,試試在forEach裏使用return吧
  return function(obj) {
    for (let i = 0; i < segs.length; i += 1) {
      if (!obj) {
        return;
      }

      obj = obj[segs[i]];
    }

    return obj;
  };
}

const obj = {
  o: { name: 'a' },
};
console.assert(parsePath('o.name')(obj) === 'a');
複製代碼

關於觀察者模式

先讓咱們看看維基百科是怎麼說的:post

The observer pattern is a software design pattern in which an object, called the subject, maintains a list of its dependents, called observers, and notifies them automatically of any state changes, usually by calling one of their methods.ui

也就是說subject用來維護依賴列表, 每一個依賴都是一個observer。當依賴列表中的某一項發生了變化,就自動通知subject自身狀態的變動。this

讓咱們先拷貝一下上篇文章的內容, 注意註釋裏的內容!spa

function defineReactive(obj, key, val) {
  if (isPlainObject(val)) {
    observe(val);
  } else if (Array.isArray(val)) {
    dealAugment(val, dep);
    observeArray(val);
  }

  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get() {
      // 將依賴加入依賴列表
      return val;
    },

    set(newVal) {
      if (val !== newVal) {
        val = newVal;
        if (isPlainObject(newVal)) {
          observe(newVal);
        } else if (Array.isArray(newVal)) {
          dealAugment(newVal, dep);
          observeArray(newVal);
        }
        // 依賴通知subject自身狀態的改變,即調用callback
      }
    },
  });
}
複製代碼

可是callback在$watch函數中,如何傳遞給依賴, 並在被監聽對象該屬性變化時調用呢?prototype

咱們能夠利用一個全局變量(在這裏咱們稱它爲DepTarget),在訪問變量的時候設置爲$watch函數的callback, 並將這個callback存到一個集合裏,訪問結束後置空。同時須要注意的是,每一個$watch函數應該只對應一個觀察者(依賴)code

let DepTarget = null;
function $watch(obj, path, cb) {
  DepTarget = cb;
  // 訪問obj,自動調用get方法實現依賴注入
  parsePath(path)(obj);
  DepTarget = null;
}

function defineReactive(obj, key, val) {
  const deps = [];
  
  if (isPlainObject(val)) {
    observe(val);
  } else if (Array.isArray(val)) {
    // 傳遞dep,在push等函數觸發時notify, 可是咱們沒法訪問到舊的value值
    dealAugment(val, deps, val);
    observeArray(val);
  }

  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get() {
      if (DepTarget) {
        // 將callback存入集合
        deps.push(DepTarget);
      }
      return val;
    },

    set(newVal) {
      if (val !== newVal) {
        val = newVal;
        if (isPlainObject(newVal)) {
          observe(newVal);
        } else if (Array.isArray(newVal)) {
          dealAugment(newVal, deps, val);
          observeArray(newVal);
        }
        // 依賴通知subject自身狀態的改變,即調用callback
        deps.forEach((cb) => {
          if (typeof cb === 'function') {
            cb(val);
          }
        });
      }
    },
  });
}

function dealAugment(val, deps, val) {
  const arrayMethods = Object.create(Array.prototype);
  // 咱們以push方法爲例
  arrayMethods.push = function mutator(...args) {
    [].push.apply(this, args);
    // 依賴通知subject自身狀態的改變,即調用callback
    deps.forEach((cb) => {
      if (typeof cb === 'function') {
        cb(val);
      }
    });
  };

  // 若是瀏覽器實現了__proto__, 覆蓋原型對象
  if ('__proto__' in {}) {
    val.__proto__ = arrayMethods;
  } else {
    // 要是瀏覽器沒有實現__proto__, 覆蓋對象自己的該方法
    Object.defineProperty(val, 'push', {
      value: arrayMethods['push'],
      enumerable: true,
    });
  }
}
複製代碼

讓咱們試一試

const obj = {
  b: true,
  o: { name: 'obj' },
  a: ['a', 'b', 'c'],
  odeep: {
    path: {
      name: 'obj deep',
      value: [],
    },
  },
};
// observe等函數的實現請查看上一篇文章, 或是文章末尾的完整示例
observe(obj);

$watch(obj, 'b', (val, oldVal) => {
  console.warn('b watched: ', val, oldVal);
});
$watch(obj, 'a', (val, oldVal) => {
  console.warn('a watched: ',val, oldVal);
});
$watch(obj, 'odeep.path.value', (val, oldVal) => {
  console.warn('odeep.path.value watched: ',val, oldVal);
});
setTimeout(() => {
  // 固然不會有什麼問題, 不過你也發現了,咱們只能訪問到當前的value值,因此咱們須要一個對象來存儲舊的value值
  obj.b = false;
  obj.a.push('d');
  obj.o.name = 'new obj';
  obj.odeep.path.value.push(1);
}, 1000);
複製代碼

進階

關於Watcher和Dep

對於上述的問題,咱們須要抽象出兩個類: 一個用來存儲value值和callback(以及傳遞舊的value值),咱們把它稱爲watcher; 另外一個用來添加/存儲watcher,咱們把它稱爲dep。

let DepTarget = null;
class Dep {
  constructor() {
    this.watchers = new Set();
  }
  add(watcher) {
    this.watchers.add(watcher);
  }
  notify() {
    this.watchers.forEach((watcher) => {
      // 依賴通知自身狀態的改變, 即調用callback
      watcher.update();
    });
  }
}

class Watcher {
  constructor(obj, path, cb) {
    this.obj = obj;
    this.path = path;
    this.cb = cb;
    this.value = this.get();
  }
  get() {
    DepTarget = this;
    // 訪問obj,自動調用get方法實現依賴注入
    const val = parsePath(this.path)(this.obj);
    DepTarget = null;
    return val;
  }
  update() {
    const val = this.get();
    // 對於當前狀態改變的被監聽屬性纔會觸發callback
    if (val !== this.value) {
      const oldVal = this.value;
      this.value = val;
      // 傳遞val和oldVal
      this.cb(val, oldVal);
    }
  }
}

function defineReactive(obj, key, val) {
  const dep = new Dep();
  
  if (isPlainObject(val)) {
    observe(val);
  } else if (Array.isArray(val)) {
    // 傳遞dep,在push等函數觸發時notify
    dealAugment(val, dep);
    observeArray(val);
  }

  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get() {
      if (DepTarget) {
        dep.add(DepTarget);
      }
      return val;
    },

    set(newVal) {
      if (val !== newVal) {
        val = newVal;
        if (isPlainObject(newVal)) {
          observe(newVal);
        } else if (Array.isArray(newVal)) {
          dealAugment(newVal, dep);
          observeArray(newVal);
        }
        dep.notify();
      }
    },
  });
}

function dealAugment(val, dep) {
  const arrayMethods = Object.create(Array.prototype);
  // 咱們以push方法爲例
  arrayMethods.push = function mutator(...args) {
    [].push.apply(this, args);
    dep.notify();
  };

  // 若是瀏覽器實現了__proto__, 覆蓋原型對象
  if ('__proto__' in {}) {
    val.__proto__ = arrayMethods;
  } else {
    // 要是瀏覽器沒有實現__proto__, 覆蓋對象自己的該方法
    Object.defineProperty(val, 'push', {
      value: arrayMethods['push'],
      enumerable: true,
    });
  }
}
複製代碼

好了,這樣咱們就實現了監聽對象某個屬性的變化。

關於深度監聽

對於深度監聽,思路其實也是同樣的: 訪問obj,自動調用get方法實現依賴注入,咱們只須要遍歷訪問對象的全部屬性便可。

class Watcher {
  constructor(obj, path, cb, opts = {}) {
    this.obj = obj;
    this.path = path;
    this.cb = cb;
    this.deep = !!opts.deep;
    this.value = this.get();
  }

  get() {
    DepTarget = this;
    const val = parsePath(this.path)(this.obj);
    if (this.deep) {
      // 若當前路徑屬性值爲對象,訪問其全部屬性
      traverse(val);
    }
    DepTarget = null;
    return val;
  }
  
  update() {
    const val = this.get(); 
    if (val !== this.value || isObject(val)) {
      const oldVal = this.value;
      this.value = val;
      this.cb(val, oldVal);
    }
  }
}

function traverse(val) {
  let i = 0;
  if (Array.isArray(val)) {
    i = val.length;
    while (i--) { traverse(val[i]); }
  } else if (isPlainObject(val)) {
    const keys = Object.keys(val);
    i = keys.length;
    while (i--) { traverse(val[keys[i]]); }
  }
}
複製代碼

完整示例

let DepTarget = null;
class Dep {
  constructor() {
    this.watchers = new Set();
  }

  add(watcher) {
    this.watchers.add(watcher);
  }

  notify() {
    this.watchers.forEach((watcher) => {
      watcher.update();
    });
  }
}

class Watcher {
  constructor(obj, path, cb, opts = {}) {
    this.obj = obj;
    this.path = path;
    this.cb = cb;
    this.deep = !!opts.deep;
    this.value = this.get();
  }

  get() {
    DepTarget = this;
    const val = parsePath(this.path)(this.obj);
    if (this.deep) {
      traverse(val);
    }
    DepTarget = null;
    return val;
  }
  
  update() {
    const val = this.get(); 
    if (val !== this.value || isObject(val)) {
      const oldVal = this.value;
      this.value = val;
      this.cb(val, oldVal);
    }
  }
}

function parsePath(path) {
  if (!/[\w.]$/.test(path)) {
    return function(obj) {};
  }

  const segs = path.split('.');
  return function(obj) {
    for (let i = 0; i < segs.length; i += 1) {
      if (!obj) {
        return;
      }

      obj = obj[segs[i]];
    }

    return obj;
  };
}

function traverse(val) {
  let i = 0;
  if (Array.isArray(val)) {
    i = val.length;
    while (i--) { traverse(val[i]); }
  } else if (isPlainObject(val)) {
    const keys = Object.keys(val);
    i = keys.length;
    while (i--) { traverse(val[keys[i]]); }
  }
}

function isObject(val) {
  const type = typeof val;
  return val != null && (type === 'object' || type === 'function');
}

function isPlainObject(obj) {
  return ({}).toString.call(obj) === '[object Object]';
}

function defineReactive(obj, key, val) {
  const dep = new Dep();

  if (isPlainObject(val)) {
    observe(val);
  } else if (Array.isArray(val)) {
    dealAugment(val, dep);
    observeArray(val);
  }

  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get() {      
      if (DepTarget) {
        dep.add(DepTarget);
      }
      return val;
    },

    set(newVal) {
      if (val !== newVal) {
        val = newVal;
        if (isPlainObject(newVal)) {
          observe(newVal);
        } else if (Array.isArray(newVal)) {
          dealAugment(newVal, dep);
          observeArray(newVal);
        }
        dep.notify();
      }
    },
  });
}

function dealAugment(val, dep) {
  const arrayMethods = Object.create(Array.prototype);
  // 咱們以push方法爲例
  arrayMethods.push = function mutator(...args) {
    [].push.apply(this, args);
    dep.notify();
  };

  // 若是瀏覽器實現了__proto__, 覆蓋原型對象
  if ('__proto__' in {}) {
    val.__proto__ = arrayMethods;
  } else {
    // 要是瀏覽器沒有實現__proto__, 覆蓋對象自己的該方法
    Object.defineProperty(val, 'push', {
      value: arrayMethods['push'],
      enumerable: true,
    });
  }
}

function observeArray(obj) {
  obj.forEach((el) => {
    if (isPlainObject(el)) {
      observe(el);
    } else if (Array.isArray(el)) {
      observeArray(el);
    }
  });
}

function observe(obj) {
  Object.keys(obj).forEach((key) => {
    defineReactive(obj, key, obj[key]);
  });
}

const obj = {
  b: true,
  o: { name: 'obj' },
  a: ['a', 'b', 'c'],
  odeep: {
    path: {
      name: 'obj deep',
      value: [],
    },
  },
};
observe(obj);

new Watcher(obj, 'b', (val, oldVal) => {
  console.warn('b watched: ', val, oldVal);
});
new Watcher(obj, 'a', (val, oldVal) => {
  console.warn('a watched: ',val, oldVal);
});
new Watcher(obj, 'odeep.path.value', (val, oldVal) => {
  console.warn('odeep.path.value watched: ',val, oldVal);
});
new Watcher(obj, 'odeep', (val, oldVal) => {
  console.warn('odeep watched: ',val, oldVal);
}, { deep: true });

setTimeout(() => {
  obj.b = false;
  obj.a.push('d');
  obj.o.name = 'b';
  obj.odeep.path.value.push(1);
  obj.odeep.path.name = 'new obj deep';
}, 1000);
複製代碼

好了,以上就是關於如何監聽一個對象某個屬性的所有內容。

相關文章
相關標籤/搜索