working on observer

This commit is contained in:
Evan You 2014-07-09 19:08:01 -04:00
parent 55bfa2f2e7
commit 0ad7f54602
10 changed files with 309 additions and 56 deletions

View File

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

View File

@ -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'])

View File

@ -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]
}
})

View File

View File

@ -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

View File

View File

@ -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,
* 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

View File

@ -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
test/unit/observer.js Normal file
View File

@ -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()
})
})

View File

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