From 0ad7f54602b41d2dfa82e2368fef1ee2639fecd4 Mon Sep 17 00:00:00 2001 From: Evan You Date: Wed, 9 Jul 2014 19:08:01 -0400 Subject: [PATCH] working on observer --- .jshintrc | 1 + gruntfile.js | 15 ++- src/observer/array-augmentations.js | 83 +++++++++++++ src/observer/array.js | 0 src/observer/object-augmentations.js | 36 ++++++ src/observer/object.js | 0 src/observer/observer.js | 179 +++++++++++++++++++++------ src/util.js | 32 ++++- test/unit/observer.js | 12 ++ test/unit/specs/main.js | 7 -- 10 files changed, 309 insertions(+), 56 deletions(-) create mode 100644 src/observer/array-augmentations.js delete mode 100644 src/observer/array.js create mode 100644 src/observer/object-augmentations.js delete mode 100644 src/observer/object.js create mode 100644 test/unit/observer.js delete mode 100644 test/unit/specs/main.js diff --git a/.jshintrc b/.jshintrc index 4a4b1305a..ee94095c3 100644 --- a/.jshintrc +++ b/.jshintrc @@ -11,6 +11,7 @@ "laxbreak": true, "evil": true, "eqnull": true, + "proto": true, "globals": { "console": true } diff --git a/gruntfile.js b/gruntfile.js index 8aabb4a92..4b8bc4ae4 100644 --- a/gruntfile.js +++ b/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']) diff --git a/src/observer/array-augmentations.js b/src/observer/array-augmentations.js new file mode 100644 index 000000000..5307307fb --- /dev/null +++ b/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] + } +}) \ No newline at end of file diff --git a/src/observer/array.js b/src/observer/array.js deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/observer/object-augmentations.js b/src/observer/object-augmentations.js new file mode 100644 index 000000000..afd93b3fa --- /dev/null +++ b/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 \ No newline at end of file diff --git a/src/observer/object.js b/src/observer/object.js deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/observer/observer.js b/src/observer/observer.js index bdda5c2fa..7734121fb 100644 --- a/src/observer/observer.js +++ b/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 +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 (_.isArray(obj)) { - this.observeArray(obj) - return true - } - 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, - * capture its get/set/mutate events and relay the events - * while prepending a key segment to the path. + * 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 {Observer} target - * @param {String} key + * @param {Array} items */ -p.connect = function (target, key) { - -} - -/** - * Disconnect from a connected target Observer. - * - * @param {Observer} target - * @param {String} key - */ - -p.disconnect = function (target, key) { +p.link = function (items) { } /** - * Mixin Array and Object observe methods + * Unlink the items from the observer's value Array. + * + * @param {Array} items */ -_.mixin(p, require('./array')) -_.mixin(p, require('./object')) +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 {String} key + * @param {Observer} ob + */ + +p.add = function (key, ob) { + +} + +/** + * Remove a child observer. + * + * @param {String} key + * @param {Observer} ob + */ + +p.remove = function (key, ob) { + +} + +/** + * 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 + */ + +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 \ No newline at end of file diff --git a/src/util.js b/src/util.js index 12ac9b2e3..cc87511fe 100644 --- a/src/util.js +++ b/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) + }) + } } \ No newline at end of file diff --git a/test/unit/observer.js b/test/unit/observer.js new file mode 100644 index 000000000..953e89a41 --- /dev/null +++ b/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() + }) + +}) \ No newline at end of file diff --git a/test/unit/specs/main.js b/test/unit/specs/main.js deleted file mode 100644 index ab39913ae..000000000 --- a/test/unit/specs/main.js +++ /dev/null @@ -1,7 +0,0 @@ -var Vue = require('../../../src/vue.js') - -describe('test', function () { - it('should work', function () { - expect(Vue).toBeDefined() - }) -}) \ No newline at end of file