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/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",

View File

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

View File

@ -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
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)
}
} else if (value !== self.value) {
self.update(value)
if (this.value.set) {
this.value.set(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

View File

@ -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 = []
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
module.exports = Compiler

View File

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

View File

@ -26,10 +26,14 @@ function Directive (directiveName, expression, oneway) {
this._update = definition.update
for (prop in definition) {
if (prop !== 'update') {
if (prop === 'unbind') {
this._unbind = definition[prop]
} else {
this[prop] = definition[prop]
}
}
}
}
this.oneway = !!oneway
this.directiveName = directiveName
@ -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
*/

View File

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

View File

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

View File

@ -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,
var compiler = this.compiler,
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
// 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
}
}

View File

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

View File

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

View File

@ -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
module.exports = ViewModel