Evan You 11 роки тому
батько
коміт
0ad7f54602

+ 1 - 0
.jshintrc

@@ -11,6 +11,7 @@
   "laxbreak": true,
   "evil": true,
   "eqnull": true,
+  "proto": true,
   "globals": {
     "console": true
   }

+ 11 - 4
gruntfile.js

@@ -23,18 +23,24 @@ module.exports = function (grunt) {
         frameworks: ['jasmine', 'commonjs'],
         files: [
           'src/**/*.js',
-          'test/unit/specs/*.js'
+          'test/unit/**/*.js'
         ],
         preprocessors: {
           'src/**/*.js': ['commonjs'],
-          'test/unit/specs/*.js': ['commonjs']
+          'test/unit/**/*.js': ['commonjs']
         },
         singleRun: true
       },
       browsers: {
         options: {
-           browsers: ['Chrome', 'Firefox'],
-           reporters: ['progress']
+          browsers: ['Chrome', 'Firefox'],
+          reporters: ['progress']
+        }
+      },
+      phantom: {
+        options: {
+          browsers: ['PhantomJS'],
+          reporters: ['progress']
         }
       }
     },
@@ -72,6 +78,7 @@ module.exports = function (grunt) {
   })
 
   grunt.registerTask('unit', ['karma:browsers'])
+  grunt.registerTask('phantom', ['karma:phantom'])
   grunt.registerTask('watch', ['browserify:watch'])
   grunt.registerTask('build', ['browserify:build'])
 

+ 83 - 0
src/observer/array-augmentations.js

@@ -0,0 +1,83 @@
+var _ = require('../util')
+var slice = [].slice
+var arrayAugmentations = Object.create(Array.prototype)
+
+/**
+ * Intercept mutating methods and emit events
+ */
+
+;[
+  'push',
+  'pop',
+  'shift',
+  'unshift',
+  'splice',
+  'sort',
+  'reverse'
+]
+.forEach(function (method) {
+  // cache original method
+  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
+    var inserted, removed
+
+    switch (method) {
+      case 'push':
+      case 'unshift':
+        inserted = args
+        break
+      case 'pop':
+      case 'shift':
+        removed = [result]
+        break
+      case 'splice':
+        inserted = args.slice(2)
+        removed = result
+        break
+    }
+
+    ob.link(inserted)
+    ob.unlink(removed)
+    // empty key, value is self
+    ob.emit('mutate', '', this, {
+      method   : method,
+      args     : args,
+      result   : result,
+      inserted : inserted,
+      removed  : removed
+    })
+  })
+})
+
+/**
+ * Swap the element at the given index with a new value
+ * and emits corresponding event.
+ *
+ * @param {Number} index
+ * @param {*} val
+ * @return {*} - replaced element
+ */
+
+_.define(arrayAugmentations, '$set', function (index, val) {
+  if (index >= this.length) {
+    this.length = index + 1
+  }
+  return this.splice(index, 1, val)[0]
+})
+
+/**
+ * Convenience method to remove the element at given index.
+ *
+ * @param {Number} index
+ * @param {*} val
+ */
+
+_.define(arrayAugmentations, '$remove', function (index) {
+  if (index > -1) {
+    return this.splice(index, 1)[0]
+  }
+})

+ 0 - 0
src/observer/array.js


+ 36 - 0
src/observer/object-augmentations.js

@@ -0,0 +1,36 @@
+var _ = require('../util')
+var objectAgumentations = Object.create(Object.prototype)
+
+/**
+ * Add a new property to an observed object
+ * and emits corresponding event
+ *
+ * @param {String} key
+ * @param {*} val
+ * @public
+ */
+
+_.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)
+})
+
+/**
+ * Deletes a property from an observed object
+ * and emits corresponding event
+ *
+ * @param {String} key
+ * @public
+ */
+
+_.define(objectAgumentations, '$delete', function (key) {
+  if (!this.hasOwnProperty(key)) return
+  // trigger set events
+  this[key] = undefined
+  delete this[key]
+  this.$observer.emit('delete', key)
+})
+
+module.exports = objectAgumentations

+ 0 - 0
src/observer/object.js


+ 128 - 29
src/observer/observer.js

@@ -1,75 +1,174 @@
 var _ = require('../util')
 var Emitter = require('../emitter')
+var arrayAugmentations = require('./array-augmentations')
+var objectAugmentations = require('./object-augmentations')
+
+// Type enums
+
+var ARRAY  = 0
+var OBJECT = 1
 
 /**
  * Observer class that are attached to each observed
  * object. They are essentially event emitters, but can
- * connect to each other and relay the events up the nested
- * object chain.
+ * connect to each other like nodes to map the hierarchy
+ * of data objects. Once connected, detected change events
+ * can propagate up the nested object chain.
+ *
+ * The constructor can be invoked without arguments to
+ * create a value-less observer that simply listens to
+ * other observers.
  *
  * @constructor
  * @extends Emitter
- * @private
+ * @param {Array|Object} [value]
+ * @param {Number} [type]
  */
 
