Browse Source

observer object

Evan You 12 years ago
parent
commit
30f67ab140
2 changed files with 127 additions and 29 deletions
  1. 108 25
      src/observer/observer.js
  2. 19 4
      test/unit/observer.js

+ 108 - 25
src/observer/observer.js

@@ -30,7 +30,7 @@ function Observer (value, type) {
   this.value = value
   this.value = value
   this.type = type
   this.type = type
   this.initiated = false
   this.initiated = false
-  this.children = Object.create(null)
+  this.adaptors = Object.create(null)
   if (value) {
   if (value) {
     _.define(value, '$observer', this)
     _.define(value, '$observer', this)
   }
   }
@@ -63,53 +63,105 @@ p.init = function () {
  */
  */
 
 
 p.walk = function (obj) {
 p.walk = function (obj) {
-  var key, val, ob
+  var key, val
   for (key in obj) {
   for (key in obj) {
     val = obj[key]
     val = obj[key]
-    ob = Observer.create(val)
-    if (ob) {
-      this.add(key, ob)
-      if (ob.initiated) {
-        this.deliver(key, val)
-      } else {
-        ob.init()
-      }
+    this.observe(key, val)
+    this.convert(key, val)
+  }
+}
+
+/**
+ * If a property is observable,
+ * create an Observer for it and add it as a child.
+ * This method is called only on properties observed
+ * for the first time.
+ *
+ * @param {String} key
+ * @param {*} val
+ */
+
+p.observe = function (key, val) {
+  var ob = Observer.create(val)
+  if (ob) {
+    this.add(key, ob)
+    if (ob.initiated) {
+      this.deliver(key, val)
     } else {
     } else {
-      this.convert(key, val)
+      ob.init()
     }
     }
   }
   }
+  // emit an initial set event
+  this.emit('set', key, val)
+  if (_.isArray(val)) {
+    this.emit('set', key + '.length', val.length)
+  }
 }
 }
 
 
 /**
 /**
- * Link a list of items to the observer's value Array.
- * When any of these items emit change event, the Array will be notified.
+ * Unobserve a property.
+ * If it has an observer, remove it from children.
  *
  *
- * @param {Array} items
+ * @param {String} key
+ * @param {*} val
  */
  */
 
 
-p.link = function (items) {
-  
+p.unobserve = function (key, val) {
+  if (val && val.$observer) {
+    this.remove(key, val.$observer)
+  }
 }
 }
 
 
 /**
 /**
- * Unlink the items from the observer's value Array.
+ * Convert a tip value into getter/setter so we can emit
+ * the events when the property is accessed/changed.
+ * Properties prefixed with `$` or `_` are ignored.
+ *
+ * @param {String} key
+ * @param {*} val
+ */
+
+p.convert = function (key, val) {
+  var prefix = key.charAt(0)
+  if (prefix === '$' || prefix === '_') {
+    return
+  }
+  var ob = this
+  Object.defineProperty(this.value, key, {
+    enumerable: true,
+    configurable: true,
+    get: function () {
+      ob.emit('get', key)
+      return val
+    },
+    set: function (newVal) {
+      if (newVal === val) return
+      ob.unobserve(key, val)
+      ob.observe(key, newVal)
+      val = newVal
+    }
+  })
+}
+
+/**
+ * Link a list of items to the observer's value Array.
+ * When any of these items emit change event, the Array will be notified.
+ * This method should only be called when value type is Array.
  *
  *
  * @param {Array} items
  * @param {Array} items
  */
  */
 
 
-p.unlink = function (items) {
+p.link = function (items) {
   
   
 }
 }
 
 
 /**
 /**
- * Convert a tip value into getter/setter so we can emit the events
- * when the property is accessed/changed.
+ * Unlink the items from the observer's value Array.
  *
  *
- * @param {String} key
- * @param {*} val
+ * @param {Array} items
  */
  */
 
 
-p.convert = function (key, val) {
+p.unlink = function (items) {
   
   
 }
 }
 
 
@@ -137,7 +189,34 @@ p.deliver = function (key, val) {
  */
  */
 
 
 p.add = function (key, ob) {
 p.add = function (key, ob) {
+  var self = this
+  var base = key + '.'
+  var adaptors = this.adaptors[key] = {}
+
+  adaptors.get = function (path) {
+    path = base + path
+    self.emit('get', path)
+  }
 
 
+  adaptors.set = function (path, val) {
+    path = base + path
+    self.emit('set', path, val)
+  }
+
+  adaptors.mutate = function (path, val, mutation) {
+    // if path is empty string, the mutation
+    // comes directly from an Array
+    path = path
+      ? base + path
+      : key
+    self.emit('mutate', path, val, mutation)
+    // also emit for length
+    self.emit('set', path + '.length', val.length)
+  }
+
+  ob.on('get', adaptors.get)
+    .on('set', adaptors.set)
+    .on('mutate', adaptors.mutate)
 }
 }
 
 
 /**
 /**
@@ -148,7 +227,11 @@ p.add = function (key, ob) {
  */
  */
 
 
 p.remove = function (key, ob) {
 p.remove = function (key, ob) {
-  
+  var adaptors = this.adaptors[key]
+  this.adaptors[key] = null
+  ob.off('get', adaptors.get)
+    .off('set', adaptors.set)
+    .off('mutate', adaptors.mutate)
 }
 }
 
 
 /**
 /**
@@ -157,7 +240,7 @@ p.remove = function (key, ob) {
  * or the existing observer if the value already has one.
  * or the existing observer if the value already has one.
  *
  *
  * @param {*} value
  * @param {*} value
- * @return {Observer}
+ * @return {Observer|undefined}
  * @static
  * @static
  */
  */
 
 

+ 19 - 4
test/unit/observer.js

@@ -2,11 +2,26 @@ var Observer = require('../../src/observer/observer')
 
 
 describe('Observer', function () {
 describe('Observer', function () {
 
 
-  it('should work', function () {
-    var obj = {}
-    var ob = Observer.create(obj)
+  var obj, ob, spy
+
+  beforeEach(function () {
+    obj = {
+      a: 1,
+      b: {
+        c: 2
+      }
+    }
+    ob = Observer.create(obj)
     ob.init()
     ob.init()
-    expect(obj.$add).toBeDefined()
+    spy = jasmine.createSpy()
+  })
+
+  it('should emit set events', function () {
+    ob.on('set', spy)
+    obj.a = 3
+    expect(spy).toHaveBeenCalledWith('a', 3, undefined)
+    obj.b.c = 4
+    expect(spy).toHaveBeenCalledWith('b.c', 4, undefined)
   })
   })
 
 
 })
 })