Skip to content
8 changes: 8 additions & 0 deletions src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@ module.exports = {

warnExpressionErrors: true,

/**
* Whether or not to handle fully object properties which
* are already backed by getters and seters. Depending on
* use case and environment, this might introduce non-neglible
* performance penalties.
*/
convertAllProperties: false,

/**
* Internal flag to indicate the delimiters have been
* changed.
Expand Down
25 changes: 21 additions & 4 deletions src/observer/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
var _ = require('../util')
var config = require('../config')
var Dep = require('./dep')
var arrayMethods = require('./array')
var arrayKeys = Object.getOwnPropertyNames(arrayMethods)
Expand Down Expand Up @@ -172,11 +173,27 @@ function copyAugment (target, src, keys) {

function defineReactive (obj, key, val) {
var dep = new Dep()
var childOb = Observer.create(val)

var target = {
val: val
}

if (config.convertAllProperties) {
var property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.get && property.set) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just noticed it's possible for a property to only have getters, or only have setters. We should handle all three possible situations here. Probably need to procedurally build up the descriptor based on the presence of the getter and the setter.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please bear with me, thanks for the effort so far :)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No worries it's all good feedback.

I think the getter/setter stuff makes sense, but might as well also make it safely handle 'configurable' being false. For now I'll make it so it just leaves everything alone; let me know if you think some other behavior makes more sense.

Object.defineProperty(target, 'val', {
get: _.bind(property.get, obj),
set: _.bind(property.set, obj)
})
}
}

var childOb = Observer.create(target.val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function metaGetter () {
var val = target.val
if (Dep.target) {
dep.depend()
if (childOb) {
Expand All @@ -189,11 +206,11 @@ function defineReactive (obj, key, val) {
}
}
}
return val
return target.val
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be fine to just return val here as well.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure how I missed replacing this one out; I'll add a test condition which enforces only calling the underlying getter once.

},
set: function metaSetter (newVal) {
if (newVal === val) return
val = newVal
if (newVal === target.val) return
target.val = newVal
childOb = Observer.create(newVal)
dep.notify()
}
Expand Down
74 changes: 74 additions & 0 deletions test/unit/specs/observer/observer_spec.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
var Observer = require('../../../../src/observer')
var Dep = require('../../../../src/observer/dep')
var _ = require('../../../../src/util')
var config = require('../../../../src/config')

describe('Observer', function () {

Expand Down Expand Up @@ -38,6 +39,40 @@ describe('Observer', function () {
expect(ob2).toBe(ob)
})

it('create on already observed object', function () {
var previousConvertAllProperties = config.convertAllProperties
config.convertAllProperties = true

// on object
var obj = {}
var val = 0
Object.defineProperty(obj, 'a', {
configurable: true,
enumerable: true,
get: function () {
return val
},
set: function (v) {
val = v
}
})

var ob = Observer.create(obj)
expect(ob instanceof Observer).toBe(true)
expect(ob.value).toBe(obj)
expect(obj.__ob__).toBe(ob)

// should return existing ob on already observed objects
var ob2 = Observer.create(obj)
expect(ob2).toBe(ob)

// should call underlying setter
obj.a = 10
expect(val).toBe(10)

config.convertAllProperties = previousConvertAllProperties
})

it('create on array', function () {
// on object
var arr = [{}, {}]
Expand Down Expand Up @@ -82,6 +117,45 @@ describe('Observer', function () {
expect(watcher.update.calls.count()).toBe(3)
})

it('observing object prop change on defined property', function () {
var previousConvertAllProperties = config.convertAllProperties
config.convertAllProperties = true

var obj = { val: 2 }
Object.defineProperty(obj, 'a', {
configurable: true,
enumerable: true,
get: function () {
return this.val
},
set: function (v) {
this.val = v
return this.val
}
})

Observer.create(obj)
// mock a watcher!
var watcher = {
deps: [],
addDep: function (dep) {
this.deps.push(dep)
dep.addSub(this)
},
update: jasmine.createSpy()
}
// collect dep
Dep.target = watcher
expect(obj.a).toBe(2) // Make sure 'this' is preserved
Dep.target = null
obj.a = 3
expect(obj.val).toBe(3) // make sure 'setter' was called
obj.val = 5
expect(obj.a).toBe(5) // make sure 'getter' was called

config.convertAllProperties = previousConvertAllProperties
})

it('observing set/delete', function () {
var obj = { a: 1 }
var ob = Observer.create(obj)
Expand Down