Ver Fonte

more observer work

Evan You há 11 anos atrás
pai
commit
3acc354f64

+ 1 - 1
src/emitter.js

@@ -21,7 +21,7 @@ var p = Emitter.prototype
 
 p.on = function (event, fn) {
   this._cbs = this._cbs || {}
-  ;(this._cbs[event] = this._cbs[event] || [])
+  ;(this._cbs[event] || (this._cbs[event] = []))
     .push(fn)
   return this
 }

+ 11 - 3
src/observer/array-augmentations.js

@@ -20,6 +20,7 @@ var arrayAugmentations = Object.create(Array.prototype)
   var original = Array.prototype[method]
   // define wrapped method
   _.define(arrayAugmentations, method, function () {
+    
     var args = slice.call(arguments)
     var result = original.apply(this, args)
     var ob = this.$observer
@@ -40,10 +41,17 @@ var arrayAugmentations = Object.create(Array.prototype)
         break
     }
 
-    ob.link(inserted)
-    ob.unlink(removed)
+    // link/unlink added/removed elements
+    if (inserted) ob.link(inserted)
+    if (removed) ob.unlink(removed)
+
+    // emit length change
+    if (inserted || removed) {
+      ob.notify('set', 'length', this.length)
+    }
+
     // empty path, value is the Array itself
-    ob.emit('mutate', [], this, {
+    ob.notify('mutate', '', this, {
       method   : method,
       args     : args,
       result   : result,

+ 2 - 2
src/observer/object-augmentations.js

@@ -14,7 +14,7 @@ _.define(objectAgumentations, '$add', function (key, val) {
   if (this.hasOwnProperty(key)) return
   this[key] = val
   this.$observer.convert(key, val)
-  this.$observer.emit('add', key, val)
+  this.$observer.notify('added', key, val)
 })
 
 /**
@@ -30,7 +30,7 @@ _.define(objectAgumentations, '$delete', function (key) {
   // trigger set events
   this[key] = undefined
   delete this[key]
-  this.$observer.emit('delete', key)
+  this.$observer.notify('deleted', key)
 })
 
 module.exports = objectAgumentations

+ 70 - 120
src/observer/observer.js

@@ -30,10 +30,16 @@ function Observer (value, type) {
   Emitter.call(this)
   this.value = value
   this.type = type
-  this.initiated = false
-  this.adaptors = Object.create(null)
+  this.parents = null
   if (value) {
     _.define(value, '$observer', this)
+    if (type === ARRAY) {
+      _.augment(value, arrayAugmentations)
+      this.link(value)
+    } else if (type === OBJECT) {
+      _.augment(value, objectAugmentations)
+      this.walk(value)
+    }
   }
 }
 
@@ -69,23 +75,6 @@ Observer.create = function (value) {
   }
 }
 
-/**
- * Initialize the observation based on value type.
- * Should only be called once.
- */
-
-p.init = function () {
-  var value = this.value
-  if (this.type === ARRAY) {
-    _.augment(value, arrayAugmentations)
-    this.link(value)
-  } else if (this.type === OBJECT) {
-    _.augment(value, objectAugmentations)
-    this.walk(value)
-  }
-  this.initiated = true
-}
-
 /**
  * Walk through each property, converting them and adding them as child.
  * This method should only be called when value type is Object.
@@ -103,6 +92,30 @@ p.walk = function (obj) {
   }
 }
 
+/**
+ * Link a list of Array items to the observer.
+ *
+ * @param {Array} items
+ */
+
+p.link = function (items) {
+  for (var i = 0, l = items.length; i < l; i++) {
+    this.observe(i, items[i])
+  }
+}
+
+/**
+ * Unlink a list of Array items from the observer.
+ *
+ * @param {Array} items
+ */
+
+p.unlink = function (items) {
+  for (var i = 0, l = items.length; i < l; i++) {
+    this.unobserve(items[i], i)
+  }
+}
+
 /**
  * If a property is observable,
  * create an Observer for it and add it as a child.
@@ -116,17 +129,11 @@ p.walk = function (obj) {
 p.observe = function (key, val) {
   var ob = Observer.create(val)
   if (ob) {
-    this.add(key, ob)
-    if (ob.initiated) {
-      this.deliver(key, val)
-    } else {
-      ob.init()
-    }
-  }
-  // emit an initial set event
-  this.emit('set', key, val)
-  if (_.isArray(val)) {
-    this.emit('set', key + delimiter + 'length', val.length)
+    // register self as a parent of the child observer.
+    (ob.parents || (ob.parents = [])).push({
+      ob: this,
+      key: key
+    })
   }
 }
 
@@ -134,13 +141,22 @@ p.observe = function (key, val) {
  * Unobserve a property.
  * If it has an observer, remove it from children.
  *
- * @param {String} key
  * @param {*} val
  */
 
-p.unobserve = function (key, val) {
+p.unobserve = function (val) {
   if (val && val.$observer) {
-    this.remove(key, val.$observer)
+    var parents = val.$observer.parents
+    var i = parents.length
+    while (i--) {
+      if (parents[i].ob === this) {
+        parents.splice(i, 1)
+        break
+      }
+    }
+    if (!parents.length) {
+      val.$observer.parents = null
+    }
   }
 }
 
@@ -163,109 +179,43 @@ p.convert = function (key, val) {
     enumerable: true,
     configurable: true,
     get: function () {
-      ob.emit('get', key)
+      ob.notify('get', key)
       return val
     },
     set: function (newVal) {
       if (newVal === val) return
-      ob.unobserve(key, val)
+      ob.unobserve(val)
       ob.observe(key, newVal)
+      ob.notify('set', key, newVal)
+      if (_.isArray(newVal)) {
+        ob.notify('set', key + delimiter + 'length', newVal.length)
+      }
       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
- */
-
-p.link = function (items) {
-  
-}
-
-/**
- * Unlink the items from the observer's value Array.
- *
- * @param {Array} items
- */
-
-p.unlink = function (items) {
-  
-}
-
-/**
- * Walk through an already observed object and emit its tip values.
- * This is necessary because newly observed objects emit their values
- * during init; for already observed ones we can skip the initialization,
- * but still need to emit the values.
- *
- * If called with no arguments, it delivers set events for the root value.
- *
- * @param {String} [key]
- * @param {*} [val]
- */
-
-p.deliver = function (key, val) {
-  
-}
-
-/**
- * Add a child observer for a property key,
- * capture its get/set/mutate events and relay the events
- * while prepending a key segment to the path.
+ * Emit event on self and recursively notify all parents.
  *
- * @param {String} key
- * @param {Observer} ob
+ * @param {String} event
+ * @param {String} path
+ * @param {*} val
+ * @param {Object|undefined} mutation
  */
 
-p.add = function (key, ob) {
-  var self = this
-  var base = key + delimiter
-  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
+p.notify = function (event, path, val, mutation) {
+  this.emit(event, path, val, mutation)
+  if (!this.parents) return
+  for (var i = 0, l = this.parents.length; i < l; i++) {
+    var parent = this.parents[i]
+    var ob = parent.ob
+    var key = parent.key
+    var parentPath = path
+      ? key + delimiter + path
       : key
-    self.emit('mutate', path, val, mutation)
-    // also emit for length
-    self.emit('set', path + delimiter + 'length', val.length)
+    ob.notify(event, parentPath, val, mutation)
   }
-
-  ob.on('get', adaptors.get)
-    .on('set', adaptors.set)
-    .on('mutate', adaptors.mutate)
-}
-
-/**
- * Remove a child observer.
- *
- * @param {String} key
- * @param {Observer} 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)
 }
 
 module.exports = Observer

+ 67 - 11
test/unit/observer.js

@@ -1,39 +1,95 @@
 var Observer = require('../../src/observer/observer')
 var delimiter = Observer.pathDelimiter
 
-describe('Observer', function () {
+function path (p) {
+  return p.replace(/\./g, '\b')
+}
 
-  var obj, ob, spy
+describe('Observer', function () {
 
+  var spy
   beforeEach(function () {
-    obj = {
+    spy = jasmine.createSpy()
+  })
+
+  it('get', function () {
+    var obj = {
       a: 1,
       b: {
         c: 2
       }
     }
-    ob = Observer.create(obj)
-    ob.init()
-    spy = jasmine.createSpy()
+    var ob = Observer.create(obj)
+    ob.on('get', spy)
+
+    var t = obj.a
+    expect(spy).toHaveBeenCalledWith('a', undefined, undefined)
+    expect(spy.callCount).toEqual(1)
+
+    t = obj.b.c
+    expect(spy).toHaveBeenCalledWith('b', undefined, undefined)
+    expect(spy).toHaveBeenCalledWith(path('b.c'), undefined, undefined)
+    expect(spy.callCount).toEqual(3)
   })
 
-  it('should emit set events', function () {
+  it('set', function () {
+    var obj = {
+      a: 1,
+      b: {
+        c: 2
+      }
+    }
+    var ob = Observer.create(obj)
     ob.on('set', spy)
+
     obj.a = 3
     expect(spy).toHaveBeenCalledWith('a', 3, undefined)
+    expect(spy.callCount).toEqual(1)
+
     obj.b.c = 4
-    expect(spy).toHaveBeenCalledWith('b' + delimiter + 'c', 4, undefined)
+    expect(spy).toHaveBeenCalledWith(path('b.c'), 4, undefined)
+    expect(spy.callCount).toEqual(2)
+
+    var newB = { c: 5 }
+    obj.b = newB
+    expect(spy).toHaveBeenCalledWith('b', newB, undefined)
+    expect(spy.callCount).toEqual(3)
+  })
+
+  it('array get', function () {
+    var obj = {
+      arr: [{a:1}, {a:2}]
+    }
+    var ob = Observer.create(obj)
+    ob.on('get', spy)
+
+    var t = obj.arr[0].a
+    expect(spy).toHaveBeenCalledWith(path('arr'), undefined, undefined)
+    expect(spy).toHaveBeenCalledWith(path('arr.0.a'), undefined, undefined)
+    expect(spy.callCount).toEqual(2)
+  })
+
+  it('array set', function () {
+    // body...
+  })
+
+  it('array mutate', function () {
+    // body...
+  })
+
+  it('object.$add', function () {
+    // body...
   })
 
-  it('should emit get events', function () {
+  it('object.$delete', function () {
     // body...
   })
 
-  it('should emit mutation events on Array mutation', function () {
+  it('array.$set', function () {
     // body...
   })
 
-  it('should emit ', function () {
+  it('array.$remove', function () {
     // body...
   })