class Sqimitive.Core
Defined in: main.js, line 94
Modifiers: protected, static
Part of: tag_Extension
from setOnDeclViaPush
May only be set after Core::extend() using MyClass.prop.push(...)
.
Specifies which instance properties are to be merged rather than overwritten when redefined in a subclass.
Types | Notes |
---|---|
array of string | Property names. |
Properties must be of these types:
Name | Types | Notes |
---|---|---|
array | Merged using Array.concat(baseClass[prop],
childClass[prop]) . Subclass’ values are added after parents’ values:
['a', 'b'] + ['c'] = ['a', 'b', 'c'] . | |
object | Merged using _.extend(baseClass[prop], childClass[prop]) .
Keys defined in parents are kept unless also defined in a subclass:
{a: 1, b: 2} + {a: 3, c: 4} = {a: 3, b: 2, c: 4} . |
mergePropsExtendWarning: when passing _mergeProps or _shareProps inside
staticProps
(second argument of extend()) all inherited items will be
removed. The correct way to add your properties while keeping those in
the base classes is this:
var MySqimitive = Sqimitive.Base.extend({
// Define instance fields...
}, {
// Define static fields if you need, else don't pass this parameter.
})
// extend() has copied the inherited _mergeProps list which we can now
// append to or modify using regular Array functions.
MySqimitive._mergeProps.push('prop1', 'prop2', ...)
// Same with _shareProps.
MySqimitive._shareProps.push('prop1', 'prop2', ...)
These are wrong ways to append to these properties:
var MySqimitive = Sqimitive.Base.extend({
// WRONG: _mergeProps is static so it won't be read from here.
_mergeProps: ['prop'],
}, {
// WRONG: technically fine but will entirely replace base class'
// merge list.
_mergeProps: ['prop'],
})
// WRONG: works but once again will replace all the inherited items.
MySqimitive._mergeProps = ['prop']
// CORRECT:
MySqimitive._mergeProps.push('prop')
Other notes:
_mergeProps
doesn’t
“un-merge” it since merging happens in extend() (mixIn()) so after
extend() the new class already has merged properties of all of its
ancestors. However, removal does affect new subclasses: var ClassA = Sqimitive.Base.extend({array: ['in A']})
ClassA._mergeProps.push('array')
// (new ClassA).array is ['in A']
var ClassB = ClassA.extend({array: ['in B']})
// (new ClassB).array is ['in A', 'in B']
var ClassC = ClassB.extend({array: ['in C']})
ClassC._mergeProps = []
// (new ClassC).array is ['in A', 'in B', 'in C']
var ClassD = ClassC.extend({array: ['in D']})
// (new ClassD).array is ['in D'] - not merged!
_mergeProps
doesn’t allow deleting members or otherwise changing the
value. However, in some contexts null
or undefined
does the job for
objects (in contrast with delete
such properties still appear when
using for..in
, etc.):
var MyParent = Sqimitive.Base.extend({
_objectProperty: {key1: 'value1', key2: 'value2'},
})
MyParent._mergeProps.push('_objectProperty')
var MyChild = MyParent.extend({
_objectProperty: {key1: null},
})
// MyChild._objectProperty is now {key1: null, key2: 'value2'}
for (var key in new MyChild) { alert(key) }
//=> key1
//=> key2
Use Base.init() or postInit() to modify inherited values in other ways:
var MyParent = Sqimitive.Base.extend({
_objectProperty: {key1: 'value1', key2: 'value2'},
})
var MyChild = MyParent.extend({
events: {
init: function () {
// MyParent's _objectProperty is unaffected, see _shareProps.
delete this._objectProperty.key1
this._objectProperty.key2 += 'foo'
},
},
})
// MyChild._objectProperty is now {key2: 'value2foo'}
for (var key in new MyChild) { alert(key) }
//=> key2
from inBB
In Backbone, when you extend a parent class with a property that it already has you end up with a completely new property. This doesn’t always make sense – for example, if a class has its own bb:View-events then what you really need is merge its own (base) events with the events of new subclass. Same thing with bb:Model-attributes and bb:Model-defaults, bb:Router-routes and others. Example (http://jsfiddle.net/Proger/u2n3e6ex/):
var MyView = Backbone.View.extend({
events: {
'click .me': function () { alert('You clicked it!') },
},
})
var MyOtherView = MyView.extend({
// This object entirely replaces MyView's event map.
events: {
'keypress .me': function () {
alert('Oh noes, we broke the button :(')
},
},
})
Defined in: main.js, line 430 • Show code
_mergeProps: [],
Modifiers: protected, static
Part of: tag_Extension
from setOnDeclViaPush
May only be set after Core::extend() using MyClass.prop.push(...)
.
Specifies which instance properties are not to be cloned upon construction. They will be shared by all instances of a class (where the property was defined, i.e. where given to extend()).
Types | Notes |
---|---|
array of string | Property names. |
Unlisted instance properties are cloned (using deepClone()) upon new
object instantiation (in constructor). If using a complex object
(Date
, Node
, etc. – not just {}
) then assign it in
Base.init() or postInit(). If using an instance property as if it
were static, either list it in _shareProps or move to staticProps
(extend()) and access as this.constructor.foo
(yes, JavaScript
makes it pretty inconvenient).
from mergePropsExtend
Warning: when passing _mergeProps or _shareProps inside
staticProps
(second argument of extend()) all inherited items will be
removed. The correct way to add your properties while keeping those in
the base classes is this:
var MySqimitive = Sqimitive.Base.extend({
// Define instance fields...
}, {
// Define static fields if you need, else don't pass this parameter.
})
// extend() has copied the inherited _mergeProps list which we can now
// append to or modify using regular Array functions.
MySqimitive._mergeProps.push('prop1', 'prop2', ...)
// Same with _shareProps.
MySqimitive._shareProps.push('prop1', 'prop2', ...)
These are wrong ways to append to these properties:
var MySqimitive = Sqimitive.Base.extend({
// WRONG: _mergeProps is static so it won't be read from here.
_mergeProps: ['prop'],
}, {
// WRONG: technically fine but will entirely replace base class'
// merge list.
_mergeProps: ['prop'],
})
// WRONG: works but once again will replace all the inherited items.
MySqimitive._mergeProps = ['prop']
// CORRECT:
MySqimitive._mergeProps.push('prop')
Other notes:
from inBB
In Backbone, values of all properties inherited by a subclass are shared
among all instances of the base class where they are defined. Just like
in Python, if you have ...extend( {array: []} )
then doing
this.array.push(123)
will affect all instances where array
wasn’t
overwritten with a new object. This poses a typical problem in day-to-day
development.
Example (http://jsfiddle.net/Proger/vwqk67h8/):
var MyView = Backbone.View.extend({foo: {}})
var x = new MyView
var y = new MyView
x.foo.bar = 123
alert(y.foo.bar)
Can you guess the alert message? It’s 123
!
_shareProps: [],
Modifiers: static
Holds recently fire()’d event hooks (cloned eobj
-s with self
set to
call context) if ::trace is on.
from trc
Sometimes you find yourself wondering why a hook was called or who caused
a property to be updated. Given that hooks often point to generic
functions or that _opt
changes are batch()’ed, finding initiators can
be a tricky business.
Effects of ::trace:
eobj
, cloned, with self
set to call context)_events
(eobj
) hold stack trace of their registration
(fuse()) under trace
trace
field in options
, along with
batchID
and otherstrace
All added properties are meant for inspection in debugger.
Note: setting trace
on subclasses of Core
will have no effect.
By default, Chrome limits trace depth to 10 which usually prevents you from seeing actually important frames. One way to increase it is:
Error.stackTraceLimit = 100
Other ways: https://stackoverflow.com/questions/9931444.
Defined in: main.js, line 253 • Show code
lastFired: [],
Modifiers: static
Enables tracing of certain events to aid in debugging. Can be changed on run-time.
trcSometimes you find yourself wondering why a hook was called or who caused
a property to be updated. Given that hooks often point to generic
functions or that _opt
changes are batch()’ed, finding initiators can
be a tricky business.
Effects of ::trace:
eobj
, cloned, with self
set to call context)_events
(eobj
) hold stack trace of their registration
(fuse()) under trace
trace
field in options
, along with
batchID
and otherstrace
All added properties are meant for inspection in debugger.
Note: setting trace
on subclasses of Core
will have no effect.
By default, Chrome limits trace depth to 10 which usually prevents you from seeing actually important frames. One way to increase it is:
Error.stackTraceLimit = 100
Other ways: https://stackoverflow.com/questions/9931444.
Defined in: main.js, line 247 • Show code
trace: false,
Modifiers: protected
from readOnly
May only be read, not changed.
An identifier of this object. Unique among all instances of
Sqimitive.Core
or its subclasses created during this session (page
load).
Currently begins with “p” for “primitive” followed by a positive number as generated by unique(). This is unlikely to change but in any case it’s guaranteed to remain a valid identifier of only Latin symbols, i.e. begin with a letter followed by zero or more letters, digits and underscores.
Can be used to namespace DOM events as in this.el.on('click.' +
this._cid)
(jQuery.attach() does this).
Historically “cid” stands for “c
lient id
entifier” – a term
originating from Backbone (bb:Model-cid) but probably not holding
much meaning at this point.
Defined in: main.js, line 1672 • Show code
_cid: '',
Part of: tag_Events
There is no such property per se but this key can be passed to extend() and mixIn() to set up new event handlers of a “subclass”.
Giving events
is the same as calling this.on({events})
after
extend()/mixIn() so see on() (with an object argument) for details.
Since in Sqimitive everything is an event (evt) this is the way you do inheritance, override methods, etc. Such events are “fused” into the new class declaration so there is no overhead of applying them on each class instantiation.
See the Events overview (evt) for examples.
from es6thiswarn
Warning: avoid using ES6 arrow functions as handlers due to their fixed this
(es6this).
P.S: again, events
is private to extend()/mixIn() and does not
become this.events
.
Defined in: main.js, line 1676
Stores the stack trace at the time this instance was constructor’ed, if ::trace was on.
from trc
Sometimes you find yourself wondering why a hook was called or who caused
a property to be updated. Given that hooks often point to generic
functions or that _opt
changes are batch()’ed, finding initiators can
be a tricky business.
Effects of ::trace:
eobj
, cloned, with self
set to call context)_events
(eobj
) hold stack trace of their registration
(fuse()) under trace
trace
field in options
, along with
batchID
and otherstrace
All added properties are meant for inspection in debugger.
Note: setting trace
on subclasses of Core
will have no effect.
By default, Chrome limits trace depth to 10 which usually prevents you from seeing actually important frames. One way to increase it is:
Error.stackTraceLimit = 100
Other ways: https://stackoverflow.com/questions/9931444.
Defined in: main.js, line 154 • Show code
Core.trace && (this.trace = (new Error).stack)
Modifiers: static
Part of: tag_Options
Returns a function for use as a change or compatible hook that calls
func
only once per batch (batchID
).
Types | Notes |
---|---|
function returning undefined if skipping func due to batchID |
Name | Types | Notes |
---|---|---|
index | int index in returned function’s arguments of the options object
with batchID key (_batchOptions()) | |
str calculate by pattern: c
(for change) or c_ (for change_OPT) preceded by any number of
. + = (event prefixes evtpf that prepend extra arguments) | ||
func | Subject to expandFunc() | |
options | missing | |
object |
Possible options
keys (all optional):
Name | Types | Notes |
---|---|---|
seen | Set | Give the same value in seen (or the same options object)
to link several batchGuard-s together (to call func once across all
guards, not once per guard). you can set seen to a Set or give {}
to the first batchGuard() and then read the value it has written to
this key. Treat Set as a blackbox (it conceals multiple nuances
explained in skb). |
cx | object | If not given then context is unchanged. |
skip | function | Called when skipping a seen batch. Useful when
figuring why func isn’t getting called when it should be. |
Do not use batchGuard() in class declaration (functions given to extend()) unless you want all instances of the class to share the same guard:
var Class = Sqimitive.Core.extend({
events: {
change: batchGuard(0, () => alert('Called')),
},
})
var o1 = new Class
var o2 = new Class
o1.batch([o2], function () {
o1.set('foo', 123) // alerts
o2.set('foo', 123) // doesn't
})
Instead, assign such a property or hook in init
:
var Class = Sqimitive.Core.extend({
events: {
init: function () {
this.fuse('change', batchGuard(...))
},
},
})
Remember that func
is called once per batch, not per batch per object.
This matters if using batchGuard() to handle events originating from
different objects:
var Collection = Sqimitive.Base.extend({
_childEvents: ['change'],
})
var col = new Collection
col.on('.change', batchGuard('.c', function ...))
var child1 = col.nested(1)
var child2 = col.nested(2)
child1.batch([child2], function () {
child1.set('foo', 1)
child2.set('bar', 2)
})
In the above example, batchGuard() calls func
just once and options
that func
receives may come from either of the batched sqimitives which
means options.batch
is arbitrary and does not list all queued events
(it has either change_foo
or change_bar
). In case multiple origins
are possible, instead of batch
read options.batched[i][1]
– but
only process objects (...[0]
) that you expect:
col.on('.change', batchGuard('.c', function (child, name, now, old, options) {
// WRONG:
options.batch.forEach(...)
}))
col.on('.change', batchGuard(4, function (child, name, now, old, options) {
// WRONG:
options.batched.forEach(function ([child, batch]) {
batch.forEach(...)
})
}))
col.on('.change', batchGuard('.c', function (child, name, now, old, options) {
// CORRECT:
options.batched.forEach(function ([child, batch]) {
if (col.nested(child)) { // or: child instanceof ..., etc.
batch.forEach(...)
}
})
}))
Checking objects is important because a batch may include unrelated sqimitives:
child1.batch([child2, unrelated], function () {
child1.set('foo', 1)
child2.set('bar', 2)
unrelated.set('quux', 3) // not part of col, must ignore in our guard
})
Defined in: main.js, lines 1560-1604 (45 lines) • Show code
batchGuard: function (index, func, options) {
if (typeof index == 'string') {
if (!index.match(/^[.+=]*c_?$/)) {
throw new Error('batchGuard: Invalid index: ' + index)
}
// function ([...] [name,] now, old, options)
index = index.length + (_.last(index) == '_' ? 0 : 2)
}
options || (options = {})
func = Core.expandFunc(func, options.cx)
var seen = options.seen || (options.seen = new Set)
var unique = Core.unique('bg') + 'bg'
function batchGuard_() {
var eventOptions = arguments[index]
var id = eventOptions.batchID
if (!id) {
throw new Error('batchGuard: No batchID in ' + index)
}
if (seen.size == seen.add(id).size) {
options.skip && options.skip.apply(options.cx || this, arguments)
} else {
var event = unique + id
var remaining = 0
_.forEach(eventOptions.batched, function (item) {
if (item[1].length) {
remaining++
item[1].push([event])
item[0].once(event, function () {
--remaining || seen.delete(id)
})
}
})
return func.apply(this, arguments)
}
}
Core.trace && (batchGuard_.trace = arguments)
return batchGuard_
},
Modifiers: static
Part of: tag_Utilities
Returns a version of the argument with recursively-copied arrays and {}
objects so that any modification to either obj
or the returned value
(obj
’s copy) won’t affect its counterpart.
deepclonebasedeepClone() is used in Core.constructor to copy non-shared
properties (_shareProps). Think of it as of recursively calling
un:clone() or jq:extend() or using LoDash’s cloneDeep()
. It’s
deliberately dumb to remain simple and will attempt to copy everything,
even classes like Date
, RegExp
, DOM nodes, etc. (resulting in invalid
objects).
Use deepClone() when need to clone an object just once or twice. Use deepCloner() when doing this often (such as upon new object instantiation) – it’s faster by 8-9 times and produces code close to what you’d have written by hand.
Defined in: main.js, lines 1174-1187 (14 lines) • Show code
deepClone: function (obj) {
if (typeof obj == 'object' && obj != null) {
if (_.isArray(obj)) {
obj = obj.map(Core.deepClone)
} else {
obj = _.assign({}, obj)
for (var prop in obj) {
obj[prop] = Core.deepClone(obj[prop])
}
}
}
return obj
},
Modifiers: static
Part of: tag_Utilities
Returns a function constructing obj
with recursively-copied arrays and
{}
objects – compiled version of deepClone().
Name | Types | Notes |
---|---|---|
obj | mixed | |
options | missing create new cloner |
non-scalar values are held in shared array, passed to the returned
function via s variable. |
object merge multiple cloners |
Types | Notes |
---|---|
function returning obj copy | |
object if no arguments or if options
given, call its compile() to get the function |
from deepclonebase
deepClone() is used in Core.constructor to copy non-shared
properties (_shareProps). Think of it as of recursively calling
un:clone() or jq:extend() or using LoDash’s cloneDeep()
. It’s
deliberately dumb to remain simple and will attempt to copy everything,
even classes like Date
, RegExp
, DOM nodes, etc. (resulting in invalid
objects).
Use deepClone() when need to clone an object just once or twice. Use deepCloner() when doing this often (such as upon new object instantiation) – it’s faster by 8-9 times and produces code close to what you’d have written by hand.
Defined in: main.js, lines 1206-1255 (50 lines) • Show code
deepCloner: function (obj, o) {
o = o || {
func: ['return '],
shared: [],
compile: function () {
// Clearing o.func to free old members which may be quite numerous.
return _.partial(new Function('s', o.func.splice(0).join('')), o.shared)
},
}
if (!arguments.length) { return o }
switch (typeof obj) {
case 'string':
if (obj.length < 50) {
o.func.push(JSON.stringify(obj))
break
}
default:
o.func.push('s[' + (o.shared.push(obj) - 1) + ']')
break
case 'object':
if (obj == null) {
// Fall through.
} else if (_.isArray(obj)) {
o.func.push('[')
for (var i = 0; i < obj.length; i++) {
Core.deepCloner(obj[i], o)
o.func.push(',')
}
o.func.push(']')
break
} else {
o.func.push('{')
for (var prop in obj) {
o.func.push(JSON.stringify(prop) + ':')
Core.deepCloner(obj[prop], o)
o.func.push(',')
}
o.func.push('}')
break
}
case 'undefined':
case 'boolean':
case 'number':
o.func.push(obj + '')
}
return arguments[1] ? o : o.compile()
},
Modifiers: static
Expands a function reference func
of object obj
(this
if not given)
into a real Function
.
expandFunc() is used in on(), events and others to short-reference the instance’s own methods.
If func
is a string and contains a dot or a dash (.-
) – returns
masked (masker()) version of this method (mask
starts with the first
such character). If it’s a string without them – returns a function that
calls the method named func
on obj
(or this
if omitted). In other
cases returns func
as is (if obj
is omitted) or _.bind(func, obj)
(if obj
is given).
Defined in: main.js, lines 954-970 (17 lines) • Show code
expandFunc: function (func, obj) {
if (typeof func == 'string') {
var parts = func.split(/([.-][\d.-]*)$/)
if (parts.length > 1) {
return Core.masker(parts[0], parts[1], obj)
} else {
function expandFunc_() {
var callCx = obj || this
return callCx[func].apply(callCx, arguments)
}
Core.trace && (expandFunc_.trace = arguments)
return expandFunc_
}
} else {
return obj ? _.bind(func, obj) : func
}
},
Modifiers: static
Part of: tag_Extension
Creates a subclass of the class on which extend() is called.
Name | Types | Notes |
---|---|---|
name | string | Optional convenience string displayed in the debugger (as
the function/constructor – “class” name). Defaults to name of base
class.
|
protoProps | object | New instance fields (properties or methods). May contain special non-field keys (see mixIn()). |
staticProps | object | New static fields. |
protoProps
fields become accessible as (new MyClass).instanceSomething()
while staticProps
– as MyClass.staticSomething()
.
Any argument may be null
or omitted. If all are such then you get a
copy of the base class (and yet BaseClass !== SubClass
).
Other notes:
__super__
(a static property pointing to the base class), calls ::mixIn() and
resolves _childClass if it’s a string (since it can’t be done before
the prototype is created).from mixInDoes
mixIn()
if newClass
contains
the mixIns
key; this is recursive), overwrites fields of this
with
deepClone-s of those in newClass
(or merges according to
_mergeProps), adds staticProps
, hardwires events into class
definition (fuse()) and calls finishMixIn()
.staticProps
argument is given, it replaces
protoProps.staticProps
. As expected, this key (or argument) is
applied by mixIn() after applying staticProps
of mixIns
.
var MixIn = {
staticProps: {sp: 'm'}
}
var Class = Sqimitive.Base.extend({
mixIns: [MixIn]
}, {
sp: 'c'
})
// Equivalent:
var Class = Sqimitive.Base.extend({
staticProps: {sp: 'c'},
mixIns: [MixIn]
})
// Above, Class.sp is 'c'. However, if mixing-in later, it'd be 'm':
Class.mixIn(MixIn)
Defined in: main.js, lines 638-684 (47 lines) • Show code
extend: function (name, protoProps, staticProps) {
// this = base class.
// Only works in strict mode which disconnects parameter vars from members
// of arguments.
name = typeof arguments[0] == 'string' ? ap.shift.call(arguments) : this.name
var child = extend(name, this, arguments[0])
//! +ig
// Since base class has its own __super__, make sure child's (set up by
// extend() above) isn't overwritten.
_.assign(child, this, {__super__: child.__super__})
// Ensure changing these in a subclass doesn't affect the parent:
// var Sub = Sqimitive.Base.extend()
// Sqimitive.Base._mergeProps.length //=> 3
// Sub._mergeProps.push('new')
// Sqimitive.Base._mergeProps.length //=> 4
child._mergeProps = (child._mergeProps || this._mergeProps).concat()
child._shareProps = (child._shareProps || this._shareProps).concat()
//! +ig
// Function.prototype.length confuses "isArrayLike" functions.
// Just `[delete Core.length`] doesn't work.
Object.defineProperty(child, 'length', {value: 'NotAnArray'})
name && Object.defineProperty(child, 'name', {value: name})
if (arguments[1]) {
(arguments[0] = arguments[0] || {}).staticProps = arguments[1]
}
arguments[0] && child.mixIn(arguments[0])
// _childClass is technically part of Base, not Core but doing it here for
// simplicity.
//
// String class path is relative to the base class; instead of searching
// all prototypes to find where this property was introduced (without this
// all children will reference to them as the base class), we "fixate" it
// when declaring the class. Done here, not in mixIn(), to avoid questions
// like "is the string, if given by a mix-in, relative to the mix-in or
// the target class?".
if (typeof child.prototype._childClass == 'string') {
child.prototype._childClass = [child, child.prototype._childClass]
}
return child
},
Modifiers: static
Invokes event handlers in response to firing an event (see instance .fire()).
funcs
is an array of event registration objects of an internal format.
::fire() calls each handler in order according to its type (such as
expecting a fixed number of arguments, accepting current result value,
affecting return value, etc. according to prefix
in on(), see
evtref) while giving it args
(array or Arguments
).
If a handler returns something other than undefined
and it’s eligible
for changing return value (as it’s the case for +event
and =event
,
see evtpf), then current result is replaced by that handler’s return
value. Returns the ultimate return value after calling every handler,
unless stopped by any eobj
.post callback setting eobj.stop
to
true
(see the example in post).
Other notes:
fire()
.funcs
can be non-isArray
, in this case undefined
is returned.funcs
is cloned; members added or removed while ::fire() is running
are ignored; changes to a member (eobj
) affect the call only if that
member wasn’t yet called._unregHandler()
clears eobj.func
). See the second example
below.Defined in: main.js, lines 1331-1387 (57 lines) • Show code
fire: function (funcs, args, inBatch) {
var res
args = args || []
// funcs can be undefined, e.g. when firing change_OPT.
// It may be non-array if non-own property was read (_events.toString).
//
// Cloning function list to ignore the outside changes (e.g. when a
// handler is removed from the same event as it's being fired; may happen
// when once() is used).
_.isArray(funcs) && funcs.concat().some(function fire_(eobj) {
if (inBatch != null && eobj.batch !== inBatch) { return }
Core.trace &&
Core.lastFired.push(_.assign({self: this}, eobj)) > 50 &&
Core.lastFired.shift()
// func is null if off() was called on a wrapped handler.
// Non-null eobj.args is implementing "ev__" (#argDanger).
var call = eobj.func && (eobj.args == null || eobj.args == args.length)
if (!call) {
// If the handler is not to be called, pretend it's not there and call
// the handlers it wraps (supList),
var thisRes = eobj.sup ? eobj.sup(eobj.cx || this, args) : undefined
} else {
if (!eobj.sup && !eobj.res) {
var thisArgs = args
} else {
var thisArgs = ap.slice.call(args)
eobj.sup && thisArgs.unshift(eobj.sup)
eobj.res && thisArgs.unshift(res)
}
// A benchmark avoiding Function's apply() was done on Chrome
// using a direct call construct from EventEmitter3:
// switch (args.len) { case 0: f(); case 1: f(args[0]); ... }
// It made no differences at all in performance.
var thisRes = eobj.func.apply(eobj.cx || this, thisArgs)
}
if (eobj.ret && thisRes !== undefined) {
res = thisRes
}
if (eobj.post && call) {
eobj.stop = false
// Attention: args may be an array-like object (often an Arguments),
// not a real array.
res = eobj.post.call(eobj.cx || this, eobj, res, args)
// eobj could be modified by post, including unsetting post.
if (eobj.stop) { return true }
}
}, this)
return res
},
Modifiers: static
Part of: tag_Events
Returns a function that fire()-s an event
with arguments of its call.
The returned function calls fire(event, ...firerArgs, ...givenArgs)
in context of cx
.
Name | Types | Notes |
---|---|---|
event | string | Like change_caption |
args | array | Push some parameters in front of the function’s arguments
(firerArgs ). |
cx | object | If not given then context is unchanged. |
Essentially, using firer() is just a short way of writing:
(function () { return this.fire(event, args.concat(arguments)) }).bind(cx)
Defined in: main.js, lines 1430-1443 (14 lines) • Show code
firer: function (event, args, cx) {
if (arguments.length > 1) {
args = Core.toArray(args)
var firer_ = function () {
return (cx || this).fire(event, args.concat( ap.slice.call(arguments) ))
}
} else {
var firer_ = function () {
return (cx || this).fire(event, arguments)
}
}
Core.trace && (firer_.trace = arguments)
return firer_
},
Modifiers: static
Part of: tag_Utilities
Returns a version of func
with arguments reordered according to mask
.
Name | Types | Notes |
---|---|---|
mask | number to skip that many leading arguments alike to no:rest() | |
null/omitted to assume the number 1 (skip first argument) | ||
string pattern maskerPattern | ||
func | string method name | Called on cx |
function | ||
cx | object | The context for func |
null/omitted use this | ||
args | array | Extra left-side arguments to func |
masker() is similar to LoDash’s rearg()
.
Masking is a way to work around argDanger and avoid writing callback function wrappers that only ignore or reorder arguments. It’s implicitly used in string events values since they are passed to expandFunc().
In a string mask
, each symbol maps arguments given to the masked
function (returned by masker) to arguments for the original func
(the
argument to masker). Each symbol represents a particular argument and
can be one of these:
Name | Types | Notes |
---|---|---|
dot | . | Gets argument at index of this dot in the mask string
(-..-. equals to -23-5 ). |
dash | - | Ignores argument (doesn’t give to func ); trailing dashes
are meaningless (arguments past the end of mask are never given unless
mask is a number). |
number | 1-9 | Gets argument by index: 1 gets the first masked
argument, etc. |
For example, if the wrapped function received arguments arg1, arg2,
arg3
then the mask
of -.1
(same as -21
) gives the original
func
arguments arg2, arg1
.
Empty mask
passes zero arguments (as do -
, --
, etc.).
Note: the mask
of '3'
(string) is different from 3
(number) -
'3'
passes 3rd wrapper’s argument as the first func
’s argument
while 3
skips first 3 arguments and passes all others.
Defined in: main.js, lines 1108-1134 (27 lines) • Show code
masker: function (func, mask, cx, args) {
mask == null && (mask = 1)
var isMethod = typeof func == 'string'
var isSkipFirst = typeof mask == 'number'
args || (args = [])
if (!isSkipFirst) {
mask = mask
.replace(/\./g, function (ch, i) { return i > 8 ? '-' : i + 1 })
.replace(/[^1-9.\-]+/g, '-')
.replace(/-+$/, '')
}
function masker_() {
var callCx = cx || this
var callArgs = isSkipFirst ? args.concat(_.rest(arguments, mask)) : args.concat()
for (var i = 0; i < mask.length; i++) {
mask[i] != '-' && callArgs.push(arguments[mask[i] - 1])
}
return (isMethod ? callCx[func] : func).apply(callCx, callArgs)
}
Core.trace && (masker_.trace = arguments)
return masker_
},
Modifiers: static
Part of: tag_Extension
Extends this class with a behaviour of another “class” (a “mix-in”).
The instance .mixIn() works the same way but allows extending a particular object instance on run-time. Its description follows.
from mixInDesc
Name | Types | Notes |
---|---|---|
this | The object receiving new “mixed-in” fields (for static
::mixIn() this is the “child” class). | |
newClass | object | The mix-in, the object “mixed into” this . |
options | An arbitrary value (usually an object) given to
newClass .finishMixIn() , allowing creation of parametrized mix-ins
(basically, generics). |
Possible newClass
keys:
Name | Types | Notes |
---|---|---|
staticProps | object | Static fields made available as
newClass.something |
events | object | Event listeners (see events). Because this is not a real field, keys in |
finishMixIn | function | Called before returning.
|
mixIns | array | Each item is either a mixed-in class or an array:
[class, options] |
* | Other keys represent instance fields (the protoProps argument of
extend())._mergeProps is respected, but of A string form of _childClass is only allowed in extend(), not here; other forms (array, object) are allowed in both places. |
Warning: this
is modified in-place; no new class is created
(mixIn()
returns no value). If you want a base class without a given
mix-in and a subclass with that mix-in – first extend() the base class
and then mix into the new sub-class:
// CORRECT:
var Base = Sqimitive.Base.extend()
var Sub = Base.extend({mixIns: [SomeMixIn]})
// WRONG: will modify Base, not return a new subclass:
var Base = Sqimitive.Base.extend()
var Sub = Base.mixIn(SomeMixIn)
Other notes:
mixIn()
if newClass
contains
the mixIns
key; this is recursive), overwrites fields of this
with
deepClone-s of those in newClass
(or merges according to
_mergeProps), adds staticProps
, hardwires events into class
definition (fuse()) and calls finishMixIn()
.mixin()
is of a similar purpose.elEvents
elEventsMixInBase lists Base.elEvents in _mergeProps so the former are
merged but unlike with newClass.events
keys (which can never have a
conflict and be dropped), keys in elEvents
get overridden on name
collisions. This is sometimes desirable (to override the parent’s
handler), sometimes not (then use a unique .ns
suffix):
var Base = Sqimitive.Base.extend({
elEvents: {
click: function () { alert("Click-click-click!') },
},
})
var ChildA = Base.extend({
elEvents: {
click: function () { alert("Overridden!') },
},
})
var ChildB = Base.extend({
elEvents: {
'click.new-in-child-B': function () { alert("Combined!') },
},
})
// in ChildA, 'click' has 1 listener (Base's dropped)
// in ChildB, 'click' has 2 listeners (Base's and ChildB's)
mixIns
is applied before setting other properties which allows
extending mix-ins themselves (later mix-ins override preceding just like
with normal inheritance on classes). For example:
var ParentMixIn = {
someEvent: function () { /* parent */ },
]
var ChildMixIn = {
mixIns: [ParentMixIn],
events: {
someEvent: function () { /* child */ },
},
}
var myClass = Sqimitive.Base.extend({
mixIns: [ChildMixIn],
})
// myClass had ParentMixIn added, then ChildMixIn, and now has two
// listeners on someEvent
ParentMixIn
could also have the mixIns
property to specify its own
parent.
In the above example, the mix-in specified its parent, which is usually
intuitive. Still, it could be specified in the final class’ mixIns
alone:
var ParentMixIn = {
someEvent: function () { /* parent */ },
]
var ChildMixIn = {
// mixIns property is missing.
events: {
someEvent: function () { /* child */ },
},
}
var myClass = Sqimitive.Base.extend({
// Added ParentMixIn in front of ChildMixIn.
mixIns: [ParentMixIn, ChildMixIn],
})
// or (equivalent, no mixIns property, mixIn() calls instead):
var myClass = Sqimitive.Base.extend()
myClass.mixIn(ParentMixIn)
myClass.mixIn(ChildMixIn)
// or (equivalent for a particular object instance):
var myClass = Sqimitive.Base.extend()
var obj = new myClass
obj.mixIn(ParentMixIn)
obj.mixIn(ChildMixIn)
// in any case, MyClass (or obj) has 'someEvent' firer() with
// 2 listeners: of ParentMixIn and of ChildMixIn
Warning: calling mixIn()
in finishMixIn()
would have a
different effect. If ChildMixIn
were defined as follows then MyClass
or obj
would have someEvent
not as a firer() but as the
ParentMixIn
’s function (because it was mixed-in after ChildMixIn
and
overrode its events
handler):
var ChildMixIn = {
finishMixIn: function (newClass) {
newClass.mixIn(ParentMixIn)
},
events: {
someEvent: function () { /* child */ },
},
}
This will override the handler introduced in the declaration of Class
for the same reason – MixIn
is added to Class
after Class
’ own
fields:
var MixIn = {
some: function () { ... },
}
var Class = Sqimitive.Base.extend({
events: {
some: function () { ... },
}
})
Class.mixIn(MixIn)
This is the correct way (using mixIns
property when extend()’ing
Class
):
var MixIn = {
some: function () { ... },
}
var Class = Sqimitive.Base.extend({
mixIns: [MixIn],
events: {
some: function () { ... },
}
})
See the source code for details.
B
and subclass C
, adding mix-ins to B
after C
has been extend()’ed from B
when C
declares events will lead to
C
not having events of the newly mixed-in objects of B
.B
are fuse()’d and their eobj
-s are
shared among all subclasses of B
and should not be changed (list of
events may change, just not properties of inherited handlers): var ClassB = Sqimitive.Core.extend({
events: {change: 'render'},
})
ClassB.prototype._events.change[0].post //=> undefined
var ClassC = ClassB.extend()
ClassC.prototype._events.change[0].post = function () { ... }
ClassB.prototype._events.change[0].post //=> Function
// In this version instances' _events are deepClone()'d so changing it
// (not the prototype) doesn't affect other classes/objects:
var obj = new ClassC
obj._events.change[0].post = 'foo'
ClassB.prototype._events.change[0].post //=> still Function
Defined in: main.js, lines 696-698 (3 lines) • Show code
mixIn: function (newClass, options) {
return this.prototype.mixIn(newClass, options)
},
Modifiers: static
Extracts portions of the given event identifier as recognized by on().
Returns an object with keys: batch
(leading ^
), priority
(number
before ^
), prefix
(like +
, see evtpf), args
(number of
trailing _
or null), event
(event name, everything else) and
trace
(null
if ::trace is off).
Errors if str
doesn’t look like a proper event reference.
Defined in: main.js, lines 1273-1282 (10 lines) • Show code
parseEvent: function (str) {
// Dots are used in _forward'ed event names, and we need prefix symbols
// (+-=) and ^ in the middle of event name for the same reason:
// '.2^-change'.
var match = str.match(/^(\^?)(-?\d+\^)?([+\-=]?)(.+?)(_*)$/)
if (!match) { throw new SyntaxError('Bad event name: ' + str) }
return {batch: !match[1], priority: parseInt(match[2]) || 0,
prefix: match[3], event: match[4], args: match[5].length || null,
trace: Core.trace && (new Error).stack}
},
Modifiers: static
Part of: tag_Utilities
Returns a function accepting an object and returning value of the
property at prop
, accessible via that object.
Name | Types | Notes |
---|---|---|
prop | string dotted property path | |
array already split path, empty array
to return the a ’rgument itself (or result of calling it) | ||
other stringified and split | ||
args | array | List of argument
lists for function -type properties. |
mixed = [[args]] | ||
omitted = [] |
The returned function (f
) expects one argument (a
). When called, f
walks prop
, treating each item as a key (“own” or not) in the “current”
object (which starts as a
) and returns the last “current” object.
If there is a function
at that key (or a
itself is one) and its
name
is not a string starting with a capital letter (i.e. not a class
constructor), f
calls it in previous “current” object’s context
(undefined
if calling a
) with the next unused member of args
([]
if none) and stores the result as the new “current” object.
f
returns undefined
when trying to descend into undefined
/null
“current” value.
picker() is similar to un:result() in Underscore and LoDash.
Defined in: main.js, lines 884-915 (32 lines) • Show code
picker: function (prop, args) {
_.isArray(prop) || (prop = (prop + '').split('.'))
if (arguments.length > 1 && !_.isArray(args)) {
args = [[args]]
}
function picker_(obj) {
var cx
var ai = 0
function picker_call() {
if (typeof obj == 'function' && !/^[A-Z]/.test(obj.name)) {
obj = obj.apply(cx, args && args[ai++])
}
}
picker_call()
for (var i = 0; i < prop.length; i++) {
if (obj == null) { return }
cx = obj
obj = obj[prop[i]]
picker_call()
}
return obj
}
Core.trace && (picker_.trace = arguments)
return picker_
},
Modifiers: static
Simply an empty function that returns undefined
.
stub() is similar to un:noop() in Underscore and LoDash.
Otherwise, if you are not a performance purist you can just use
function () {}
or new Function
:
var MySqim = Sqimitive.Base.extend({
success: function () { },
})
var my = new MySqim
my.on('success', function () { alert('Good!') })
//=> my._events.success.length is 2
Special values stub() and undefined
are only recognized in
properties, not events:
var MySqim = Sqimitive.Base.extend({
events: {
// Registers new hook that is called but has no effect on anything.
success: Sqimitive.Base.stub,
// Itself does nothing but previously registered hooks are removed.
'=success': Sqimitive.Base.stub,
// Simply wrong, undefined is not callable.
success: undefined,
},
})
var my = new MySqim
my.on('success', function () { alert('Good!') })
//=> my._events.success.length is 2 (existing stub handler kept)
my.on('success', Sqimitive.Base.stub)
//=> my._events.success.length is 3 (new stub handler added)
my.on('success', undefined)
// simply wrong, undefined is not callable
Defined in: main.js, line 757 • Show code
stub: function Sqimitive_stub() { },
Modifiers: static
Part of: tag_Utilities
Attempts to cast value
into a native Array
object.
The Arguments
object becomes an array, Array
(no:isArray()) is
returned as is while anything else is wrapped into an array to become its
sole member. This means that false
, null
and undefined
all result
in [value]
, not in []
.
Note: toArray() does not clone the result:
var a = [5]
Sqimitive.Core.toArray(a).push('foo')
// a is now [5, 'foo']
Defined in: main.js, lines 1633-1641 (9 lines) • Show code
toArray: function (value) {
if (_.isArguments(value)) {
return ap.slice.call(value)
} else if (!_.isArray(value)) {
return [value]
} else {
return value
}
},
Modifiers: static
Part of: tag_Utilities
Returns a sequential number starting from 1
that is guaranteed to be
unique among all calls to unique() with the same prefix
during this
session (page load, etc.).
_cid receives one such value in constructor.
unique() is similar to un:uniqueId() in Underscore and LoDash.
Well-known prefix
’es:
Name | Types | Notes |
---|---|---|
p | Used in _cid (sqimitive’s ID). | |
e | Used in on() (event handler’s ID). | |
o | Used in set() (operation’s ID). | |
b | Used in batch() (batch’s ID – group of operations). | |
bg | Used in batchGuard() (guard’s instance). |
Defined in: main.js, lines 787-789 (3 lines) • Show code
unique: function (prefix) {
return this._unique[prefix] = 1 + (this._unique[prefix] || 0)
},
Modifiers: protected
Part of: tag_Options
Returns information about currently active batch() for use in batched events.
Types | Notes |
---|---|
object cloned options with extra keys |
Name | Types | Notes |
---|---|---|
id | number | Active batch’s identifier as given to batch()’s func |
object | object | Optional keys to merge into result (batch-specific keys override them). |
Call _batchOptions() once for each “operation” when adding custom batch events, then pass the result to all operation’s events (e.g. ifSet() gives both change_OPT and change the same options object).
You may pass the same object to all events of the same operation (to allow clients persist data across related events) or you may clone it (to avoid interference) – but never pass it to events of different operations or batches.
Defined in: main.js, lines 3547-3586 (40 lines) • Show code
_batchOptions: function (id, options) {
// It might be tempting to optimize cloning away if options was given by
// mutating and returning options - but that would be erroneous:
//
// var sq1 = new Sqimitive.Base
// var sq2 = new Sqimitive.Base
// var options = {}
//
// sq1.batch(null, function () {
// sq1.set('a', 1, options)
//
// sq2.batch(null, function () {
// sq2.set('b', 2, options)
//
// // Expected batched events at this point:
// // sq2._batch = [['change_b', 2, undefined, {batchID: 2, ...}]]
// // sq1._batch = [['change_a', 1, undefined, {batchID: 1, ...}]]
// //
// // If _batchOptions() called by set() did not clone options,
// // sq1._batch would be [['change_a', ... {batchID: 2, ...}]] -
// // note the different batchID. This is because change_a's
// // options object was mutated by sq2.set() and now all
// // operations where it was used have wrong values (originating
// // from last called set()) for those operations.
// //
// // For example, when change_a is fired its options.batch must
// // be [['change_a', 1, undefined, {options...}]] but because
// // this key (batch) was overridden (in options) by sq2.set()
// // and because change events of b were already dispatched,
// // options.batch for change_a would be in fact []!
// })
// })
return _.assign({}, options, {
batch: this._batch,
batched: this._batched,
batchID: id,
operationID: Core.unique('o'),
trace: Core.trace && (new Error).stack,
})
},
Modifiers: protected
Part of: tag_Events
Forwards events
occurring on sqim
to this
, with arguments
prepend
’ed.
Types | Notes |
---|---|
sqim |
Forwarding is done by firing “prefix + event_name” on this
(the object
on which _forward() is called) with prepend
([sqim]
if omitted)
pushed in front of original event’s arguments. event_name
is a complete
reference string as given to parseEvent(), e.g. =foo_
.
_forward() is used to set up _childEvents, with the prefix
of
.
.
Defined in: main.js, lines 3511-3523 (13 lines) • Show code
_forward: function (prefix, events, sqim, prepend) {
_.forEach(events.concat(), function (event) {
var name = prefix + event
function _forward_() {
ap.unshift.apply(arguments, prepend || [sqim])
return this.fire(name, arguments)
}
Core.trace && (_forward_.trace = [prefix, events, sqim, prepend])
sqim.on(event, _forward_, this)
}, this)
return sqim
},
Part of: tag_Events
A utility method for tracking object connections.
this.autoOff(sqim, {}) // add sqim to the list
this.autoOff(sqim, {foo: ...}) // ...and hook some events; cx = this
this.autoOff(sqim, ['foo', ...]) // equivalent
this.autoOff(sqim, {foo: ...}, null) // ...cx = sqim
this.autoOff(sqim, {foo: ...}, other) // ...cx = other
this.autoOff() // off() all sqims tracked so far and clear the list
this.autoOff(sqim) // off() and un-list sqim, only if it was tracked
this.autoOff(sqim, true) // just un-list sqim, keep hooks on it
// Iterate over tracked objects:
var found = this.autoOff(sqim => sqim.get('foo'))
autoOff() has three main forms – tracking, untracking and iterating:
sqim
(an object) to this object’s Set
of tracked objects
(_autoOff
), optionally hooking events
on it – returns sqim
.this
on them -
returns this
.this
on it – returns
sqim
.func
for every object
until it returns a value that is not undefined
or null
; autoOff()
returns this value, or last func
’s result, or undefined
if the list
was empty.The list is not used by Sqimitive but your application can use it to
unbind all listeners added by this object to other objects in one go. For
example, you can do this.autoOff()
when this
is about to be
destroyed so that events on other objects to which this
had any
connections won’t trigger its handlers.
events
, if given and non-true
, is an object in on() format (keys
are comma-separated event references and values are their handlers -
expandFunc()) or an array (hooked in order): ['evt', func, 'evt2',
func2, ...]
. Commas in event references are allowed even though on()
only accepts them when fuse’ing.
With events
, cx
sets the context in which handlers will be called. If
cx
is null
then sqim
is used, if no cx
argument is given then
this
is used (i.e. the object on which autoOff() was called).
sqim
’s on() always receives cx
of this
.
Other notes:
events
(hooks will be added
twice); doing just autoOff(sqim, {})
twice is fine, and duplicates
won’t inflate the list because it’s a Set
.autoOff(sqim)
doesn’t call off() if sqim
wasn’t tracked
(better for performance). This works if the code doesn’t manually set up
hooks (on()) on objects-to-be-autoOff’d, which is recommended. If it
does, do this.autoOff(sqim).off(this)
or this.autoOff(sqim, {})
and later this.autoOff(sqim)
.func
to track or untrack
objects – func
will receive each object exactly once. Re-tracking the
same object until the iterating autoOff() returns will call func
twice for that object (Set
’s behaviour). From func
, calling
autoOff() without arguments will call func
for every object
remaining to be iterated over before that call to autoOff() was made,
regardless of subsequent autoOff() calls.from chevaoff
As a general rule, autoOff() is not used for permanently connected
objects, i.e. ones that are destroyed at the same time as this
. Such
objects are typically set to “_protected” properties and init’ialized
during construction:
var MyGame = Sqimitive.Base.extend({
// Created together with MyGame and only referenced by MyGame:
_timer: null,
events: {
init: function () {
this._timer = new MyTimer({interval: 1000})
this._timer.on({expired: () => alert("Time's up!")})
// on() instead of autoOff({expired: ...})
},
},
})
from unnestedoff
Base’s implementation of unnested() calls sqim
.off(this
) to
unregister all event handlers that might have been previously attached to
the removed child by this instance (provided they were not hardwired with
fuse() and used cx === this
).
Because of this, there’s no need to track and explicitly unsubscribe from children (regardless of _owning):
var MyClass = Sqimitive.Base.extend({
events: {
init: function () {
var child = this.nest(new MyClass.Child)
this.autoOff(child, {changed: 'recalc'})
},
unnested: function (sqim) {
this.autoOff(sqim)
},
},
recalc: ...
})
The above is redundant and can be simplified, provided that the context
given to on()
is this
as below:
var MyClass = Sqimitive.Base.extend({
events: {
init: function () {
var child = this.nest(new MyClass.Child)
// this (_parent) will call off(this) on child when child is unnested().
child.on('changed', 'recalc', this)
},
},
recalc: ...
})
If you are hooking all children then use _childEvents to further simplify the task:
var MyGame = Sqimitive.Base.extend({
_childEvents: ['changed'],
events: {
'.changed': 'recalc',
},
recalc: ...
})
autoOff() prevents sqim
from being garbage-collected by creating a
reference from this
to sqim
. (WeakSet
would be a solution if only
it supported enumeration.)
Normally, sqim.on(..., this)
results in a reference to this
being
stored in sqim
so that if sqim
is not referenced anywhere else (e.g.
it is not nest’ed) then it will be freed even if this
remains alive.
However, autoOff() additionally lists sqim
in the _autoOff
Set
of
this
, creating a two-way link; now both sqim
and this
may only be
freed together. Imagine a long-term “server” object (this
) and
short-term “clients” (sqim
): this
-server autoOff()-s (tracks) new
sqim
-client, then unnest()-s the latter at some point. Even though
clients are not referenced anywhere except server’s _autoOff
, they will
be kept alive until the server itself is destroyed.
Addressing this is application-specific but one common way is to hook
sqim
’s unnest (or Base.remove()) and remove it from _autoOff
(this
= server):
this.autoOff(client, {
...,
remove: function () {
this.autoOff(client)
},
})
Defined in: main.js, lines 3070-3126 (57 lines) • Show code
autoOff: function (sqim, events, cx) {
var list = this._autoOff
switch (arguments.length) {
case 0:
if (list && list.size) {
this._autoOff = null
list.forEach(function (sqim) { sqim.off(this) }, this)
}
return this
case 1:
case 2:
if (typeof sqim == 'function') {
var res
list && list.forEach(function (item) {
// Can't break out of Set's forEach().
res = res == null && sqim.call(events || this, item)
}, this)
return res
} else if (arguments.length == 1 || events === true) {
if (list && list.delete(sqim)) {
events === true || sqim.off(this)
}
return sqim
}
cx = this
default:
;(list || (this._autoOff = new Set))
.add(sqim)
var self = this
function bind(func, name) {
// For sqim.off(this) to work, on()'s cx must be this. If user wants
// another context, bind the func.
if (cx !== self) {
func = Core.expandFunc(func, cx || sqim)
}
var names = name.split(', ')
for (var i = 0, l = names.length; i < l; i++) {
sqim.on(names[i], func, self)
}
}
if (_.isArray(events)) {
for (var i = 1, l = events.length; i < l; i += 2) {
bind(events[i], events[i - 1])
}
} else {
_.forEach(events, bind)
}
return sqim
}
},
Part of: tag_Options
Calls func
and defers certain events it has produced on this
and
sqims
until func
returns.
batch() is the backend used in Sqimitive to defer change and change_OPT events produced by ifSet() (and its set() wrapper). Clients may use it for other events.
Types | Notes |
---|---|
mixed as returned by func |
Name | Types | Notes |
---|---|---|
sqims | null | Sqimitives to defer and to share the same batch ID
with. If array, fails if this or any sqims has an active batch.
First member is ignored if it’s this . If you need several sqimitives
to be batched but don’t require special effects of having them in one
batch, do nested batch() calls with sqims of null . |
array | ||
func | Receives current batch id . May push to sqim._batch . | |
cx | object | The context for func |
null/omitted use this |
Some options may be related so that changing one causes a refresh based
on values of other options. For example, 2D coordinates consist of x
and y
; set()’ing them one by one (e.g. first x
, then y
) will
cause multiple refreshes but only the last will use actual (new) values
and others may use partially updated state (resulting in an object moving
from x1
/y1
to x2
/y1
, then to x2
/y2
rather than straight to
x2
/y2
). batch() groups change events and fires them after setting
all values instead of after setting each value.
Once func
returns, the batch ends and batch() starts dispatching the
events. During this process, if a hook on a change event (generated by
func
) creates a new batch, that batch’s events will be dispatched after
events of the current batch. Therefore, batch() may return much later
than func
as there may be numerous pending batches.
Special fields in options
of batched events (added by
_batchOptions()):
Name | Types | Notes |
---|---|---|
batch | array | Events in the batch being dispatched, in this._batch
format; custom eventIndex property points to the entry handled by the
current ^ handler (naor); for regular non-^ handlers it’s null
and batch itself omits already fired events (current event is the
first member hence batch is never empty); can be mutated except for
the current member (eventIndex ’th if ^, else 0); doesn’t include
events of the later pending batches. |
batchID | int | A unique batch ID; tells if two change events
originated from the same func |
batched | array of [sqim, batch] | this and sqims given to the “main”
batch(), with their respective batch arrays.From inside a change listener, each
|
operationID | int | A unique identifier of the event group; tells if events originated from the same ifSet() call. |
trace | str | Set to the stack trace of the batch entry’s creation if Core.trace was enabled. |
null |
Other notes:
sqims
) of the same batch, options.batchID
of
their change events are equal but sqim._batch
are equal only for
events of the same sqimitive (IDs are shared but _batch
instances are
not).func
(or fails depending on sqims
).
batch() after func
(i.e. by a hook on a change) defers events to a
new group dispatched after the original func
’s events are done.sqims
are dispatched not in order of
their operation calls (e.g. O1
on sqim1
, O2
on sqim2
, O3
on
sqim1
) but in groups per each member of sqims
, starting from the
last one (O2
on sqim2
, O1
on sqim1
, O3
on sqim1
). Order
within a group is guaranteed but order of groups is an internal detail
that should not be relied upon.Normally, batch() calls accumulated events in order of changes so that last change dispatches its events last while events in response to a batch (by hooks on events it’s dispatched) are fired after that batch’s own events.
To illustrate the issue, consider this class:
Sqimitive.Base.extend({
_opt: {
original: '123',
linked: null,
},
events: {
change_original: function (now) { this.set('linked', now) },
change: function (opt) { console.log('change of ' + opt) },
},
})
When calling set('original', 'new')
on such an object, logically you
would expect these events: change_original
, change
of original
,
change_linked
, change
of linked
. However, with a naive
implementation handling change_original
would trigger change_linked
,
then change
of linked
and only then – change
of original
.
This would be also a problem if changing the same option from within its
own event handler because change
with the new value would be fired
first (since set() was called last), then when the original set()
returns it would also fire change
but this time with the original (now
old!) value.
Notwithstanding, rarely a handler must be notified of the change
immediately (sort of “out of band data”). batch() calls listeners whose
event name starts with ^
immediately after func
returns, even if
there are events of earlier-created batches yet to be dispatched. While
normal listeners can be seen as an asynchronous event bus, ^
are closer
to direct function calls.
For example, let’s assume two objects: one an array of 2D points, another
its index (array of points on the given Y axis). The index must be kept
in sync so that clients accessing it at any point in time see no
discrepancies with the main array (the only exception we allow is inside
the same batch() func
).
var Points = Sqimitive.Base.extend({
points: [],
add: function (point) {
this.points.push(point)
this.batch(null, id => this._batch.push(['added', point]))
},
added: Sqimitive.Core.stub,
})
var Index = Sqimitive.Base.extend({
byY: {},
events: {
init: function (opt) {
this.autoOff(opt.points, {
added: '_added',
//'^added': '_added',
})
},
},
_added: function (point) {
(this.byY[point.y] || (this.byY[point.y] = [])).push(point)
},
})
var points = new Points
var index = new Index({points})
points.add({x: 0, y: 0})
// index.byY = {0: [{...}]}
points.add({x: 1, y: 1})
// index.byY = {0: [{...}], 1: [{...}]}
The above implementation works but at the first glance only:
var points = new Points
var index = new Index({points})
points.on('change_foo', function () {
console.dir(index.byY) // (1)
points.add({x: 2, y: 2})
console.dir(index.byY) // (2)
})
points.add({x: 0, y: 0})
points.set('foo', 123)
console.dir(index.byY) // (3)
Both (1) and (2) output the same result: {0}
while (3) outputs
{0, 2}
. Why output of (2) is the same as of (3)? Because ifSet()
uses batch, it delays dispatching events of other batches (added
in
our case) until its own events are finished. change_foo
calls add()
which creates a batch (queued after the currently executing ifSet’s
batch) and pushes added
to it (to be dispatched after set('foo')
returns, hence (3) showing expected result). In contrast, if Index
hooks '^added'
then it becomes exempt from this delay and causes
_added()
to be called after points.add({x: 2, y: 2})
returns (or,
precisely, upon returning from func
id => ...
given to batch()
inside add()
) and the index to be updated.
^
makes it “closer” to direct function call but not exactly that: such
handler is called once the batch closes (func
returns), not once a
corresponding event if queued (“function is called”). Below, add()
runs
inside an already active batch so no index update takes place then:
points.batch(null, function () {
points.add({x: 3, y: 3})
// points._batch = [['added', {x: 3, y: 3}]]
console.dir(index.byY)
//=> {}
})
console.dir(index.byY)
//=> {3: [{...}]}
^
has no special meaning for dispatching initiated by fire() and,
consequently, in =
wrapped handlers (evtpf; compare eventIndex
with null
to determine the phase).
^
separates firing into two groups which are then ordered by priority.
This means that in response to a batch, ^0^event
will run before
-1^event
even though the latter has lower priority.
sqim.on('^0^event', () => console.log(0))
sqim.on('-1^event', () => console.log(-1))
sqim.fire('event') // outputs -1, 0
sqim.batch(null, function () {
this._batch.push(['event'])
}) // outputs 0, -1
You can defer arbitrary events from within func
by pushing an array to
sqim._batch
, with first member being event name and others being its
arguments. Because sqims
is rarely used, sqim
here is typically
this
.
For consistency with Sqimitive’s ifSet(), provide hooks with information (_batchOptions()) about the current batch.
this.batch(function (id) {
this._batch.push(['firstOff', 'pa', 'ra', 'ms'])
// Or, better:
var options = {batch: this._batch, batchID: id, operationID: Core.unique('o')}
this._batch.push(['firstOff', 'pa', 'ra', 'ms', options])
// Or, best:
this._batch.push(['firstOff', 'pa', 'ra', 'ms', this._batchOptions(id)])
})
// does fire('firstOff', ['pa', 'ra', 'ms'...]) when func returns
Once batched events start firing, the batch is considered “closed”. This
typically means that _batch
will no longer change, allowing heavy
update functions to process all batched events as one unit in response to
the first event such a function is interested in, remembering its batch
ID and ignoring subsequent events with the same ID. Remembering the ID
might be simpler than maintaining a hash of current state (_recalced
)
as in the earlier Rect
example:
var Rect = new Sqimitive.jQuery.extend({
_opt: {width: 0, height: 0},
_lastBatch: 0,
events: {
change_width: '_ifRecalc',
change_height: '_ifRecalc',
},
_ifRecalc: function (now, old, options) {
if (this._lastBatch != options.batchID) {
this._lastBatch = options.batchID
this._recalc()
}
},
_recalc: function () {
var w = this.get('width')
// ...
},
})
Or the ID itself may be seen as a kind of hash:
var Rect = new Sqimitive.jQuery.extend({
_opt: {width: 0, height: 0, _lastBatch: 0},
events: {
change_width: '_ifRecalc',
change_height: '_ifRecalc',
change__lastBatch: '_recalc',
},
_ifRecalc: function (now, old, options) {
this.set('_lastBatch', options.batchID)
},
_recalc: function () { /* as above */ },
})
Things get hairy if your “_ifRecalc
” handler is attached to multiple
sqimitives and processes all batched sqimitives as one, rather than each
batched sqimitive’s events as one:
function update(options) {
if (!skip(options)) { // read below
_.each(options.batched, function (item) {
// item[0] is a sqimitive.
_.each(item[1], function (event) {
// Any change must cause an update. Or can check event[1] if
// only some _opt'ions cause it. Don't use event[0] == 'change_OPT'
// as it will never match since change_OPT is fired prior to
// change and is already removed from batched by the time update()
// runs.
if (event[0] == 'change') {
// ...
}
}
sqim1.on('change', (name, now, old, options) => update(options)) // (1)
sqim2.on('change', (name, now, old, options) => update(options)) // (2)
sqim3.on('change', (name, now, old, options) => update(options)) // (3)
Now, update()
should ignore batches that it has seen. Previously used
!=
fails short since there may be events of other sqimitives fired
between batched events of one sqimitive, as below:
var lastBatch = 0
function skip(options) {
if (lastBatch != options.batchID) {
lastBatch = options.batchID
} else {
return true
}
}
sqim3.on('change', function () { // (4)
sqim2.set('quux', 789)
})
sqim1.batch([sqim3], function () { // (5)
sqim1.set('foo', 123)
sqim3.set('bar', 456)
})
// When batch()'s func returns, first event to be fired happens to be
// sqim3 change_bar, handled by (3). skip() receives options.batchID
// of N, storing it in lastBatch. Then, change_bar is handled by (4),
// which starts another batch (N + 1) that is closed immediately after
// set() in (4). This triggers (2), where skip() stores N + 1 in
// lastBatch. But now, the pending sqim1 change_foo from (5) triggers
// (1) where skip() sees options.batchID of N and considers this batch
// to be "new", even though it was already processed in response to (3).
Another incorrect solution would be to skip all batches with the same or
lower ID than previously seen. After all, batchID
is guaranteed to be
unique and growing. However, the ID is generated when a batch starts
(before calling func
), not when the first batched event is dispatched:
var lastBatch = 0
function skip(options) {
if (lastBatch < options.batchID) {
lastBatch = options.batchID
} else {
return true
}
}
sqim1.batch(null, function () {
sqim1.set('foo', 123)
sqim3.set('bar', 456) // not part of sqim1's batch!
})
// batch() has allocated the batchID of N before calling func.
// Then, before batch()'s func returns, sqim3 change_bar is fired
// since this time sqim3 is not part of batch()'s sqims list (the
// batchID for change_bar is N + 1). This triggers (3), where
// skip() stores N + 1 in lastBatch. Then sqim3.set() returns, then
// sqim1.batch()'s func returns and fires the delayed sqim1 change_foo,
// to which (1) responds. skip() compares lastBatch of N + 1 with
// options.batchID of N and decides this batch is not "new".
To summarize, a handler hooked onto multiple objects must store the
actual list of batchID
-s it has seen. Most straightforward approach is
to have a round-robin array holding up to N last IDs but choosing N is
not trivial – make it too large and you will be wasting memory; make it
too small and you will be identifying old batches as new.
Instead of hardcoding any such value, we may rely on the fact that a
given batchID
can never reappear after the batch ends and that
options.batched
may not change after the batch has started. We can
keep only as many IDs in the list as there are currently active batches.
On the first event, push batchID
to the list and push an internal event
to each of the batched sqimitive’s current batch. When the event fires N
times, we pop that batchID
(N = count of batched sqimitives, except
ones with empty – already drained – batches).
var seenBatches = new Set
function skip(options) {
if (seenBatches.size == seenBatches.add(options.batchID).size) {
// Already saw this batchID.
return true
} else {
// Event is firing in the batch for the first time, across all
// batched sqimitives. Wait for all sqimitives to drain this batch.
var event = 'mySkip' + Core.unique('mySkip')
var remaining = 0
_.each(options.batched, function (item) {
if (item[1].length) {
remaining++
item[1].push([event])
item[0].once(event, function () {
--remaining || seenBatches.delete(options.batchID)
})
}
})
// Instead of hooking every batched sqimitive, we could hook only
// batched[0] since it will fire its events last. However, the order
// in which batched sqimitives are processed is an internal detail.
}
}
Use batchGuard() that implements this algorithm in a generalized form.
Occasionally you need to do some clean-up when all batched events finish
but before the next batch starts firing. One way would be to override
batch()
itself, tap into func
and push some internal event into
_batch
as the client-provided func
returns. This is the only option
when you need to react to every batch, but usually the clean-up is
necessary only if certain events occurred during a batch, similarly to
try
/finally
(except an exception breaks the batch). In this case you
can push the internal event to options.batch
or options.batched
from within a listener as done in the example in skb.
Generally speaking, it is permitted to modify options.batch
in other
ways as well, except touching the member corresponding to the event being
fired. However, this is not recommended as it may quickly become
unmanageable. In particular, since the order in which listeners are
called is hard to predict, there is no guarantee that the batch update
function alike to _recalc
above will be affected by these modifications
(such as an added or removed change_width
) because it might have
already executed. Or, with the skip()
function example in skb,
appending a new change event that would trigger update()
will cause
skip()
to wrongly report a batch with such event as “new” because
skip()
’s internal on-batch-end listener has already deleted batchID
from seenBatches
.
This problem does not exist for internal events like restoreCursor
and
mySkip
that are not used outside of their private scope.
Defined in: main.js, lines 4169-4257 (89 lines) • Show code
batch: function (sqims, func, cx) {
var id = this._batchID || (this._batchID = Core.unique('b'))
if (sqims) {
if (this._batch) {
// Disallowing as active batch's ID cannot change.
throw new Error('Batch already active')
}
var i = +(sqims[0] === this)
if (sqims.length > i) {
var sqim = sqims[i]
// Start a batch across multiple sqimitives.
if (!sqim._batch) {
// If !!_batch, don't overwrite (call batch() and let it throw).
sqim._batchID = id
sqim._batched = this._batched
}
func = sqim.batch.bind(sqim, sqims.slice(i + 1), func, cx || this)
}
}
// Batch already active, append.
if (this._batch) {
return func.call(cx || this, id)
}
var batch = this._batch = []
var outermost = !this._batches
outermost ? this._batches = [batch] : this._batches.push(batch)
this._batched.push([this, batch])
try {
try {
var res = func.call(cx || this, id)
} finally {
// Multi-sqim batch must not only start simultaneously but also finish
// (i.e. clear _batch, etc.) on all sqims at once. If each sqim would
// finish after its own func has returned, this would fail:
//
// sqim2.on('^change_foo', function () {
// sqim1.batch([sqim2], function () { // (1)
// sqim2.set('foo', 456)
// })
// })
//
// sqim2.set('foo', 123) // (2)
//
// (2) triggers change_foo which calls (1), whose func queues another
// change_foo. Before sqim1.batch() returns, sqim2.batch([]) (called
// by the former) would first clear _batch on itself, i.e. sqim2 (but
// not sqim1!), then proceed to dispatching queued events on sqim2.
// After that sqim2.batch() would return and sqim1.batch() would clear
// _batch on sqim1. In the example, sqim2.batch() dispatches
// change_foo, reentering the hook which calls (1) and fails with
// "batch active on sqim1" because sqim2._batch would be null but
// sqim1._batch would be not.
_.forEach(this._batched, function (item) {
item[0]._batch = item[0]._batchID = null
item[0]._batched = []
})
}
batch.eventIndex = -1
for (var item; item = batch[++batch.eventIndex]; ) {
this.fire(item[0], item.slice(1), false)
}
batch.eventIndex = null
// Outermost batch has finished but hooks on events that it has produced
// could have added new batches. Process them until the queue is
// exhaused.
if (outermost) {
while (batch = this._batches.shift()) {
while (batch.length) {
this.fire(batch[0][0], batch[0].slice(1), true)
batch.shift()
}
}
}
return res
} finally {
outermost && (this._batches = null)
}
},
Modifiers: constructor
Assigns new unique _cid (a string: p
+ unique positive number) and
clones all instance properties of self that are not listed in
_shareProps. Cloning puts a stop to accidental static sharing of
properties among all instances of a class where those properties are
defined.
Defined in: main.js, line 143
Part of: tag_Events
Triggers an event giving args
as parameters to all registered
listeners. Returns result of the last called listener that was eligible
for changing it.
The actual event processing is performed by static ::fire().
It’s safe to add/remove new listeners while fire() is executing – they will be in effect starting with the next fire() call (even if it’s nested).
fireOverrideWarning: to override fire, fuse and on, don’t use the usual
on('fire')
as it will lead to recursion (even the =fire
form,
evtpf). Use the old-school prototype overriding (__super__
is set up
by extend()) – see Async’s source code for an example:
fire: function (event, args) {
// Do your stuff...
return MyClass.__super__.fire.apply(this, arguments)
},
all
eventfireAllfire() first triggers the special all
event. If all
’s return
value is anything but undefined
– fire() returns it bypassing the
actual handlers of event
.
all
’s handlers get event
put in front of other args
(e.g.
['eventName', 'arg1', 2, ...]
).
Note that the all
event is only triggered for actual events so if, for
example, render() isn’t overridden then it will be called as a regular
member function without triggering all
or any other event:
var ClassA = Sqimitive.Base.extend({
events: {
render: function () { return this },
},
})
;(new ClassA)
.on({all: () => alert('booh!')})
.render()
// alert() happens, render is a firer
var ClassB = Sqimitive.Base.extend({
render: function () { return this },
})
;(new ClassB)
.on({all: () => alert('booh!')})
.render()
// no alert(), render of extend() is not an event handler but a
// method
;(new ClassB)
.on({all: () => alert('booh!')})
.on({render: function () { return this }})
.render()
// alert() happens because of on('render') which converted
// the render function from ClassB's declaration to a
// firer('render') which when called triggers fire('render') which
// in turn triggers 'all'
Defined in: main.js, lines 2318-2327 (10 lines) • Show code
fire: function (event, args, inBatch) {
if (this._events.all && event != 'all') {
var allArgs = arguments.length < 2 ? [] : Core.toArray(args).concat()
allArgs.unshift(event)
var res = this.constructor.fire.call(this, this._events.all, allArgs)
if (res !== undefined) { return res }
}
return this.constructor.fire.call(this, this._events[event], args, inBatch)
},
Part of: tag_Events
Registers a single permanent event handler.
Unlike with on() and once(), this handler cannot be removed with off().
Name | Types | Notes |
---|---|---|
event | string | A single event reference (no comma notation). |
func | function | Masked method name (expandFunc()). |
string | ||
cx | object | The context for func |
null/omitted use this |
Types | Notes |
---|---|
object | An internal event registration object (eobj ) that should be
discarded. |
fuse() is a low-level method. You are advised to call on({event}) instead.
post
callbackpostOne case when eobj
can be accessed is to assign its post
key (this is
considered advanced usage). If set to a function, ::fire() calls
post(eobj, res, args)
after executing the handler (func
), within
the handler’s cx
. post
must return the new result (replaces res
in
any case – even if undefined
and regardless of evtpf). args
is the
event’s arguments, an array-like object (possibly Arguments
). If post
sets eobj.stop
to true
then remaining handlers are skipped. post
is only called along with its associated handler and not called, for
example, if it was attached to a =wrapped
hook that was off’ed, or to
a hook with mismatching arguments (ev__
, see argDanger).
from fireOverride
Warning: to override fire, fuse and on, don’t use the usual
on('fire')
as it will lead to recursion (even the =fire
form,
evtpf). Use the old-school prototype overriding (__super__
is set up
by extend()) – see Async’s source code for an example:
fire: function (event, args) {
// Do your stuff...
return MyClass.__super__.fire.apply(this, arguments)
},
Defined in: main.js, lines 3206-3253 (48 lines) • Show code
fuse: function (event, func, cx) {
func = Core.expandFunc(func)
var eobj = Core.parseEvent(event)
// Don't just do _events[name] or (name in _events) because if name is
// toString or other Object.prototype member, this will fail (array won't
// be created).
var list = _.has(this._events, eobj.event)
? this._events[eobj.event] : (this._events[eobj.event] = [])
this._wrapHandler(eobj.event)
eobj.func = func
eobj.cx = cx
if (eobj.prefix == '+') {
eobj.res = eobj.ret = true
} else if (eobj.prefix == '=') {
eobj.ret = true
eobj.supList = list.splice(0)
// function (this[, arguments])
// sup() itself is removed if present as arguments[0].
var sup = eobj.sup = function (self, args) {
if (args && args[0] === sup && _.isArguments(args)) {
args = ap.slice.call(args, 1)
}
return Core.fire.call(self, eobj.supList, args)
}
Object.defineProperty(sup, 'name', {value: eobj.event})
}
this._insertHandler(list, eobj)
// A benchmark with optimized event handlers was done on Chrome: it was
// discovered that in a certain application 60% of fire() calls were on
// events with 1 handler so I made it bypass fire() if there were no
// handlers (replaced the wrapped method, firer() with stub) or 1 handler
// (replaced with direct call to it); this was also maintained on event
// handler changes (on()/off()).
//
// However, performance savings from this optimization were ~5% (on 30k
// fire() calls) while the added complexity was significant (due to
// different eobj properties like cx and post which implementation needed
// to be duplicated in both fire() and the fire()-less wrapper) so it was
// discarded.
return eobj
},
Part of: tag_Events
A debug method for logging all triggered events to the console.
logEvents() allows enabling event logging as well as acts as an event
handler for all
. The handler can be used on its own, without enabling
logging with logEvents(true)
.
Name | Types | Notes |
---|---|---|
true | Enable logging; do nothing if browser doesn’t provide
console.log() | |
- | no arguments | Same as true (enable logging). |
false | Disable logging, if enabled. | |
string | event name | Log this event; other arguments are the event’s arguments. |
Types | Notes |
---|---|
this if enable is bool | |
undefined if string |
Note: logEvents logs only events passing through the instance .fire(). This means that only real event calls are tracked, not calls of non-overridden methods (i.e. of regular functions, not firer). See fireAll.
You can override or extend the default logging behaviour by testing the argument for string:
var MyLoggee = Sqimitive.Base.extend({
events: {
logEvents: function (event) {
// logEvents() calls itself when an event occurs and the first
// argument is the event name - a string. In other cases it's
// not the logger being called.
if (typeof event == 'string') {
console.log('el.' + this.el[0].className)
}
},
},
})
// Logs both standard logEvents() info and our class name line.
;(new MyLogger).logEvents().fire('something')
Note: returning non-undefined
from the all
handler prevents calling
real event handlers (see .fire()). In the above example it never
happens since logEvents
event is hooked with no prefix (evtpf)
meaning the handler’s return value is ignored (even if it does return
anything). However, not in this example:
events: {
'=logEvents': function (sup, event) {
if (typeof event == 'string') {
console.log(this.el[0].className)
} else {
sup(this, arguments)
}
return 'break!'
// returning non-undefined from an 'all' handler bypasses real
// event handlers
},
},
Defined in: main.js, lines 2401-2427 (27 lines) • Show code
logEvents: function (enable) {
if (typeof enable == 'string') {
// function (event, eventArg1, arg2, ...)
var info = this._cid
if (this.el) {
//! +ig
var el = this.el.nodeType ? this.el : this.el[0] // jQuery
info += '\t\t' + el.tagName
var className = el.className.trim()
if (className != '') {
info += (' ' + className).replace(/\s+/g, '.')
}
}
console.log(enable + ' (' + (arguments.length - 1) + ') on ' + info)
return undefined
} else if (!enable && arguments.length) {
this.off(this._logEventID)
this._logEventID = null
} else if (console && console.log && !this._logEventID) {
this._logEventID = this.on('all', Core.logEvents)
}
return this
},
Part of: tag_Extension
Extends this object instance with a behaviour of another “class” (a “mix-in”).
The static ::mixIn() exists which affects all objects of a class.
Name | Types | Notes |
---|---|---|
this | mixInDesc | The object receiving new “mixed-in” fields (for static
::mixIn() this is the “child” class). |
newClass | object | The mix-in, the object “mixed into” this . |
options | An arbitrary value (usually an object) given to
newClass .finishMixIn() , allowing creation of parametrized mix-ins
(basically, generics). |
Possible newClass
keys:
Name | Types | Notes |
---|---|---|
staticProps | object | Static fields made available as
newClass.something |
events | object | Event listeners (see events). Because this is not a real field, keys in |
finishMixIn | function | Called before returning.
|
mixIns | array | Each item is either a mixed-in class or an array:
[class, options] |
* | Other keys represent instance fields (the protoProps argument of
extend())._mergeProps is respected, but of A string form of _childClass is only allowed in extend(), not here; other forms (array, object) are allowed in both places. |
Warning: this
is modified in-place; no new class is created
(mixIn()
returns no value). If you want a base class without a given
mix-in and a subclass with that mix-in – first extend() the base class
and then mix into the new sub-class:
// CORRECT:
var Base = Sqimitive.Base.extend()
var Sub = Base.extend({mixIns: [SomeMixIn]})
// WRONG: will modify Base, not return a new subclass:
var Base = Sqimitive.Base.extend()
var Sub = Base.mixIn(SomeMixIn)
Other notes:
mixIn()
if newClass
contains
the mixIns
key; this is recursive), overwrites fields of this
with
deepClone-s of those in newClass
(or merges according to
_mergeProps), adds staticProps
, hardwires events into class
definition (fuse()) and calls finishMixIn()
.mixin()
is of a similar purpose.elEvents
elEventsMixInBase lists Base.elEvents in _mergeProps so the former are
merged but unlike with newClass.events
keys (which can never have a
conflict and be dropped), keys in elEvents
get overridden on name
collisions. This is sometimes desirable (to override the parent’s
handler), sometimes not (then use a unique .ns
suffix):
var Base = Sqimitive.Base.extend({
elEvents: {
click: function () { alert("Click-click-click!') },
},
})
var ChildA = Base.extend({
elEvents: {
click: function () { alert("Overridden!') },
},
})
var ChildB = Base.extend({
elEvents: {
'click.new-in-child-B': function () { alert("Combined!') },
},
})
// in ChildA, 'click' has 1 listener (Base's dropped)
// in ChildB, 'click' has 2 listeners (Base's and ChildB's)
mixIns
is applied before setting other properties which allows
extending mix-ins themselves (later mix-ins override preceding just like
with normal inheritance on classes). For example:
var ParentMixIn = {
someEvent: function () { /* parent */ },
]
var ChildMixIn = {
mixIns: [ParentMixIn],
events: {
someEvent: function () { /* child */ },
},
}
var myClass = Sqimitive.Base.extend({
mixIns: [ChildMixIn],
})
// myClass had ParentMixIn added, then ChildMixIn, and now has two
// listeners on someEvent
ParentMixIn
could also have the mixIns
property to specify its own
parent.
In the above example, the mix-in specified its parent, which is usually
intuitive. Still, it could be specified in the final class’ mixIns
alone:
var ParentMixIn = {
someEvent: function () { /* parent */ },
]
var ChildMixIn = {
// mixIns property is missing.
events: {
someEvent: function () { /* child */ },
},
}
var myClass = Sqimitive.Base.extend({
// Added ParentMixIn in front of ChildMixIn.
mixIns: [ParentMixIn, ChildMixIn],
})
// or (equivalent, no mixIns property, mixIn() calls instead):
var myClass = Sqimitive.Base.extend()
myClass.mixIn(ParentMixIn)
myClass.mixIn(ChildMixIn)
// or (equivalent for a particular object instance):
var myClass = Sqimitive.Base.extend()
var obj = new myClass
obj.mixIn(ParentMixIn)
obj.mixIn(ChildMixIn)
// in any case, MyClass (or obj) has 'someEvent' firer() with
// 2 listeners: of ParentMixIn and of ChildMixIn
Warning: calling mixIn()
in finishMixIn()
would have a
different effect. If ChildMixIn
were defined as follows then MyClass
or obj
would have someEvent
not as a firer() but as the
ParentMixIn
’s function (because it was mixed-in after ChildMixIn
and
overrode its events
handler):
var ChildMixIn = {
finishMixIn: function (newClass) {
newClass.mixIn(ParentMixIn)
},
events: {
someEvent: function () { /* child */ },
},
}
This will override the handler introduced in the declaration of Class
for the same reason – MixIn
is added to Class
after Class
’ own
fields:
var MixIn = {
some: function () { ... },
}
var Class = Sqimitive.Base.extend({
events: {
some: function () { ... },
}
})
Class.mixIn(MixIn)
This is the correct way (using mixIns
property when extend()’ing
Class
):
var MixIn = {
some: function () { ... },
}
var Class = Sqimitive.Base.extend({
mixIns: [MixIn],
events: {
some: function () { ... },
}
})
See the source code for details.
B
and subclass C
, adding mix-ins to B
after C
has been extend()’ed from B
when C
declares events will lead to
C
not having events of the newly mixed-in objects of B
.B
are fuse()’d and their eobj
-s are
shared among all subclasses of B
and should not be changed (list of
events may change, just not properties of inherited handlers): var ClassB = Sqimitive.Core.extend({
events: {change: 'render'},
})
ClassB.prototype._events.change[0].post //=> undefined
var ClassC = ClassB.extend()
ClassC.prototype._events.change[0].post = function () { ... }
ClassB.prototype._events.change[0].post //=> Function
// In this version instances' _events are deepClone()'d so changing it
// (not the prototype) doesn't affect other classes/objects:
var obj = new ClassC
obj._events.change[0].post = 'foo'
ClassB.prototype._events.change[0].post //=> still Function
Defined in: main.js, lines 2129-2237 (109 lines) • Show code
mixIn: function (newClass, options) {
//! +ig=4
// Don't expose internal inheritance fields on the final classes.
var merged = {mixIns: undefined, finishMixIn: undefined, staticProps: undefined, events: undefined}
_.forEach(newClass.mixIns || [], function (mixIn) {
// mixIns items are either objects or arrays. concat() ensures mixIn()
// is called either with (class, options) or (class) alone.
this.mixIn.apply(this, [].concat(mixIn))
}, this)
_.forEach(this.constructor._mergeProps, function (prop) {
if ((prop in this) && (prop in newClass)) {
if (_.isArray(newClass[prop])) {
merged[prop] = this[prop].concat(newClass[prop])
} else {
merged[prop] = _.assign({}, this[prop], newClass[prop])
}
}
}, this)
_.assign(this, newClass, merged)
_.assign(this.constructor, newClass.staticProps || {})
if (newClass.events) {
// Core has no __super__ but it doesn't use mix-ins either so no check
// for null. This condition will evaluate to true except when a class
// has mix-ins (via mixIns property or mixIn() method) - we could clone
// it in the second case too but this would be a waste.
//
// Warning: this operates under assumption that the base class is
// finalized (all mix-ins applied) before any of its sub-classes is
// created.
//
// var Base = Sqimitive.extend()
// Base.mixIn(...) // fine
// var Child = Base.extend()
// Base.mixIn(...) // wrong
//
// Adding mix-ins after Child was declared may have unexpected side
// effects - if the mix-in adds an event and if Child had its own events
// block, then Child won't receive new mix-in's events. This is an
// implementation detail - "officially", adding mix-ins after declaring
// a subclass leads to undefined behaviour and should never be used (but
// it's fine to mix-in into live instances at any time).
if (this._events === this.constructor.__super__._events) {
//! +ig
// Could use deepClone but it's more intense - we don't clone eobj-s
// which theoretically could be changed before instantiation but we
// ignore this possibility.
this._events = _.assign({}, this._events)
for (var ev in this._events) {
this._events[ev] = this._events[ev].concat()
}
}
this.on(newClass.events)
}
if (this === this.constructor.prototype) { // static mixIn()
//! +ig
// Constructor fills this one when the class is created for the first
// time. It should be regenerated for every class and not be copied to
// subclasses to avoid the following:
//
// var Base = Sqimitive.Base.extend()
// new Base
// Base._copyProps //=> [...]
// var Sub = Base.extend({newProp: []})
// new Sub
// Sub._copyProps //=> same as Base; newProp is missing
// ;(new Sub).newProp.push(123)
// ;(new Sub).newProp //=> [123] instead of []
//
// Additionally, it should be cleared when a mix-in is added since it
// may have provided new properties (in finishMixIn if not via
// newClass). This is not done for instance mixIn() since the latter is
// called after constructor.
this.constructor._copyProps = initCopyProps
} else { // instance mixIn()
// When mixing-in to a declaration, inherited and mixed-in properties'
// values are cloned by the constructor. Obviously, the latter isn't
// called when mixing-in to an instance so need to clone now.
//
// // Instance mix-in:
// var MixIn = {newProp: []}
// var sqim = new (Sqimitive.Base.extend())
// sqim.mixIn(MixIn)
// sqim.newProp.push(123)
// MixIn.newProp //=> must be [], not [123]
//
// // Compare with declaration mix-in (cloning done by constructor):
// var sqim = new (Sqimitive.Base.extend({
// mixIns: [MixIn]
// }))
// // Or, the same:
// var Decl = Sqimitive.Base.extend()
// Decl.mixIn(MixIn)
// var sqim = new Decl
for (var prop in newClass) {
if (!(prop in merged) && this._mustClone(prop)) {
this[prop] = this.constructor.deepClone(this[prop])
}
}
}
newClass.finishMixIn && newClass.finishMixIn(this, options)
},
Part of: tag_Events
Removes non-fuse()’d event listener(s).
Types | Notes |
---|---|
this |
key
can be one of:
Name | Types | Notes |
---|---|---|
string | event name | Like “render”; removes all listeners to that event. |
number | listener ID | As returned by on(); removes that particular listener from that particular event. |
object | context | The cx to which listeners were registered; removes
all listeners to all events with that context. |
array | Containing any of the above values including more sub-arrays; identical to multiple off() calls. |
Does nothing if no matching events, contexts or listeners were found (thus safe to call multiple times).
When unregistering a wrapping handler (=event
, see evtpf) its
underlying handlers are reinserted into the list of handlers (“undoing”
method override) maintaining priority (evtref) but not necessary the
original order (calls to on()).
See evt for a nice example on this subject and once() for attaching one-shot listeners.
from unnestedoff
Base’s implementation of unnested() calls sqim
.off(this
) to
unregister all event handlers that might have been previously attached to
the removed child by this instance (provided they were not hardwired with
fuse() and used cx === this
).
Defined in: main.js, lines 3387-3412 (26 lines) • Show code
off: function (key) {
if (_.isArray(key)) {
// List of identifiers of some kind.
_.forEach(key, this.off, this)
} else if (key instanceof Object) {
// By context.
var list = this._eventsByCx.get(key)
if (list) {
this._eventsByCx.delete(key)
_.forEach(list, this._unregHandler.bind(this, {cx: true}))
}
} else if (typeof key == 'string') {
// By event name.
var list = this._events[key]
if (list) {
delete this._events[key]
_.forEach(list, this._unregHandler.bind(this, {sup: true}))
}
} else {
// By handler ID.
var eobj = this._eventsByID[key]
eobj && this._unregHandler({}, eobj)
}
return this
},
Part of: tag_Events
Registers a single event handler and returns its ID or permanently hardwires multiple events and/or handlers.
Name | Types | Notes |
---|---|---|
event | string to register one handler and return ID that can be used to unregister it with off() | Object keys are event references (evtref), values
are handlers. If an object, keys can contain multiple event references separated with
|
object to fuse() one or multiple handlers
and return this | ||
cx | onOnceobject | Context in which the handler(s)
are called. this is the object on which on() was called so
on('e', 'h') will call h() on the object where the event occurs
(and in that object’s context). |
null/omitted use this |
The handler (func
or event
object value) can be either a (masked)
method reference (see expandFunc()) or function. Strings are resolved
when the event is fired so their handlers are always up-to-date and don’t
even have to exist by the time on() is called.
Errors if an event reference can’t be parsed.
from fireOverride
Warning: to override fire, fuse and on, don’t use the usual
on('fire')
as it will lead to recursion (even the =fire
form,
evtpf). Use the old-school prototype overriding (__super__
is set up
by extend()) – see Async’s source code for an example:
fire: function (event, args) {
// Do your stuff...
return MyClass.__super__.fire.apply(this, arguments)
},
Other notes:
event
is like fuse() but with added comma
notation in event
keys.on({event: func}, cx)
or fuse() (this also
clearly conveys your intention of keeping it forever).cx
is not null
), off() – also two or three.An event reference is a string with four parts:
[priority^][prefix]event[args]
.
Name | Types | Notes |
---|---|---|
priority | Optional; an integer ending on ^ determining the order of
calling hooks of the same event (defaults to 0 , may be negative),
lower called first. If begins with ^ or is ^ alone, marks the
handler out-of-batch: when due for calling in response to batch()’ed
events, it is called immediately after func returns – without draining
accumulated batches (see naor). | |
prefix | Optional; changes the way event handler is bound and called as explained in evtpf. | |
event | Event name – exactly what is given to fire() when triggering an event. | |
args | Zero or more underscores (_ ); if present, the handler gets
called only if event was given that exact number of arguments (it’s
not possible to match zero arguments).For example, In case of Generally, usage of |
Name | Types | Notes |
---|---|---|
none | evArgs… | Add new handler after existing handlers. It neither receives the current event result nor it can change it (the handler’s return value is ignored). This form is used most often and it’s perfect for “attaching” extra behaviour to the end of the original code, retaining the original result. |
- | evArgs… | Add new handler before existing handlers of the same priority, otherwise identical to “no prefix”. |
+ | res, evArgs… | Add new handler after existing handlers. It
receives the current event return value (res ) and can change it if
the handler returns anything but undefined . |
= | sup, evArgs… | Wrap around existing handlers – they are
removed, regardless of their priority (own priority of = sorts it in
relation to non-= handlers added later). It receives a callable sup
of form function (this, args) which it may call or not (alike to
calling super in Java). sup.name equals plain event name, handy
with comma notation. If it returns undefined the current event return
value is unchanged.First argument of Note: old handlers are not technically “removed” – they are kept around
and restored if the
This works even if more handlers were added to the event after wrapping:
Warning: don’t pass a wrong
Wrapped handlers are sorted by their priorities, as usual. However,
their out-of-batch marks (leading
|
It’s impossible for a handler to set the event’s result to undefined
as
it is considered a “keep current result” marker. This can be worked
around using eobj
.post whose result is taken literally:
sqim.fuse('event', new Function)
.post = function (eobj, res, args) { return undefined }
In JavaScript, functions accept extra arguments with ease; often you
would provide an iterator and only care for some of its arguments.
However, if using the ev__
form (underscores are the args
part of
evtref) then the event must have been given that exact number of
arguments even if none of its handlers uses the rest:
var MySqimitive = Sqimitive.Base.extend({
accessor: function (prop, value) {
if (arguments.length == 1) {
return this._foo[prop]
} else {
this._foo[prop] = value
return value
}
},
})
var sqim = new MySqimitive
// This handler should always be called when a property is being
// set... right?
sqim.on('accessor__', function (prop, value) {
alert('Now ' + prop + ' is ' + value)
})
// Alerts "Now name is val". So far so good.
sqim.accessor('name', 'val')
var propsToSet = {prop: 'foo', bar: 123}
// map() calls sqim.accessor() for every key in propsToSet and
// properties are indeed properly set - but no alerts appear! This is
// because map() passes 3 arguments to the iterator: value, key and
// the list itself (object in our case). Therefore even if accessor()
// uses just 2 of these arguments (like it does above) the actual
// event fired is "accessor___" (3 underscores), not "accessor__" (the
// one we've hooked).
_.map(_.invert(propsToSet), sqim.accessor, sqim)
Defined in: main.js, lines 2701-2715 (15 lines) • Show code
on: function (event, func, cx) {
if (event instanceof Object) {
for (var name in event) {
var names = name.split(', ')
for (var i = 0, l = names.length; i < l; i++) {
this.fuse(names[i], event[name], func)
}
}
return this
} else if (arguments.length >= 2) {
return this._regHandler( this.fuse(event, func, cx) )
} else {
throw new TypeError('on: Bad arguments')
}
},
Part of: tag_Events
Regsiters a single one-shot event handler that removes itself as soon as
func
returns non-retainer
(or after the first call).
In all other aspects once() is identical to on() with the string
argument, i.e. on(event, func, cx)
. Returns new event ID suitable for
off() so you can unregister it before it’s called (or during or after -
nothing will happen). Doesn’t allow registering multiple events/handlers
in one call.
Name | Types | Notes |
---|---|---|
event | string | |
retainer | mixed except string, function, NaN (not comparable) | Compared using === , acts as event result if returned; typical
choices are undefined , null , true and false |
omitted | ||
func | string | |
function |
from onOnce
Name | Types | Notes |
---|---|---|
cx | object | Context in which the handler(s)
are called. this is the object on which on() was called so
on('e', 'h') will call h() on the object where the event occurs
(and in that object’s context). |
null/omitted use this |
The handler (func
or event
object value) can be either a (masked)
method reference (see expandFunc()) or function. Strings are resolved
when the event is fired so their handlers are always up-to-date and don’t
even have to exist by the time on() is called.
Errors if an event reference can’t be parsed.
If the handler somehow gets called again after it should not have been,
or if event
occurs again while running (reenters), or if func
throws
- the handler returns undefined
without calling func
and unbinds.
This is in line with no:once().
Defined in: main.js, lines 2754-2783 (30 lines) • Show code
once: function (event, retainer, func, cx) {
var args = arguments
var self = this
var running = 0
switch (typeof retainer) {
case 'string': // expandFunc()
case 'function':
ap.splice.call(args, 1, 0, retainer = {})
}
function once_() {
var incomplete = {}
var res = incomplete
try {
if (!running++) {
res = Core.expandFunc(args[2]).apply(this, arguments)
}
} finally {
res === retainer ? running-- : self.off(id)
}
return res === incomplete ? undefined : res
}
Core.trace && (once_.trace = arguments)
var id = this.on(event, once_, args[3])
return id
},