From d78df3101711ee85b4c3ebf5d488a0d4e9e89ad6 Mon Sep 17 00:00:00 2001 From: Evan You Date: Wed, 14 Aug 2013 16:47:21 -0400 Subject: [PATCH] rename internal, optimize memory usage for GC --- component.json | 4 +- examples/todomvc/js/app.js | 30 ++--- src/binding.js | 114 +++++++++-------- src/{seed.js => compiler.js} | 216 ++++++++++++++++++--------------- src/deps-parser.js | 36 +++--- src/directive-parser.js | 19 ++- src/directives/each.js | 42 +++---- src/directives/index.js | 4 +- src/directives/on.js | 30 ++--- src/main.js | 22 ++-- src/utils.js | 2 +- src/{scope.js => viewmodel.js} | 62 +++++----- 12 files changed, 317 insertions(+), 264 deletions(-) rename src/{seed.js => compiler.js} (56%) rename src/{scope.js => viewmodel.js} (54%) diff --git a/component.json b/component.json index 8abcf734f..b6c70d952 100644 --- a/component.json +++ b/component.json @@ -6,8 +6,8 @@ "src/main.js", "src/config.js", "src/utils.js", - "src/seed.js", - "src/scope.js", + "src/compiler.js", + "src/viewmodel.js", "src/binding.js", "src/directive-parser.js", "src/text-parser.js", diff --git a/examples/todomvc/js/app.js b/examples/todomvc/js/app.js index 5be9348ac..6c85bc5c3 100644 --- a/examples/todomvc/js/app.js +++ b/examples/todomvc/js/app.js @@ -12,6 +12,7 @@ Seed.controller('todos', { // initializer, reserved init: function () { + window.app = this // listen for hashtag change this.updateFilter() this.$on('filterchange', this.updateFilter.bind(this)) @@ -29,9 +30,9 @@ Seed.controller('todos', { return this.total - this.remaining }}, - // dynamic context computed property using info from target scope + // dynamic context computed property using info from target viewmodel todoFiltered: {get: function (ctx) { - return filters[this.filter](ctx.scope) + return filters[this.filter]({ completed: ctx.vm.completed }) }}, // dynamic context computed property using info from target element @@ -45,10 +46,11 @@ Seed.controller('todos', { return this.remaining === 0 }, set: function (value) { - this.remaining = value ? 0 : this.total this.todos.forEach(function (todo) { todo.completed = value }) + this.remaining = value ? 0 : this.total + todoStorage.save(this.todos) } }, @@ -64,32 +66,32 @@ Seed.controller('todos', { }, removeTodo: function (e) { - this.todos.remove(e.scope) - this.remaining -= e.scope.completed ? 0 : 1 + this.todos.remove(e.vm) + this.remaining -= e.vm.completed ? 0 : 1 todoStorage.save(this.todos) }, toggleTodo: function (e) { - this.remaining += e.scope.completed ? -1 : 1 + this.remaining += e.vm.completed ? -1 : 1 todoStorage.save(this.todos) }, editTodo: function (e) { - this.beforeEditCache = e.scope.title - e.scope.editing = true + this.beforeEditCache = e.vm.title + e.vm.editing = true }, doneEdit: function (e) { - if (!e.scope.editing) return - e.scope.editing = false - e.scope.title = e.scope.title.trim() - if (!e.scope.title) this.removeTodo(e) + if (!e.vm.editing) return + e.vm.editing = false + e.vm.title = e.vm.title.trim() + if (!e.vm.title) this.removeTodo(e) todoStorage.save(this.todos) }, cancelEdit: function (e) { - e.scope.editing = false - e.scope.title = this.beforeEditCache + e.vm.editing = false + e.vm.title = this.beforeEditCache }, removeCompleted: function () { diff --git a/src/binding.js b/src/binding.js index 79651860d..baf267729 100644 --- a/src/binding.js +++ b/src/binding.js @@ -5,17 +5,17 @@ var utils = require('./utils'), /* * Binding class. * - * each property on the scope has one corresponding Binding object + * each property on the viewmodel has one corresponding Binding object * which has multiple directive instances on the DOM * and multiple computed property dependents */ -function Binding (seed, key) { - this.seed = seed - this.scope = seed.scope - this.key = key +function Binding (compiler, key) { + this.compiler = compiler + this.vm = compiler.vm + this.key = key var path = key.split('.') - this.inspect(utils.getNestedValue(seed.scope, path)) - this.def(seed.scope, path) + this.inspect(utils.getNestedValue(compiler.vm, path)) + this.def(compiler.vm, path) this.instances = [] this.subs = [] this.deps = [] @@ -27,85 +27,82 @@ var BindingProto = Binding.prototype * Pre-process a passed in value based on its type */ BindingProto.inspect = function (value) { - var type = utils.typeOf(value), - self = this + var type = utils.typeOf(value) // preprocess the value depending on its type if (type === 'Object') { if (value.get) { var l = Object.keys(value).length if (l === 1 || (l === 2 && value.set)) { - self.isComputed = true // computed property - value.get = value.get.bind(self.scope) - if (value.set) value.set = value.set.bind(self.scope) + this.isComputed = true // computed property + this.rawGet = value.get + value.get = value.get.bind(this.vm) + if (value.set) value.set = value.set.bind(this.vm) } } } else if (type === 'Array') { + value = utils.dump(value) utils.watchArray(value) - value.on('mutate', function () { - self.pub() - }) + value.on('mutate', this.pub.bind(this)) } - self.value = value + this.value = value } /* - * Define getter/setter for this binding on scope + * Define getter/setter for this binding on viewmodel * recursive for nested objects */ -BindingProto.def = function (scope, path) { - var self = this, - seed = self.seed, - key = path[0] +BindingProto.def = function (viewmodel, path) { + var key = path[0] if (path.length === 1) { // here we are! at the end of the path! // define the real value accessors. - def(scope, key, { - get: function () { + def(viewmodel, key, { + get: (function () { if (observer.isObserving) { - observer.emit('get', self) + observer.emit('get', this) } - return self.isComputed - ? self.value.get({ - el: seed.el, - scope: seed.scope + return this.isComputed + ? this.value.get({ + el: this.compiler.el, + vm: this.compiler.vm }) - : self.value - }, - set: function (value) { - if (self.isComputed) { + : this.value + }).bind(this), + set: (function (value) { + if (this.isComputed) { // computed properties cannot be redefined // no need to call binding.update() here, // as dependency extraction has taken care of that - if (self.value.set) { - self.value.set(value) + if (this.value.set) { + this.value.set(value) } - } else if (value !== self.value) { - self.update(value) + } else if (value !== this.value) { + this.update(value) } - } + }).bind(this) }) } else { // we are not there yet!!! - // create an intermediate subscope + // create an intermediate object // which also has its own getter/setters - var subScope = scope[key] - if (!subScope) { - subScope = {} - def(scope, key, { - get: function () { - return subScope - }, - set: function (value) { - // when the subScope is given a new value, + var nestedObject = viewmodel[key] + if (!nestedObject) { + nestedObject = {} + def(viewmodel, key, { + get: (function () { + return this + }).bind(nestedObject), + set: (function (value) { + // when the nestedObject is given a new value, // copy everything over to trigger the setters for (var prop in value) { - subScope[prop] = value[prop] + this[prop] = value[prop] } - } + }).bind(nestedObject) }) } // recurse - this.def(subScope, path.slice(1)) + this.def(nestedObject, path.slice(1)) } } @@ -116,7 +113,7 @@ BindingProto.update = function (value) { this.inspect(value) var i = this.instances.length while (i--) { - this.instances[i].update(value) + this.instances[i].update(this.value) } this.pub() } @@ -132,6 +129,21 @@ BindingProto.refresh = function () { } } +BindingProto.unbind = function () { + var i = this.instances.length + while (i--) { + this.instances[i].unbind() + } + i = this.deps.length + var subs + while (i--) { + subs = this.deps[i].subs + subs.splice(subs.indexOf(this), 1) + } + if (Array.isArray(this.value)) this.value.off('mutate') + this.vm = this.compiler = this.pubs = this.subs = this.instances = null +} + /* * Notify computed properties that depend on this binding * to update themselves diff --git a/src/seed.js b/src/compiler.js similarity index 56% rename from src/seed.js rename to src/compiler.js index 19abefc34..003db75f6 100644 --- a/src/seed.js +++ b/src/compiler.js @@ -1,5 +1,5 @@ var config = require('./config'), - Scope = require('./scope'), + ViewModel = require('./viewmodel'), Binding = require('./binding'), DirectiveParser = require('./directive-parser'), TextParser = require('./text-parser'), @@ -11,26 +11,26 @@ var slice = Array.prototype.slice, eachAttr = config.prefix + '-each' /* - * The main ViewModel class - * scans a node and parse it to populate data bindings + * The DOM compiler + * scans a DOM node and compile bindings for a ViewModel */ -function Seed (el, options) { - - config.log('\ncreated new Seed instance.\n') +function Compiler (el, options) { + config.log('\ncreated new Compiler instance.\n') if (typeof el === 'string') { el = document.querySelector(el) } - this.el = el - el.seed = this - this._bindings = {} - this._watchers = {} - this._listeners = [] + this.el = el + el.compiler = this + this.bindings = {} + this.directives = [] + this.watchers = {} + this.listeners = [] // list of computed properties that need to parse dependencies for - this._computed = [] + this.computed = [] // list of bindings that has dynamic context dependencies - this._contextBindings = [] + this.contextBindings = [] // copy options options = options || {} @@ -48,71 +48,71 @@ function Seed (el, options) { data = data || {} el.removeAttribute(dataAttr) - // if the passed in data is the scope of a Seed instance, + // if the passed in data is the viewmodel of a Compiler instance, // make a copy from it - if (data.$seed instanceof Seed) { + if (data instanceof ViewModel) { data = data.$dump() } - // check if there is a controller associated with this seed + // check if there is a controller associated with this compiler var ctrlID = el.getAttribute(ctrlAttr), controller if (ctrlID) { el.removeAttribute(ctrlAttr) controller = config.controllers[ctrlID] if (controller) { - this._controller = controller + this.controller = controller } else { config.warn('controller "' + ctrlID + '" is not defined.') } } - // create the scope object - // if the controller has an extended scope contructor, use it; - // otherwise, use the original scope constructor. - var ScopeConstructor = (controller && controller.ExtendedScope) || Scope, - scope = this.scope = new ScopeConstructor(this, options) + // create the viewmodel object + // if the controller has an extended viewmodel contructor, use it; + // otherwise, use the original viewmodel constructor. + var VMCtor = (controller && controller.ExtendedVM) || ViewModel, + viewmodel = this.vm = new VMCtor(this, options) // copy data for (var key in data) { - scope[key] = data[key] + viewmodel[key] = data[key] } // apply controller initialize function if (controller && controller.init) { - controller.init.call(scope) + controller.init.call(viewmodel) } // now parse the DOM - this._compileNode(el, true) + this.compileNode(el, true) - // for anything in scope but not binded in DOM, create bindings for them - for (key in scope) { - if (key.charAt(0) !== '$' && !this._bindings[key]) { - this._createBinding(key) + // for anything in viewmodel but not binded in DOM, create bindings for them + for (key in viewmodel) { + if (key.charAt(0) !== '$' && !this.bindings[key]) { + this.createBinding(key) } } // extract dependencies for computed properties - if (this._computed.length) depsParser.parse(this._computed) - delete this._computed + if (this.computed.length) depsParser.parse(this.computed) + this.computed = null // extract dependencies for computed properties with dynamic context - if (this._contextBindings.length) this._bindContexts(this._contextBindings) - delete this._contextBindings + if (this.contextBindings.length) this.bindContexts(this.contextBindings) + this.contextBindings = null } // for better compression -var SeedProto = Seed.prototype +var CompilerProto = Compiler.prototype /* * Compile a DOM node (recursive) */ -SeedProto._compileNode = function (node, root) { - var seed = this +CompilerProto.compileNode = function (node, root) { + var compiler = this if (node.nodeType === 3) { // text node - seed._compileTextNode(node) + compiler.compileTextNode(node) } else if (node.nodeType === 1) { @@ -125,14 +125,14 @@ SeedProto._compileNode = function (node, root) { directive = DirectiveParser.parse(eachAttr, eachExp) if (directive) { directive.el = node - seed._bind(directive) + compiler.bindDirective(directive) } } else if (ctrlExp && !root) { // nested controllers - new Seed(node, { + new Compiler(node, { child: true, - parentSeed: seed + parentCompiler: compiler }) } else { // normal node @@ -153,7 +153,7 @@ SeedProto._compileNode = function (node, root) { if (directive) { valid = true directive.el = node - seed._bind(directive) + compiler.bindDirective(directive) } } if (valid) node.removeAttribute(attr.name) @@ -162,7 +162,7 @@ SeedProto._compileNode = function (node, root) { // recursively compile childNodes if (node.childNodes.length) { - slice.call(node.childNodes).forEach(seed._compileNode, seed) + slice.call(node.childNodes).forEach(compiler.compileNode, compiler) } } } @@ -171,10 +171,10 @@ SeedProto._compileNode = function (node, root) { /* * Compile a text node */ -SeedProto._compileTextNode = function (node) { +CompilerProto.compileTextNode = function (node) { var tokens = TextParser.parse(node) if (!tokens) return - var seed = this, + var compiler = this, dirname = config.prefix + '-text', el, token, directive for (var i = 0, l = tokens.length; i < l; i++) { @@ -184,7 +184,7 @@ SeedProto._compileTextNode = function (node) { directive = DirectiveParser.parse(dirname, token.key) if (directive) { directive.el = el - seed._bind(directive) + compiler.bindDirective(directive) } } else { el.nodeValue = token @@ -195,29 +195,56 @@ SeedProto._compileTextNode = function (node) { } /* - * Add a directive instance to the correct binding & scope + * Create binding and attach getter/setter for a key to the viewmodel object */ -SeedProto._bind = function (directive) { +CompilerProto.createBinding = function (key) { + config.log(' created binding: ' + key) + var binding = new Binding(this, key) + this.bindings[key] = binding + if (binding.isComputed) this.computed.push(binding) + return binding +} + +/* + * Add a directive instance to the correct binding & viewmodel + */ +CompilerProto.bindDirective = function (directive) { + + this.directives.push(directive) + directive.compiler = this + directive.vm = this.vm var key = directive.key, - seed = directive.seed = this + compiler = this // deal with each block if (this.each) { if (key.indexOf(this.eachPrefix) === 0) { key = directive.key = key.replace(this.eachPrefix, '') } else { - seed = this.parentSeed + compiler = this.parentCompiler } } // deal with nesting - seed = traceOwnerSeed(directive, seed) - var binding = seed._bindings[key] || seed._createBinding(key) + compiler = traceOwnerCompiler(directive, compiler) + var binding = compiler.bindings[key] || compiler.createBinding(key) binding.instances.push(directive) directive.binding = binding + // for newly inserted sub-VMs (each items), need to bind deps + // because they didn't get processed when the parent compiler + // was binding dependencies. + var i, dep + if (binding.contextDeps) { + i = binding.contextDeps.length + while (i--) { + dep = this.bindings[binding.contextDeps[i]] + dep.subs.push(directive) + } + } + // invoke bind hook if exists if (directive.bind) { directive.bind(binding.value) @@ -230,81 +257,74 @@ SeedProto._bind = function (directive) { } } -/* - * Create binding and attach getter/setter for a key to the scope object - */ -SeedProto._createBinding = function (key) { - config.log(' created binding: ' + key) - var binding = new Binding(this, key) - this._bindings[key] = binding - if (binding.isComputed) this._computed.push(binding) - return binding -} - /* * Process subscriptions for computed properties that has * dynamic context dependencies */ -SeedProto._bindContexts = function (bindings) { - var i = bindings.length, j, binding, depKey, dep +CompilerProto.bindContexts = function (bindings) { + var i = bindings.length, j, k, binding, depKey, dep, ins while (i--) { binding = bindings[i] j = binding.contextDeps.length while (j--) { depKey = binding.contextDeps[j] - dep = this._bindings[depKey] - dep.subs.push(binding) + k = binding.instances.length + while (k--) { + ins = binding.instances[k] + dep = ins.compiler.bindings[depKey] + dep.subs.push(ins) + } } } } -/* - * Call unbind() of all directive instances - * to remove event listeners, destroy child seeds, etc. - */ -SeedProto._unbind = function () { - var i, ins, key, listener - // unbind all bindings - for (key in this._bindings) { - ins = this._bindings[key].instances - i = ins.length - while (i--) { - if (ins[i].unbind) ins[i].unbind() - } - } - // remove all listeners on eventbus - i = this._listeners.length - while (i--) { - listener = this._listeners[i] - eventbus.off(listener.event, listener.handler) - } -} - /* * Unbind and remove element */ -SeedProto._destroy = function () { - this._unbind() +CompilerProto.destroy = function () { + var i, key, dir, listener, inss + // remove all directives that are instances of external bindings + i = this.directives.length + while (i--) { + dir = this.directives[i] + if (dir.binding.compiler !== this) { + inss = dir.binding.instances + inss.splice(inss.indexOf(dir), 1) + } + dir.unbind() + } + // remove all listeners on eventbus + i = this.listeners.length + while (i--) { + listener = this.listeners[i] + eventbus.off(listener.event, listener.handler) + } + // unbind all bindings + for (key in this.bindings) { + this.bindings[key].unbind() + } + // remove el + this.el.compiler = null this.el.parentNode.removeChild(this.el) } // Helpers -------------------------------------------------------------------- /* - * determine which scope a key belongs to based on nesting symbols + * determine which viewmodel a key belongs to based on nesting symbols */ -function traceOwnerSeed (key, seed) { +function traceOwnerCompiler (key, compiler) { if (key.nesting) { var levels = key.nesting - while (seed.parentSeed && levels--) { - seed = seed.parentSeed + while (compiler.parentCompiler && levels--) { + compiler = compiler.parentCompiler } } else if (key.root) { - while (seed.parentSeed) { - seed = seed.parentSeed + while (compiler.parentCompiler) { + compiler = compiler.parentCompiler } } - return seed + return compiler } -module.exports = Seed \ No newline at end of file +module.exports = Compiler \ No newline at end of file diff --git a/src/deps-parser.js b/src/deps-parser.js index f2f505bf9..cea81ee52 100644 --- a/src/deps-parser.js +++ b/src/deps-parser.js @@ -4,7 +4,7 @@ var Emitter = require('emitter'), var dummyEl = document.createElement('div'), ARGS_RE = /^function\s*?\((.+?)\)/, - SCOPE_RE_STR = '\\.scope\\.[\\.A-Za-z0-9_][\\.A-Za-z0-9_$]*', + SCOPE_RE_STR = '\\.vm\\.[\\.A-Za-z0-9_][\\.A-Za-z0-9_$]*', noop = function () {} /* @@ -21,7 +21,7 @@ function catchDeps (binding) { }) parseContextDependency(binding) binding.value.get({ - scope: createDummyScope(binding), + vm: createDummyVM(binding), el: dummyEl }) observer.off('get') @@ -37,31 +37,37 @@ function filterDeps (binding) { while (i--) { dep = binding.deps[i] if (!dep.deps.length) { - config.log(' └─' + dep.key) + config.log(' └─ ' + dep.key) dep.subs.push(binding) } else { binding.deps.splice(i, 1) } } + var ctdeps = binding.contextDeps + if (!ctdeps || !config.debug) return + i = ctdeps.length + while (i--) { + config.log(' └─ ctx:' + ctdeps[i]) + } } /* * We need to invoke each binding's getter for dependency parsing, - * but we don't know what sub-scope properties the user might try + * but we don't know what sub-viewmodel properties the user might try * to access in that getter. To avoid thowing an error or forcing * the user to guard against an undefined argument, we staticly * analyze the function to extract any possible nested properties - * the user expects the target scope to possess. They are all assigned + * the user expects the target viewmodel to possess. They are all assigned * a noop function so they can be invoked with no real harm. */ -function createDummyScope (binding) { - var scope = {}, +function createDummyVM (binding) { + var viewmodel = {}, deps = binding.contextDeps - if (!deps) return scope + if (!deps) return viewmodel var i = binding.contextDeps.length, j, level, key, path while (i--) { - level = scope + level = viewmodel path = deps[i].split('.') j = 0 while (j < path.length) { @@ -71,20 +77,20 @@ function createDummyScope (binding) { j++ } } - return scope + return viewmodel } /* * Extract context dependency paths */ function parseContextDependency (binding) { - var fn = binding.value.get, + var fn = binding.rawGet, str = fn.toString(), args = str.match(ARGS_RE) if (!args) return null - var argRE = new RegExp(args[1] + SCOPE_RE_STR, 'g'), - matches = str.match(argRE), - base = args[1].length + 7 + var depsRE = new RegExp(args[1] + SCOPE_RE_STR, 'g'), + matches = str.match(depsRE), + base = args[1].length + 4 if (!matches) return null var i = matches.length, deps = [], dep @@ -95,7 +101,7 @@ function parseContextDependency (binding) { } } binding.contextDeps = deps - binding.seed._contextBindings.push(binding) + binding.compiler.contextBindings.push(binding) } module.exports = { diff --git a/src/directive-parser.js b/src/directive-parser.js index e1186d184..e98bb6edc 100644 --- a/src/directive-parser.js +++ b/src/directive-parser.js @@ -26,7 +26,11 @@ function Directive (directiveName, expression, oneway) { this._update = definition.update for (prop in definition) { if (prop !== 'update') { - this[prop] = definition[prop] + if (prop === 'unbind') { + this._unbind = definition[prop] + } else { + this[prop] = definition[prop] + } } } } @@ -62,11 +66,11 @@ DirProto.update = function (value) { * called when a dependency has changed */ DirProto.refresh = function () { - // pass element and scope info to the getter + // pass element and viewmodel info to the getter // enables powerful context-aware bindings var value = this.value.get({ el: this.el, - scope: this.seed.scope + vm: this.vm }) if (value === this.computedValue) return this.computedValue = value @@ -135,6 +139,15 @@ DirProto.parseKey = function (rawKey) { this.key = key } +/* + * unbind noop, to be overwritten by definitions + */ +DirProto.unbind = function (update) { + if (!this.el) return + if (this._unbind) this._unbind(update) + if (!update) this.vm = this.el = this.compiler = this.binding = null +} + /* * parse a filter expression */ diff --git a/src/directives/each.js b/src/directives/each.js index ba9bb2aab..87ca762dd 100644 --- a/src/directives/each.js +++ b/src/directives/each.js @@ -1,4 +1,5 @@ -var config = require('../config') +var config = require('../config'), + utils = require('../utils') /* * Mathods that perform precise DOM manipulation @@ -59,16 +60,16 @@ var mutationHandlers = { }, sort: function () { - var i, l = this.collection.length, scope + var i, l = this.collection.length, viewmodel for (i = 0; i < l; i++) { - scope = this.collection[i] - scope.$index = i - this.container.insertBefore(scope.$el, this.ref) + viewmodel = this.collection[i] + viewmodel.$index = i + this.container.insertBefore(viewmodel.$el, this.ref) } } } -mutationHandlers.reverse = mutationHandlers.sort +//mutationHandlers.reverse = mutationHandlers.sort module.exports = { @@ -84,7 +85,6 @@ module.exports = { update: function (collection) { this.unbind(true) - if (!Array.isArray(collection)) return this.collection = collection // attach an object to container to hold handlers @@ -92,10 +92,9 @@ module.exports = { // listen for collection mutation events // the collection has been augmented during Binding.set() - var self = this - collection.on('mutate', function (mutation) { - mutationHandlers[mutation.method].call(self, mutation) - }) + collection.on('mutate', (function (mutation) { + mutationHandlers[mutation.method].call(this, mutation) + }).bind(this)) // create child-seeds and append to DOM for (var i = 0, l = collection.length; i < l; i++) { @@ -106,16 +105,16 @@ module.exports = { buildItem: function (ref, data, index) { var node = this.el.cloneNode(true) this.container.insertBefore(node, ref) - var Seed = require('../seed'), - spore = new Seed(node, { + var Compiler = require('../compiler'), + spore = new Compiler(node, { each: true, eachPrefix: this.arg + '.', - parentSeed: this.seed, + parentCompiler: this.compiler, index: index, data: data, delegator: this.container }) - this.collection[index] = spore.scope + this.collection[index] = spore.vm }, updateIndexes: function () { @@ -125,20 +124,19 @@ module.exports = { } }, - unbind: function (reset) { - if (this.collection && this.collection.length) { - var i = this.collection.length, - fn = reset ? '_destroy' : '_unbind' + unbind: function () { + if (this.collection) { + this.collection.off('mutate') + var i = this.collection.length while (i--) { - this.collection[i].$seed[fn]() + this.collection[i].$destroy() } - this.collection = null } var ctn = this.container, handlers = ctn.sd_dHandlers for (var key in handlers) { ctn.removeEventListener(handlers[key].event, handlers[key]) } - delete ctn.sd_dHandlers + ctn.sd_dHandlers = null } } \ No newline at end of file diff --git a/src/directives/index.js b/src/directives/index.js index 3a5c02017..5a5d5c7ae 100644 --- a/src/directives/index.js +++ b/src/directives/index.js @@ -51,7 +51,7 @@ module.exports = { if (this.oneway) return var el = this.el, self = this this.change = function () { - self.seed.scope[self.key] = el.value + self.compiler.vm[self.key] = el.value } el.addEventListener('keyup', this.change) }, @@ -69,7 +69,7 @@ module.exports = { if (this.oneway) return var el = this.el, self = this this.change = function () { - self.seed.scope[self.key] = el.checked + self.compiler.vm[self.key] = el.checked } el.addEventListener('change', this.change) }, diff --git a/src/directives/on.js b/src/directives/on.js index 5bd3b95ac..7c1fa4bfb 100644 --- a/src/directives/on.js +++ b/src/directives/on.js @@ -13,29 +13,29 @@ module.exports = { expectFunction : true, bind: function () { - if (this.seed.each) { + if (this.compiler.each) { // attach an identifier to the el // so it can be matched during event delegation this.el[this.expression] = true - // attach the owner scope of this directive - this.el.sd_scope = this.seed.scope + // attach the owner viewmodel of this directive + this.el.sd_viewmodel = this.vm } }, update: function (handler) { - this.unbind() + this.unbind(true) if (!handler) return - var seed = this.seed, - event = this.arg, - ownerScope = this.binding.seed.scope + var compiler = this.compiler, + event = this.arg, + ownerVM = this.binding.vm - if (seed.each && event !== 'blur' && event !== 'blur') { + if (compiler.each && event !== 'blur' && event !== 'blur') { // for each blocks, delegate for better performance // focus and blur events dont bubble so exclude them - var delegator = seed.delegator, + var delegator = compiler.delegator, identifier = this.expression, dHandler = delegator.sd_dHandlers[identifier] @@ -46,8 +46,8 @@ module.exports = { var target = delegateCheck(e.target, delegator, identifier) if (target) { e.el = target - e.scope = target.sd_scope - handler.call(ownerScope, e) + e.vm = target.sd_viewmodel + handler.call(ownerVM, e) } } dHandler.event = event @@ -58,15 +58,17 @@ module.exports = { // a normal, single element handler this.handler = function (e) { e.el = e.currentTarget - e.scope = seed.scope - handler.call(seed.scope, e) + e.vm = compiler.vm + handler.call(compiler.vm, e) } this.el.addEventListener(event, this.handler) } }, - unbind: function () { + unbind: function (update) { this.el.removeEventListener(this.arg, this.handler) + this.handler = null + if (!update) this.el.sd_viewmodel = null } } \ No newline at end of file diff --git a/src/main.js b/src/main.js index 48b5eb47c..9e00a964e 100644 --- a/src/main.js +++ b/src/main.js @@ -1,6 +1,6 @@ var config = require('./config'), - Seed = require('./seed'), - Scope = require('./scope'), + Compiler = require('./compiler'), + ViewModel = require('./viewmodel'), directives = require('./directives'), filters = require('./filters'), textParser = require('./text-parser'), @@ -40,12 +40,12 @@ api.data = function (id, data) { */ api.controller = function (id, properties) { if (!properties) return controllers[id] - // create a subclass of Scope that has the extension methods mixed-in - var ExtendedScope = function () { - Scope.apply(this, arguments) + // create a subclass of ViewModel that has the extension methods mixed-in + var ExtendedVM = function () { + ViewModel.apply(this, arguments) } - var p = ExtendedScope.prototype = Object.create(Scope.prototype) - p.constructor = ExtendedScope + var p = ExtendedVM.prototype = Object.create(ViewModel.prototype) + p.constructor = ExtendedVM for (var prop in properties) { if (prop !== 'init') { p[prop] = properties[prop] @@ -53,7 +53,7 @@ api.controller = function (id, properties) { } controllers[id] = { init: properties.init, - ExtendedScope: ExtendedScope + ExtendedVM: ExtendedVM } } @@ -91,12 +91,12 @@ api.config = function (opts) { * Compile a single element */ api.compile = function (el) { - return new Seed(el).scope + return new Compiler(el).vm } /* * Bootstrap the whole thing - * by creating a Seed instance for top level nodes + * by creating a Compiler instance for top level nodes * that has either sd-controller or sd-data */ api.bootstrap = function (opts) { @@ -107,7 +107,7 @@ api.bootstrap = function (opts) { dataSlt = '[' + config.prefix + '-data]' /* jshint boss: true */ while (el = document.querySelector(ctrlSlt) || document.querySelector(dataSlt)) { - new Seed(el) + new Compiler(el) } booted = true } diff --git a/src/utils.js b/src/utils.js index 2ff1fe05a..9fa9ee266 100644 --- a/src/utils.js +++ b/src/utils.js @@ -31,7 +31,7 @@ function dump (val) { } else if (type === 'Object') { if (val.get) { // computed property return val.get() - } else { // object / child scope + } else { // object / child viewmodel var ret = {}, prop for (var key in val) { prop = val[key] diff --git a/src/scope.js b/src/viewmodel.js similarity index 54% rename from src/scope.js rename to src/viewmodel.js index 8b0967e2b..4f4c9d882 100644 --- a/src/scope.js +++ b/src/viewmodel.js @@ -1,26 +1,25 @@ var utils = require('./utils') /* - * Scope is the ViewModel/whatever exposed to the user - * that holds data, computed properties, event handlers + * ViewModel exposed to the user that holds data, + * computed properties, event handlers * and a few reserved methods */ -function Scope (seed, options) { - this.$seed = seed - this.$el = seed.el +function ViewModel (compiler, options) { + this.$compiler = compiler + this.$el = compiler.el this.$index = options.index - this.$parent = options.parentSeed && options.parentSeed.scope - this.$seed._watchers = {} + this.$parent = options.parentCompiler && options.parentCompiler.vm } -var ScopeProto = Scope.prototype +var VMProto = ViewModel.prototype /* * register a listener that will be broadcasted from the global event bus */ -ScopeProto.$on = function (event, handler) { +VMProto.$on = function (event, handler) { utils.eventbus.on(event, handler) - this.$seed._listeners.push({ + this.$compiler.listeners.push({ event: event, handler: handler }) @@ -29,9 +28,9 @@ ScopeProto.$on = function (event, handler) { /* * remove the registered listener */ -ScopeProto.$off = function (event, handler) { +VMProto.$off = function (event, handler) { utils.eventbus.off(event, handler) - var listeners = this.$seed._listeners, + var listeners = this.$compiler.listeners, i = listeners.length, listener while (i--) { listener = listeners[i] @@ -43,19 +42,19 @@ ScopeProto.$off = function (event, handler) { } /* - * watch a key on the scope for changes + * watch a key on the viewmodel for changes * fire callback with new value */ -ScopeProto.$watch = function (key, callback) { +VMProto.$watch = function (key, callback) { var self = this - // yield and wait for seed to finish compiling + // yield and wait for compiler to finish compiling setTimeout(function () { - var scope = self.$seed.scope, - binding = self.$seed._bindings[key], + var viewmodel = self.$compiler.vm, + binding = self.$compiler.bindings[key], i = binding.deps.length, - watcher = self.$seed._watchers[key] = { + watcher = self.$compiler.watchers[key] = { refresh: function () { - callback(scope[key]) + callback(viewmodel[key]) }, deps: binding.deps } @@ -68,50 +67,51 @@ ScopeProto.$watch = function (key, callback) { /* * remove watcher */ -ScopeProto.$unwatch = function (key) { +VMProto.$unwatch = function (key) { var self = this setTimeout(function () { - var watcher = self.$seed._watchers[key] + var watcher = self.$compiler.watchers[key] if (!watcher) return var i = watcher.deps.length, subs while (i--) { subs = watcher.deps[i].subs subs.splice(subs.indexOf(watcher)) } - delete self.$seed._watchers[key] + self.$compiler.watchers[key] = null }, 0) } /* - * load data into scope + * load data into viewmodel */ -ScopeProto.$load = function (data) { +VMProto.$load = function (data) { for (var key in data) { this[key] = data[key] } } /* - * Dump a copy of current scope data, excluding seed-exposed properties. + * Dump a copy of current viewmodel data, excluding compiler-exposed properties. * @param key (optional): key for the value to dump */ -ScopeProto.$dump = function (key) { - var bindings = this.$seed._bindings +VMProto.$dump = function (key) { + var bindings = this.$compiler.bindings return utils.dump(key ? bindings[key].value : this) } /* * stringify the result from $dump */ -ScopeProto.$serialize = function (key) { +VMProto.$serialize = function (key) { return JSON.stringify(this.$dump(key)) } /* * unbind everything, remove everything */ -ScopeProto.$destroy = function () { - this.$seed._destroy() +VMProto.$destroy = function () { + this.$compiler.destroy() + this.$compiler = null } -module.exports = Scope \ No newline at end of file +module.exports = ViewModel \ No newline at end of file