rename internal, optimize memory usage for GC

This commit is contained in:
Evan You 2013-08-14 16:47:21 -04:00
parent aff965f353
commit d78df31017
12 changed files with 317 additions and 264 deletions

View File

@ -6,8 +6,8 @@
"src/main.js", "src/main.js",
"src/config.js", "src/config.js",
"src/utils.js", "src/utils.js",
"src/seed.js", "src/compiler.js",
"src/scope.js", "src/viewmodel.js",
"src/binding.js", "src/binding.js",
"src/directive-parser.js", "src/directive-parser.js",
"src/text-parser.js", "src/text-parser.js",

View File

@ -12,6 +12,7 @@ Seed.controller('todos', {
// initializer, reserved // initializer, reserved
init: function () { init: function () {
window.app = this
// listen for hashtag change // listen for hashtag change
this.updateFilter() this.updateFilter()
this.$on('filterchange', this.updateFilter.bind(this)) this.$on('filterchange', this.updateFilter.bind(this))
@ -29,9 +30,9 @@ Seed.controller('todos', {
return this.total - this.remaining 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) { 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 // dynamic context computed property using info from target element
@ -45,10 +46,11 @@ Seed.controller('todos', {
return this.remaining === 0 return this.remaining === 0
}, },
set: function (value) { set: function (value) {
this.remaining = value ? 0 : this.total
this.todos.forEach(function (todo) { this.todos.forEach(function (todo) {
todo.completed = value todo.completed = value
}) })
this.remaining = value ? 0 : this.total
todoStorage.save(this.todos)
} }
}, },
@ -64,32 +66,32 @@ Seed.controller('todos', {
}, },
removeTodo: function (e) { removeTodo: function (e) {
this.todos.remove(e.scope) this.todos.remove(e.vm)
this.remaining -= e.scope.completed ? 0 : 1 this.remaining -= e.vm.completed ? 0 : 1
todoStorage.save(this.todos) todoStorage.save(this.todos)
}, },
toggleTodo: function (e) { toggleTodo: function (e) {
this.remaining += e.scope.completed ? -1 : 1 this.remaining += e.vm.completed ? -1 : 1
todoStorage.save(this.todos) todoStorage.save(this.todos)
}, },
editTodo: function (e) { editTodo: function (e) {
this.beforeEditCache = e.scope.title this.beforeEditCache = e.vm.title
e.scope.editing = true e.vm.editing = true
}, },
doneEdit: function (e) { doneEdit: function (e) {
if (!e.scope.editing) return if (!e.vm.editing) return
e.scope.editing = false e.vm.editing = false
e.scope.title = e.scope.title.trim() e.vm.title = e.vm.title.trim()
if (!e.scope.title) this.removeTodo(e) if (!e.vm.title) this.removeTodo(e)
todoStorage.save(this.todos) todoStorage.save(this.todos)
}, },
cancelEdit: function (e) { cancelEdit: function (e) {
e.scope.editing = false e.vm.editing = false
e.scope.title = this.beforeEditCache e.vm.title = this.beforeEditCache
}, },
removeCompleted: function () { removeCompleted: function () {

View File

@ -5,17 +5,17 @@ var utils = require('./utils'),
/* /*
* Binding class. * 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 * which has multiple directive instances on the DOM
* and multiple computed property dependents * and multiple computed property dependents
*/ */
function Binding (seed, key) { function Binding (compiler, key) {
this.seed = seed this.compiler = compiler
this.scope = seed.scope this.vm = compiler.vm
this.key = key this.key = key
var path = key.split('.') var path = key.split('.')
this.inspect(utils.getNestedValue(seed.scope, path)) this.inspect(utils.getNestedValue(compiler.vm, path))
this.def(seed.scope, path) this.def(compiler.vm, path)
this.instances = [] this.instances = []
this.subs = [] this.subs = []
this.deps = [] this.deps = []
@ -27,85 +27,82 @@ var BindingProto = Binding.prototype
* Pre-process a passed in value based on its type * Pre-process a passed in value based on its type
*/ */
BindingProto.inspect = function (value) { BindingProto.inspect = function (value) {
var type = utils.typeOf(value), var type = utils.typeOf(value)
self = this
// preprocess the value depending on its type // preprocess the value depending on its type
if (type === 'Object') { if (type === 'Object') {
if (value.get) { if (value.get) {
var l = Object.keys(value).length var l = Object.keys(value).length
if (l === 1 || (l === 2 && value.set)) { if (l === 1 || (l === 2 && value.set)) {
self.isComputed = true // computed property this.isComputed = true // computed property
value.get = value.get.bind(self.scope) this.rawGet = value.get
if (value.set) value.set = value.set.bind(self.scope) value.get = value.get.bind(this.vm)
if (value.set) value.set = value.set.bind(this.vm)
} }
} }
} else if (type === 'Array') { } else if (type === 'Array') {
value = utils.dump(value)
utils.watchArray(value) utils.watchArray(value)
value.on('mutate', function () { value.on('mutate', this.pub.bind(this))
self.pub()
})
} }
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 * recursive for nested objects
*/ */
BindingProto.def = function (scope, path) { BindingProto.def = function (viewmodel, path) {
var self = this, var key = path[0]
seed = self.seed,
key = path[0]
if (path.length === 1) { if (path.length === 1) {
// here we are! at the end of the path! // here we are! at the end of the path!
// define the real value accessors. // define the real value accessors.
def(scope, key, { def(viewmodel, key, {
get: function () { get: (function () {
if (observer.isObserving) { if (observer.isObserving) {
observer.emit('get', self) observer.emit('get', this)
} }
return self.isComputed return this.isComputed
? self.value.get({ ? this.value.get({
el: seed.el, el: this.compiler.el,
scope: seed.scope vm: this.compiler.vm
}) })
: self.value : this.value
}, }).bind(this),
set: function (value) { set: (function (value) {
if (self.isComputed) { if (this.isComputed) {
// computed properties cannot be redefined // computed properties cannot be redefined
// no need to call binding.update() here, // no need to call binding.update() here,
// as dependency extraction has taken care of that // as dependency extraction has taken care of that
if (self.value.set) { if (this.value.set) {
self.value.set(value) this.value.set(value)
}
} else if (value !== self.value) {
self.update(value)
} }
} else if (value !== this.value) {
this.update(value)
} }
}).bind(this)
}) })
} else { } else {
// we are not there yet!!! // we are not there yet!!!
// create an intermediate subscope // create an intermediate object
// which also has its own getter/setters // which also has its own getter/setters
var subScope = scope[key] var nestedObject = viewmodel[key]
if (!subScope) { if (!nestedObject) {
subScope = {} nestedObject = {}
def(scope, key, { def(viewmodel, key, {
get: function () { get: (function () {
return subScope return this
}, }).bind(nestedObject),
set: function (value) { set: (function (value) {
// when the subScope is given a new value, // when the nestedObject is given a new value,
// copy everything over to trigger the setters // copy everything over to trigger the setters
for (var prop in value) { for (var prop in value) {
subScope[prop] = value[prop] this[prop] = value[prop]
}
} }
}).bind(nestedObject)
}) })
} }
// recurse // recurse
this.def(subScope, path.slice(1)) this.def(nestedObject, path.slice(1))
} }
} }
@ -116,7 +113,7 @@ BindingProto.update = function (value) {
this.inspect(value) this.inspect(value)
var i = this.instances.length var i = this.instances.length
while (i--) { while (i--) {
this.instances[i].update(value) this.instances[i].update(this.value)
} }
this.pub() 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 * Notify computed properties that depend on this binding
* to update themselves * to update themselves

View File

@ -1,5 +1,5 @@
var config = require('./config'), var config = require('./config'),
Scope = require('./scope'), ViewModel = require('./viewmodel'),
Binding = require('./binding'), Binding = require('./binding'),
DirectiveParser = require('./directive-parser'), DirectiveParser = require('./directive-parser'),
TextParser = require('./text-parser'), TextParser = require('./text-parser'),
@ -11,26 +11,26 @@ var slice = Array.prototype.slice,
eachAttr = config.prefix + '-each' eachAttr = config.prefix + '-each'
/* /*
* The main ViewModel class * The DOM compiler
* scans a node and parse it to populate data bindings * scans a DOM node and compile bindings for a ViewModel
*/ */
function Seed (el, options) { function Compiler (el, options) {
config.log('\ncreated new Seed instance.\n')
config.log('\ncreated new Compiler instance.\n')
if (typeof el === 'string') { if (typeof el === 'string') {
el = document.querySelector(el) el = document.querySelector(el)
} }
this.el = el this.el = el
el.seed = this el.compiler = this
this._bindings = {} this.bindings = {}
this._watchers = {} this.directives = []
this._listeners = [] this.watchers = {}
this.listeners = []
// list of computed properties that need to parse dependencies for // list of computed properties that need to parse dependencies for
this._computed = [] this.computed = []
// list of bindings that has dynamic context dependencies // list of bindings that has dynamic context dependencies
this._contextBindings = [] this.contextBindings = []
// copy options // copy options
options = options || {} options = options || {}
@ -48,71 +48,71 @@ function Seed (el, options) {
data = data || {} data = data || {}
el.removeAttribute(dataAttr) 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 // make a copy from it
if (data.$seed instanceof Seed) { if (data instanceof ViewModel) {
data = data.$dump() 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 var ctrlID = el.getAttribute(ctrlAttr), controller
if (ctrlID) { if (ctrlID) {
el.removeAttribute(ctrlAttr) el.removeAttribute(ctrlAttr)
controller = config.controllers[ctrlID] controller = config.controllers[ctrlID]
if (controller) { if (controller) {
this._controller = controller this.controller = controller
} else { } else {
config.warn('controller "' + ctrlID + '" is not defined.') config.warn('controller "' + ctrlID + '" is not defined.')
} }
} }
// create the scope object // create the viewmodel object
// if the controller has an extended scope contructor, use it; // if the controller has an extended viewmodel contructor, use it;
// otherwise, use the original scope constructor. // otherwise, use the original viewmodel constructor.
var ScopeConstructor = (controller && controller.ExtendedScope) || Scope, var VMCtor = (controller && controller.ExtendedVM) || ViewModel,
scope = this.scope = new ScopeConstructor(this, options) viewmodel = this.vm = new VMCtor(this, options)
// copy data // copy data
for (var key in data) { for (var key in data) {
scope[key] = data[key] viewmodel[key] = data[key]
} }
// apply controller initialize function // apply controller initialize function
if (controller && controller.init) { if (controller && controller.init) {
controller.init.call(scope) controller.init.call(viewmodel)
} }
// now parse the DOM // 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 anything in viewmodel but not binded in DOM, create bindings for them
for (key in scope) { for (key in viewmodel) {
if (key.charAt(0) !== '$' && !this._bindings[key]) { if (key.charAt(0) !== '$' && !this.bindings[key]) {
this._createBinding(key) this.createBinding(key)
} }
} }
// extract dependencies for computed properties // extract dependencies for computed properties
if (this._computed.length) depsParser.parse(this._computed) if (this.computed.length) depsParser.parse(this.computed)
delete this._computed this.computed = null
// extract dependencies for computed properties with dynamic context // extract dependencies for computed properties with dynamic context
if (this._contextBindings.length) this._bindContexts(this._contextBindings) if (this.contextBindings.length) this.bindContexts(this.contextBindings)
delete this._contextBindings this.contextBindings = null
} }
// for better compression // for better compression
var SeedProto = Seed.prototype var CompilerProto = Compiler.prototype
/* /*
* Compile a DOM node (recursive) * Compile a DOM node (recursive)
*/ */
SeedProto._compileNode = function (node, root) { CompilerProto.compileNode = function (node, root) {
var seed = this var compiler = this
if (node.nodeType === 3) { // text node if (node.nodeType === 3) { // text node
seed._compileTextNode(node) compiler.compileTextNode(node)
} else if (node.nodeType === 1) { } else if (node.nodeType === 1) {
@ -125,14 +125,14 @@ SeedProto._compileNode = function (node, root) {
directive = DirectiveParser.parse(eachAttr, eachExp) directive = DirectiveParser.parse(eachAttr, eachExp)
if (directive) { if (directive) {
directive.el = node directive.el = node
seed._bind(directive) compiler.bindDirective(directive)
} }
} else if (ctrlExp && !root) { // nested controllers } else if (ctrlExp && !root) { // nested controllers
new Seed(node, { new Compiler(node, {
child: true, child: true,
parentSeed: seed parentCompiler: compiler
}) })
} else { // normal node } else { // normal node
@ -153,7 +153,7 @@ SeedProto._compileNode = function (node, root) {
if (directive) { if (directive) {
valid = true valid = true
directive.el = node directive.el = node
seed._bind(directive) compiler.bindDirective(directive)
} }
} }
if (valid) node.removeAttribute(attr.name) if (valid) node.removeAttribute(attr.name)
@ -162,7 +162,7 @@ SeedProto._compileNode = function (node, root) {
// recursively compile childNodes // recursively compile childNodes
if (node.childNodes.length) { 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 * Compile a text node
*/ */
SeedProto._compileTextNode = function (node) { CompilerProto.compileTextNode = function (node) {
var tokens = TextParser.parse(node) var tokens = TextParser.parse(node)
if (!tokens) return if (!tokens) return
var seed = this, var compiler = this,
dirname = config.prefix + '-text', dirname = config.prefix + '-text',
el, token, directive el, token, directive
for (var i = 0, l = tokens.length; i < l; i++) { for (var i = 0, l = tokens.length; i < l; i++) {
@ -184,7 +184,7 @@ SeedProto._compileTextNode = function (node) {
directive = DirectiveParser.parse(dirname, token.key) directive = DirectiveParser.parse(dirname, token.key)
if (directive) { if (directive) {
directive.el = el directive.el = el
seed._bind(directive) compiler.bindDirective(directive)
} }
} else { } else {
el.nodeValue = token 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, var key = directive.key,
seed = directive.seed = this compiler = this
// deal with each block // deal with each block
if (this.each) { if (this.each) {
if (key.indexOf(this.eachPrefix) === 0) { if (key.indexOf(this.eachPrefix) === 0) {
key = directive.key = key.replace(this.eachPrefix, '') key = directive.key = key.replace(this.eachPrefix, '')
} else { } else {
seed = this.parentSeed compiler = this.parentCompiler
} }
} }
// deal with nesting // deal with nesting
seed = traceOwnerSeed(directive, seed) compiler = traceOwnerCompiler(directive, compiler)
var binding = seed._bindings[key] || seed._createBinding(key) var binding = compiler.bindings[key] || compiler.createBinding(key)
binding.instances.push(directive) binding.instances.push(directive)
directive.binding = binding 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 // invoke bind hook if exists
if (directive.bind) { if (directive.bind) {
directive.bind(binding.value) 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 * Process subscriptions for computed properties that has
* dynamic context dependencies * dynamic context dependencies
*/ */
SeedProto._bindContexts = function (bindings) { CompilerProto.bindContexts = function (bindings) {
var i = bindings.length, j, binding, depKey, dep var i = bindings.length, j, k, binding, depKey, dep, ins
while (i--) { while (i--) {
binding = bindings[i] binding = bindings[i]
j = binding.contextDeps.length j = binding.contextDeps.length
while (j--) { while (j--) {
depKey = binding.contextDeps[j] depKey = binding.contextDeps[j]
dep = this._bindings[depKey] k = binding.instances.length
dep.subs.push(binding) 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 * Unbind and remove element
*/ */
SeedProto._destroy = function () { CompilerProto.destroy = function () {
this._unbind() 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) this.el.parentNode.removeChild(this.el)
} }
// Helpers -------------------------------------------------------------------- // 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) { if (key.nesting) {
var levels = key.nesting var levels = key.nesting
while (seed.parentSeed && levels--) { while (compiler.parentCompiler && levels--) {
seed = seed.parentSeed compiler = compiler.parentCompiler
} }
} else if (key.root) { } else if (key.root) {
while (seed.parentSeed) { while (compiler.parentCompiler) {
seed = seed.parentSeed compiler = compiler.parentCompiler
} }
} }
return seed return compiler
} }
module.exports = Seed module.exports = Compiler

View File

@ -4,7 +4,7 @@ var Emitter = require('emitter'),
var dummyEl = document.createElement('div'), var dummyEl = document.createElement('div'),
ARGS_RE = /^function\s*?\((.+?)\)/, 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 () {} noop = function () {}
/* /*
@ -21,7 +21,7 @@ function catchDeps (binding) {
}) })
parseContextDependency(binding) parseContextDependency(binding)
binding.value.get({ binding.value.get({
scope: createDummyScope(binding), vm: createDummyVM(binding),
el: dummyEl el: dummyEl
}) })
observer.off('get') observer.off('get')
@ -37,31 +37,37 @@ function filterDeps (binding) {
while (i--) { while (i--) {
dep = binding.deps[i] dep = binding.deps[i]
if (!dep.deps.length) { if (!dep.deps.length) {
config.log(' └─' + dep.key) config.log(' └─ ' + dep.key)
dep.subs.push(binding) dep.subs.push(binding)
} else { } else {
binding.deps.splice(i, 1) 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, * 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 * to access in that getter. To avoid thowing an error or forcing
* the user to guard against an undefined argument, we staticly * the user to guard against an undefined argument, we staticly
* analyze the function to extract any possible nested properties * 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. * a noop function so they can be invoked with no real harm.
*/ */
function createDummyScope (binding) { function createDummyVM (binding) {
var scope = {}, var viewmodel = {},
deps = binding.contextDeps deps = binding.contextDeps
if (!deps) return scope if (!deps) return viewmodel
var i = binding.contextDeps.length, var i = binding.contextDeps.length,
j, level, key, path j, level, key, path
while (i--) { while (i--) {
level = scope level = viewmodel
path = deps[i].split('.') path = deps[i].split('.')
j = 0 j = 0
while (j < path.length) { while (j < path.length) {
@ -71,20 +77,20 @@ function createDummyScope (binding) {
j++ j++
} }
} }
return scope return viewmodel
} }
/* /*
* Extract context dependency paths * Extract context dependency paths
*/ */
function parseContextDependency (binding) { function parseContextDependency (binding) {
var fn = binding.value.get, var fn = binding.rawGet,
str = fn.toString(), str = fn.toString(),
args = str.match(ARGS_RE) args = str.match(ARGS_RE)
if (!args) return null if (!args) return null
var argRE = new RegExp(args[1] + SCOPE_RE_STR, 'g'), var depsRE = new RegExp(args[1] + SCOPE_RE_STR, 'g'),
matches = str.match(argRE), matches = str.match(depsRE),
base = args[1].length + 7 base = args[1].length + 4
if (!matches) return null if (!matches) return null
var i = matches.length, var i = matches.length,
deps = [], dep deps = [], dep
@ -95,7 +101,7 @@ function parseContextDependency (binding) {
} }
} }
binding.contextDeps = deps binding.contextDeps = deps
binding.seed._contextBindings.push(binding) binding.compiler.contextBindings.push(binding)
} }
module.exports = { module.exports = {

View File

@ -26,10 +26,14 @@ function Directive (directiveName, expression, oneway) {
this._update = definition.update this._update = definition.update
for (prop in definition) { for (prop in definition) {
if (prop !== 'update') { if (prop !== 'update') {
if (prop === 'unbind') {
this._unbind = definition[prop]
} else {
this[prop] = definition[prop] this[prop] = definition[prop]
} }
} }
} }
}
this.oneway = !!oneway this.oneway = !!oneway
this.directiveName = directiveName this.directiveName = directiveName
@ -62,11 +66,11 @@ DirProto.update = function (value) {
* called when a dependency has changed * called when a dependency has changed
*/ */
DirProto.refresh = function () { DirProto.refresh = function () {
// pass element and scope info to the getter // pass element and viewmodel info to the getter
// enables powerful context-aware bindings // enables powerful context-aware bindings
var value = this.value.get({ var value = this.value.get({
el: this.el, el: this.el,
scope: this.seed.scope vm: this.vm
}) })
if (value === this.computedValue) return if (value === this.computedValue) return
this.computedValue = value this.computedValue = value
@ -135,6 +139,15 @@ DirProto.parseKey = function (rawKey) {
this.key = key 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 * parse a filter expression
*/ */

View File

@ -1,4 +1,5 @@
var config = require('../config') var config = require('../config'),
utils = require('../utils')
/* /*
* Mathods that perform precise DOM manipulation * Mathods that perform precise DOM manipulation
@ -59,16 +60,16 @@ var mutationHandlers = {
}, },
sort: function () { sort: function () {
var i, l = this.collection.length, scope var i, l = this.collection.length, viewmodel
for (i = 0; i < l; i++) { for (i = 0; i < l; i++) {
scope = this.collection[i] viewmodel = this.collection[i]
scope.$index = i viewmodel.$index = i
this.container.insertBefore(scope.$el, this.ref) this.container.insertBefore(viewmodel.$el, this.ref)
} }
} }
} }
mutationHandlers.reverse = mutationHandlers.sort //mutationHandlers.reverse = mutationHandlers.sort
module.exports = { module.exports = {
@ -84,7 +85,6 @@ module.exports = {
update: function (collection) { update: function (collection) {
this.unbind(true) this.unbind(true)
if (!Array.isArray(collection)) return
this.collection = collection this.collection = collection
// attach an object to container to hold handlers // attach an object to container to hold handlers
@ -92,10 +92,9 @@ module.exports = {
// listen for collection mutation events // listen for collection mutation events
// the collection has been augmented during Binding.set() // the collection has been augmented during Binding.set()
var self = this collection.on('mutate', (function (mutation) {
collection.on('mutate', function (mutation) { mutationHandlers[mutation.method].call(this, mutation)
mutationHandlers[mutation.method].call(self, mutation) }).bind(this))
})
// create child-seeds and append to DOM // create child-seeds and append to DOM
for (var i = 0, l = collection.length; i < l; i++) { for (var i = 0, l = collection.length; i < l; i++) {
@ -106,16 +105,16 @@ module.exports = {
buildItem: function (ref, data, index) { buildItem: function (ref, data, index) {
var node = this.el.cloneNode(true) var node = this.el.cloneNode(true)
this.container.insertBefore(node, ref) this.container.insertBefore(node, ref)
var Seed = require('../seed'), var Compiler = require('../compiler'),
spore = new Seed(node, { spore = new Compiler(node, {
each: true, each: true,
eachPrefix: this.arg + '.', eachPrefix: this.arg + '.',
parentSeed: this.seed, parentCompiler: this.compiler,
index: index, index: index,
data: data, data: data,
delegator: this.container delegator: this.container
}) })
this.collection[index] = spore.scope this.collection[index] = spore.vm
}, },
updateIndexes: function () { updateIndexes: function () {
@ -125,20 +124,19 @@ module.exports = {
} }
}, },
unbind: function (reset) { unbind: function () {
if (this.collection && this.collection.length) { if (this.collection) {
var i = this.collection.length, this.collection.off('mutate')
fn = reset ? '_destroy' : '_unbind' var i = this.collection.length
while (i--) { while (i--) {
this.collection[i].$seed[fn]() this.collection[i].$destroy()
} }
this.collection = null
} }
var ctn = this.container, var ctn = this.container,
handlers = ctn.sd_dHandlers handlers = ctn.sd_dHandlers
for (var key in handlers) { for (var key in handlers) {
ctn.removeEventListener(handlers[key].event, handlers[key]) ctn.removeEventListener(handlers[key].event, handlers[key])
} }
delete ctn.sd_dHandlers ctn.sd_dHandlers = null
} }
} }

View File

@ -51,7 +51,7 @@ module.exports = {
if (this.oneway) return if (this.oneway) return
var el = this.el, self = this var el = this.el, self = this
this.change = function () { this.change = function () {
self.seed.scope[self.key] = el.value self.compiler.vm[self.key] = el.value
} }
el.addEventListener('keyup', this.change) el.addEventListener('keyup', this.change)
}, },
@ -69,7 +69,7 @@ module.exports = {
if (this.oneway) return if (this.oneway) return
var el = this.el, self = this var el = this.el, self = this
this.change = function () { this.change = function () {
self.seed.scope[self.key] = el.checked self.compiler.vm[self.key] = el.checked
} }
el.addEventListener('change', this.change) el.addEventListener('change', this.change)
}, },

View File

@ -13,29 +13,29 @@ module.exports = {
expectFunction : true, expectFunction : true,
bind: function () { bind: function () {
if (this.seed.each) { if (this.compiler.each) {
// attach an identifier to the el // attach an identifier to the el
// so it can be matched during event delegation // so it can be matched during event delegation
this.el[this.expression] = true this.el[this.expression] = true
// attach the owner scope of this directive // attach the owner viewmodel of this directive
this.el.sd_scope = this.seed.scope this.el.sd_viewmodel = this.vm
} }
}, },
update: function (handler) { update: function (handler) {
this.unbind() this.unbind(true)
if (!handler) return if (!handler) return
var seed = this.seed, var compiler = this.compiler,
event = this.arg, event = this.arg,
ownerScope = this.binding.seed.scope 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 // for each blocks, delegate for better performance
// focus and blur events dont bubble so exclude them // focus and blur events dont bubble so exclude them
var delegator = seed.delegator, var delegator = compiler.delegator,
identifier = this.expression, identifier = this.expression,
dHandler = delegator.sd_dHandlers[identifier] dHandler = delegator.sd_dHandlers[identifier]
@ -46,8 +46,8 @@ module.exports = {
var target = delegateCheck(e.target, delegator, identifier) var target = delegateCheck(e.target, delegator, identifier)
if (target) { if (target) {
e.el = target e.el = target
e.scope = target.sd_scope e.vm = target.sd_viewmodel
handler.call(ownerScope, e) handler.call(ownerVM, e)
} }
} }
dHandler.event = event dHandler.event = event
@ -58,15 +58,17 @@ module.exports = {
// a normal, single element handler // a normal, single element handler
this.handler = function (e) { this.handler = function (e) {
e.el = e.currentTarget e.el = e.currentTarget
e.scope = seed.scope e.vm = compiler.vm
handler.call(seed.scope, e) handler.call(compiler.vm, e)
} }
this.el.addEventListener(event, this.handler) this.el.addEventListener(event, this.handler)
} }
}, },
unbind: function () { unbind: function (update) {
this.el.removeEventListener(this.arg, this.handler) this.el.removeEventListener(this.arg, this.handler)
this.handler = null
if (!update) this.el.sd_viewmodel = null
} }
} }

View File

@ -1,6 +1,6 @@
var config = require('./config'), var config = require('./config'),
Seed = require('./seed'), Compiler = require('./compiler'),
Scope = require('./scope'), ViewModel = require('./viewmodel'),
directives = require('./directives'), directives = require('./directives'),
filters = require('./filters'), filters = require('./filters'),
textParser = require('./text-parser'), textParser = require('./text-parser'),
@ -40,12 +40,12 @@ api.data = function (id, data) {
*/ */
api.controller = function (id, properties) { api.controller = function (id, properties) {
if (!properties) return controllers[id] if (!properties) return controllers[id]
// create a subclass of Scope that has the extension methods mixed-in // create a subclass of ViewModel that has the extension methods mixed-in
var ExtendedScope = function () { var ExtendedVM = function () {
Scope.apply(this, arguments) ViewModel.apply(this, arguments)
} }
var p = ExtendedScope.prototype = Object.create(Scope.prototype) var p = ExtendedVM.prototype = Object.create(ViewModel.prototype)
p.constructor = ExtendedScope p.constructor = ExtendedVM
for (var prop in properties) { for (var prop in properties) {
if (prop !== 'init') { if (prop !== 'init') {
p[prop] = properties[prop] p[prop] = properties[prop]
@ -53,7 +53,7 @@ api.controller = function (id, properties) {
} }
controllers[id] = { controllers[id] = {
init: properties.init, init: properties.init,
ExtendedScope: ExtendedScope ExtendedVM: ExtendedVM
} }
} }
@ -91,12 +91,12 @@ api.config = function (opts) {
* Compile a single element * Compile a single element
*/ */
api.compile = function (el) { api.compile = function (el) {
return new Seed(el).scope return new Compiler(el).vm
} }
/* /*
* Bootstrap the whole thing * 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 * that has either sd-controller or sd-data
*/ */
api.bootstrap = function (opts) { api.bootstrap = function (opts) {
@ -107,7 +107,7 @@ api.bootstrap = function (opts) {
dataSlt = '[' + config.prefix + '-data]' dataSlt = '[' + config.prefix + '-data]'
/* jshint boss: true */ /* jshint boss: true */
while (el = document.querySelector(ctrlSlt) || document.querySelector(dataSlt)) { while (el = document.querySelector(ctrlSlt) || document.querySelector(dataSlt)) {
new Seed(el) new Compiler(el)
} }
booted = true booted = true
} }

View File

@ -31,7 +31,7 @@ function dump (val) {
} else if (type === 'Object') { } else if (type === 'Object') {
if (val.get) { // computed property if (val.get) { // computed property
return val.get() return val.get()
} else { // object / child scope } else { // object / child viewmodel
var ret = {}, prop var ret = {}, prop
for (var key in val) { for (var key in val) {
prop = val[key] prop = val[key]

View File

@ -1,26 +1,25 @@
var utils = require('./utils') var utils = require('./utils')
/* /*
* Scope is the ViewModel/whatever exposed to the user * ViewModel exposed to the user that holds data,
* that holds data, computed properties, event handlers * computed properties, event handlers
* and a few reserved methods * and a few reserved methods
*/ */
function Scope (seed, options) { function ViewModel (compiler, options) {
this.$seed = seed this.$compiler = compiler
this.$el = seed.el this.$el = compiler.el
this.$index = options.index this.$index = options.index
this.$parent = options.parentSeed && options.parentSeed.scope this.$parent = options.parentCompiler && options.parentCompiler.vm
this.$seed._watchers = {}
} }
var ScopeProto = Scope.prototype var VMProto = ViewModel.prototype
/* /*
* register a listener that will be broadcasted from the global event bus * 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) utils.eventbus.on(event, handler)
this.$seed._listeners.push({ this.$compiler.listeners.push({
event: event, event: event,
handler: handler handler: handler
}) })
@ -29,9 +28,9 @@ ScopeProto.$on = function (event, handler) {
/* /*
* remove the registered listener * remove the registered listener
*/ */
ScopeProto.$off = function (event, handler) { VMProto.$off = function (event, handler) {
utils.eventbus.off(event, handler) utils.eventbus.off(event, handler)
var listeners = this.$seed._listeners, var listeners = this.$compiler.listeners,
i = listeners.length, listener i = listeners.length, listener
while (i--) { while (i--) {
listener = listeners[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 * fire callback with new value
*/ */
ScopeProto.$watch = function (key, callback) { VMProto.$watch = function (key, callback) {
var self = this var self = this
// yield and wait for seed to finish compiling // yield and wait for compiler to finish compiling
setTimeout(function () { setTimeout(function () {
var scope = self.$seed.scope, var viewmodel = self.$compiler.vm,
binding = self.$seed._bindings[key], binding = self.$compiler.bindings[key],
i = binding.deps.length, i = binding.deps.length,
watcher = self.$seed._watchers[key] = { watcher = self.$compiler.watchers[key] = {
refresh: function () { refresh: function () {
callback(scope[key]) callback(viewmodel[key])
}, },
deps: binding.deps deps: binding.deps
} }
@ -68,50 +67,51 @@ ScopeProto.$watch = function (key, callback) {
/* /*
* remove watcher * remove watcher
*/ */
ScopeProto.$unwatch = function (key) { VMProto.$unwatch = function (key) {
var self = this var self = this
setTimeout(function () { setTimeout(function () {
var watcher = self.$seed._watchers[key] var watcher = self.$compiler.watchers[key]
if (!watcher) return if (!watcher) return
var i = watcher.deps.length, subs var i = watcher.deps.length, subs
while (i--) { while (i--) {
subs = watcher.deps[i].subs subs = watcher.deps[i].subs
subs.splice(subs.indexOf(watcher)) subs.splice(subs.indexOf(watcher))
} }
delete self.$seed._watchers[key] self.$compiler.watchers[key] = null
}, 0) }, 0)
} }
/* /*
* load data into scope * load data into viewmodel
*/ */
ScopeProto.$load = function (data) { VMProto.$load = function (data) {
for (var key in data) { for (var key in data) {
this[key] = data[key] 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 * @param key (optional): key for the value to dump
*/ */
ScopeProto.$dump = function (key) { VMProto.$dump = function (key) {
var bindings = this.$seed._bindings var bindings = this.$compiler.bindings
return utils.dump(key ? bindings[key].value : this) return utils.dump(key ? bindings[key].value : this)
} }
/* /*
* stringify the result from $dump * stringify the result from $dump
*/ */
ScopeProto.$serialize = function (key) { VMProto.$serialize = function (key) {
return JSON.stringify(this.$dump(key)) return JSON.stringify(this.$dump(key))
} }
/* /*
* unbind everything, remove everything * unbind everything, remove everything
*/ */
ScopeProto.$destroy = function () { VMProto.$destroy = function () {
this.$seed._destroy() this.$compiler.destroy()
this.$compiler = null
} }
module.exports = Scope module.exports = ViewModel