-function Observer () {
+function Observer (value, type) {
   Emitter.call(this)
-  this.connections = Object.create(null)
+  this.value = value
+  this.type = type
+  this.initiated = false
+  this.children = Object.create(null)
+  if (value) {
+    _.define(value, '$observer', this)
+  }
 }
 
 var p = Observer.prototype = Object.create(Emitter.prototype)
 
 /**
- * Observe an object of unkown type.
- *
- * @param {*} obj
- * @return {Boolean} - returns true if successfully observed.
+ * Initialize the observation based on value type.
+ * Should only be called once.
  */
 
-p.observe = function (obj) {
-  if (obj && obj.$observer) {
-    // already observed
-    return
-  }
-  if (_.isArray(obj)) {
-    this.observeArray(obj)
-    return true
+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)
   }
-  if (_.isObject(obj)) {
-    this.observeObject(obj)
-    return true
+  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.
+ *
+ * @param {Object} obj
+ */
+
+p.walk = function (obj) {
+  var key, val, ob
+  for (key in obj) {
+    val = obj[key]
+    ob = Observer.create(val)
+    if (ob) {
+      this.add(key, ob)
+      if (ob.initiated) {
+        this.deliver(key, val)
+      } else {
+        ob.init()
+      }
+    } else {
+      this.convert(key, val)
+    }
   }
 }
 
 /**
- * Connect to another Observer instance,
+ * Link a list of items to the observer's value Array.
+ * When any of these items emit change event, the Array will be notified.
+ *
+ * @param {Array} items
+ */
+
+p.link = function (items) {
+  
+}
+
+/**
+ * Unlink the items from the observer's value Array.
+ *
+ * @param {Array} items
+ */
+
+p.unlink = function (items) {
+  
+}
+
+/**
+ * Convert a tip value into getter/setter so we can emit the events
+ * when the property is accessed/changed.
+ *
+ * @param {String} key
+ * @param {*} val
+ */
+
+p.convert = function (key, val) {
+  
+}
+
+/**
+ * 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.
+ *
+ * @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.
  *
- * @param {Observer} target
  * @param {String} key
+ * @param {Observer} ob
  */
 
-p.connect = function (target, key) {
+p.add = function (key, ob) {
 
 }
 
 /**
- * Disconnect from a connected target Observer.
+ * Remove a child observer.
  *
- * @param {Observer} target
  * @param {String} key
+ * @param {Observer} ob
  */
 
-p.disconnect = function (target, key) {
+p.remove = function (key, ob) {
   
 }
 
 /**
- * Mixin Array and Object observe methods
+ * Attempt to create an observer instance for a value,
+ * returns the new observer if successfully observed,
+ * or the existing observer if the value already has one.
+ *
+ * @param {*} value
+ * @return {Observer}
+ * @static
  */
 
-_.mixin(p, require('./array'))
-_.mixin(p, require('./object'))
+Observer.create = function (value) {
+  if (value && value.$observer) {
+    return value.$observer
+  } if (_.isArray(value)) {
+    return new Observer(value, ARRAY)
+  } else if (_.isObject(value)) {
+    return new Observer(value, OBJECT)
+  }
+}
 
 module.exports = Observer

+ 27 - 5
src/util.js

@@ -37,7 +37,7 @@ exports.isArray = function (obj) {
 }
 
 /**
- * Define a readonly, in-enumerable property
+ * Define a non-enumerable property
  *
  * @param {Object} obj
  * @param {String} key
@@ -46,9 +46,31 @@ exports.isArray = function (obj) {
 
 exports.define = function (obj, key, val) {
   Object.defineProperty(obj, key, {
-    value: val,
-    enumerable: false,
-    writable: false,
-    configurable: true
+    value        : val,
+    enumerable   : false,
+    writable     : true,
+    configurable : true
   })
+}
+
+/**
+ * Augment an target Object or Array by either
+ * intercepting the prototype chain using __proto__,
+ * or copy over property descriptors
+ *
+ * @param {Object|Array} target
+ * @param {Object} proto
+ */
+
+if ('__proto__' in {}) {
+  exports.augment = function (target, proto) {
+    target.__proto__ = proto
+  }
+} else {
+  exports.augment = function (target, proto) {
+    Object.getOwnPropertyNames(proto).forEach(function (key) {
+      var descriptor = Object.getOwnPropertyDescriptor(proto, key)
+      Object.defineProperty(target, key, descriptor)
+    })
+  }
 }

+ 12 - 0
test/unit/observer.js

@@ -0,0 +1,12 @@
+var Observer = require('../../src/observer/observer')
+
+describe('Observer', function () {
+
+  it('should work', function () {
+    var obj = {}
+    var ob = Observer.create(obj)
+    ob.init()
+    expect(obj.$add).toBeDefined()
+  })
+
+})

+ 0 - 7
test/unit/specs/main.js

@@ -1,7 +0,0 @@
-var Vue = require('../../../src/vue.js')
-
-describe('test', function () {
-  it('should work', function () {
-    expect(Vue).toBeDefined()
-  })
-})