| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362 |
- /* jshint proto:true */
- var Emitter = require('./emitter'),
- utils = require('./utils'),
- // cache methods
- typeOf = utils.typeOf,
- def = utils.defProtected,
- slice = Array.prototype.slice,
- // types
- OBJECT = 'Object',
- ARRAY = 'Array',
- // Array mutation methods to wrap
- methods = ['push','pop','shift','unshift','splice','sort','reverse'],
- // fix for IE + __proto__ problem
- // define methods as inenumerable if __proto__ is present,
- // otherwise enumerable so we can loop through and manually
- // attach to array instances
- hasProto = ({}).__proto__,
- // lazy load
- ViewModel
- // The proxy prototype to replace the __proto__ of
- // an observed array
- var ArrayProxy = Object.create(Array.prototype)
- // Define mutation interceptors so we can emit the mutation info
- methods.forEach(function (method) {
- def(ArrayProxy, method, function () {
- var result = Array.prototype[method].apply(this, arguments)
- this.__observer__.emit('mutate', null, this, {
- method: method,
- args: slice.call(arguments),
- result: result
- })
- return result
- }, !hasProto)
- })
- /**
- * Convenience method to remove an element in an Array
- * This will be attached to observed Array instances
- */
- function removeElement (index) {
- if (typeof index === 'function') {
- var i = this.length,
- removed = []
- while (i--) {
- if (index(this[i])) {
- removed.push(this.splice(i, 1)[0])
- }
- }
- return removed.reverse()
- } else {
- if (typeof index !== 'number') {
- index = this.indexOf(index)
- }
- if (index > -1) {
- return this.splice(index, 1)[0]
- }
- }
- }
- /**
- * Convenience method to replace an element in an Array
- * This will be attached to observed Array instances
- */
- function replaceElement (index, data) {
- if (typeof index === 'function') {
- var i = this.length,
- replaced = [],
- replacer
- while (i--) {
- replacer = index(this[i])
- if (replacer !== undefined) {
- replaced.push(this.splice(i, 1, replacer)[0])
- }
- }
- return replaced.reverse()
- } else {
- if (typeof index !== 'number') {
- index = this.indexOf(index)
- }
- if (index > -1) {
- return this.splice(index, 1, data)[0]
- }
- }
- }
- // Augment the ArrayProxy with convenience methods
- def(ArrayProxy, 'remove', removeElement, !hasProto)
- def(ArrayProxy, 'set', replaceElement, !hasProto)
- def(ArrayProxy, 'replace', replaceElement, !hasProto)
- /**
- * Watch an Object, recursive.
- */
- function watchObject (obj) {
- for (var key in obj) {
- convert(obj, key)
- }
- }
- /**
- * Watch an Array, overload mutation methods
- * and add augmentations by intercepting the prototype chain
- */
- function watchArray (arr) {
- var observer = arr.__observer__
- if (!observer) {
- observer = new Emitter()
- def(arr, '__observer__', observer)
- }
- if (hasProto) {
- arr.__proto__ = ArrayProxy
- } else {
- for (var key in ArrayProxy) {
- def(arr, key, ArrayProxy[key])
- }
- }
- }
- /**
- * Define accessors for a property on an Object
- * so it emits get/set events.
- * Then watch the value itself.
- */
- function convert (obj, key) {
- var keyPrefix = key.charAt(0)
- if (
- (keyPrefix === '$' || keyPrefix === '_') &&
- key !== '$index' &&
- key !== '$key' &&
- key !== '$value'
- ) {
- return
- }
- // emit set on bind
- // this means when an object is observed it will emit
- // a first batch of set events.
- var observer = obj.__observer__,
- values = observer.values
- init(obj[key])
- Object.defineProperty(obj, key, {
- get: function () {
- var value = values[key]
- // only emit get on tip values
- if (pub.shouldGet && typeOf(value) !== OBJECT) {
- observer.emit('get', key)
- }
- return value
- },
- set: function (newVal) {
- var oldVal = values[key]
- unobserve(oldVal, key, observer)
- copyPaths(newVal, oldVal)
- // an immediate property should notify its parent
- // to emit set for itself too
- init(newVal, true)
- }
- })
- function init (val, propagate) {
- values[key] = val
- observer.emit('set', key, val, propagate)
- if (Array.isArray(val)) {
- observer.emit('set', key + '.length', val.length)
- }
- observe(val, key, observer)
- }
- }
- /**
- * Check if a value is watchable
- */
- function isWatchable (obj) {
- ViewModel = ViewModel || require('./viewmodel')
- var type = typeOf(obj)
- return (type === OBJECT || type === ARRAY) && !(obj instanceof ViewModel)
- }
- /**
- * When a value that is already converted is
- * observed again by another observer, we can skip
- * the watch conversion and simply emit set event for
- * all of its properties.
- */
- function emitSet (obj) {
- var type = typeOf(obj),
- emitter = obj && obj.__observer__
- if (type === ARRAY) {
- emitter.emit('set', 'length', obj.length)
- } else if (type === OBJECT) {
- var key, val
- for (key in obj) {
- val = obj[key]
- emitter.emit('set', key, val)
- emitSet(val)
- }
- }
- }
- /**
- * Make sure all the paths in an old object exists
- * in a new object.
- * So when an object changes, all missing keys will
- * emit a set event with undefined value.
- */
- function copyPaths (newObj, oldObj) {
- if (typeOf(oldObj) !== OBJECT || typeOf(newObj) !== OBJECT) {
- return
- }
- var path, type, oldVal, newVal
- for (path in oldObj) {
- if (!(path in newObj)) {
- oldVal = oldObj[path]
- type = typeOf(oldVal)
- if (type === OBJECT) {
- newVal = newObj[path] = {}
- copyPaths(newVal, oldVal)
- } else if (type === ARRAY) {
- newObj[path] = []
- } else {
- newObj[path] = undefined
- }
- }
- }
- }
- /**
- * walk along a path and make sure it can be accessed
- * and enumerated in that object
- */
- function ensurePath (obj, key) {
- var path = key.split('.'), sec
- for (var i = 0, d = path.length - 1; i < d; i++) {
- sec = path[i]
- if (!obj[sec]) {
- obj[sec] = {}
- if (obj.__observer__) convert(obj, sec)
- }
- obj = obj[sec]
- }
- if (typeOf(obj) === OBJECT) {
- sec = path[i]
- if (!(sec in obj)) {
- obj[sec] = undefined
- if (obj.__observer__) convert(obj, sec)
- }
- }
- }
- /**
- * Observe an object with a given path,
- * and proxy get/set/mutate events to the provided observer.
- */
- function observe (obj, rawPath, parentOb) {
- if (!isWatchable(obj)) return
- var path = rawPath ? rawPath + '.' : '',
- alreadyConverted = !!obj.__observer__,
- childOb
- if (!alreadyConverted) {
- def(obj, '__observer__', new Emitter())
- }
- childOb = obj.__observer__
- childOb.values = childOb.values || utils.hash()
- // setup proxy listeners on the parent observer.
- // we need to keep reference to them so that they
- // can be removed when the object is un-observed.
- parentOb.proxies = parentOb.proxies || {}
- var proxies = parentOb.proxies[path] = {
- get: function (key) {
- parentOb.emit('get', path + key)
- },
- set: function (key, val, propagate) {
- parentOb.emit('set', path + key, val)
- // also notify observer that the object itself changed
- // but only do so when it's a immediate property. this
- // avoids duplicate event firing.
- if (rawPath && propagate) {
- parentOb.emit('set', rawPath, obj, true)
- }
- },
- mutate: function (key, val, mutation) {
- // if the Array is a root value
- // the key will be null
- var fixedPath = key ? path + key : rawPath
- parentOb.emit('mutate', fixedPath, val, mutation)
- // also emit set for Array's length when it mutates
- var m = mutation.method
- if (m !== 'sort' && m !== 'reverse') {
- parentOb.emit('set', fixedPath + '.length', val.length)
- }
- }
- }
- // attach the listeners to the child observer.
- // now all the events will propagate upwards.
- childOb
- .on('get', proxies.get)
- .on('set', proxies.set)
- .on('mutate', proxies.mutate)
- if (alreadyConverted) {
- // for objects that have already been converted,
- // emit set events for everything inside
- emitSet(obj)
- } else {
- var type = typeOf(obj)
- if (type === OBJECT) {
- watchObject(obj)
- } else if (type === ARRAY) {
- watchArray(obj)
- }
- }
- }
- /**
- * Cancel observation, turn off the listeners.
- */
- function unobserve (obj, path, observer) {
- if (!obj || !obj.__observer__) return
- path = path ? path + '.' : ''
- var proxies = observer.proxies[path]
- if (!proxies) return
- // turn off listeners
- obj.__observer__
- .off('get', proxies.get)
- .off('set', proxies.set)
- .off('mutate', proxies.mutate)
- // remove reference
- observer.proxies[path] = null
- }
- var pub = module.exports = {
- // whether to emit get events
- // only enabled during dependency parsing
- shouldGet : false,
- observe : observe,
- unobserve : unobserve,
- ensurePath : ensurePath,
- convert : convert,
- copyPaths : copyPaths,
- watchArray : watchArray
- }
|