Add type-checking methods, make a few things stop de-opting under Chrome

This commit is contained in:
impinball 2015-07-09 15:22:51 -04:00
parent b28045aa60
commit 83a30a4d3c
3 changed files with 60 additions and 44 deletions

View file

@ -1,9 +1,20 @@
var m = (function app(window, undefined) { var m = (function app(window, undefined) {
var OBJECT = "[object Object]", ARRAY = "[object Array]", STRING = "[object String]", FUNCTION = "function"; function isFunction(object) {
return typeof object === "function";
}
function isObject(object) {
return type.call(object) === "[object Object]";
}
function isString(object) {
return type.call(object) === "[object String]";
}
var isArray = Array.isArray || function (object) {
return type.call(object) === "[object Array]";
};
var type = {}.toString; var type = {}.toString;
var parser = /(?:(^|#|\.)([^#\.\[\]]+))|(\[.+?\])/g, attrParser = /\[(.+?)(?:=("|'|)(.*?)\2)?\]/; var parser = /(?:(^|#|\.)([^#\.\[\]]+))|(\[.+?\])/g, attrParser = /\[(.+?)(?:=("|'|)(.*?)\2)?\]/;
var voidElements = /^(AREA|BASE|BR|COL|COMMAND|EMBED|HR|IMG|INPUT|KEYGEN|LINK|META|PARAM|SOURCE|TRACK|WBR)$/; var voidElements = /^(AREA|BASE|BR|COL|COMMAND|EMBED|HR|IMG|INPUT|KEYGEN|LINK|META|PARAM|SOURCE|TRACK|WBR)$/;
var noop = function() {}; var noop = function () {};
// caching commonly used variables // caching commonly used variables
var $document, $location, $requestAnimationFrame, $cancelAnimationFrame; var $document, $location, $requestAnimationFrame, $cancelAnimationFrame;
@ -32,15 +43,17 @@ var m = (function app(window, undefined) {
* @param {...mNode=[]} Zero or more Mithril child nodes. Can be an array, or splat (optional) * @param {...mNode=[]} Zero or more Mithril child nodes. Can be an array, or splat (optional)
* *
*/ */
function m(tag, attrs) { function m(tag, pairs) {
var args = [].slice.call(arguments, 1); for (var args = [], i = 1; i < arguments.length; i++) {
if (type.call(tag) === OBJECT) return parameterize(tag, args); args[i - 1] = arguments[i];
var hasAttrs = attrs != null && type.call(attrs) === OBJECT && !("tag" in attrs || "view" in attrs) && !("subtree" in attrs); }
var attrs = hasAttrs ? attrs : {}; if (isObject(tag)) return parameterize(tag, args);
var hasAttrs = pairs != null && isObject(pairs) && !("tag" in pairs || "view" in pairs || "subtree" in pairs);
var attrs = hasAttrs ? pairs : {};
var classAttrName = "class" in attrs ? "class" : "className"; var classAttrName = "class" in attrs ? "class" : "className";
var cell = {tag: "div", attrs: {}}; var cell = {tag: "div", attrs: {}};
var match, classes = []; var match, classes = [];
if (type.call(tag) != STRING) throw new Error("selector in m(selector, attrs, children) should be a string"); if (!isString(tag)) throw new Error("selector in m(selector, attrs, children) should be a string");
while (match = parser.exec(tag)) { while (match = parser.exec(tag)) {
if (match[1] === "" && match[2]) cell.tag = match[2]; if (match[1] === "" && match[2]) cell.tag = match[2];
else if (match[1] === "#") cell.attrs.id = match[2]; else if (match[1] === "#") cell.attrs.id = match[2];
@ -52,7 +65,7 @@ var m = (function app(window, undefined) {
} }
var children = hasAttrs ? args.slice(1) : args; var children = hasAttrs ? args.slice(1) : args;
if (children.length === 1 && type.call(children[0]) === ARRAY) { if (children.length === 1 && isArray(children[0])) {
cell.children = children[0]; cell.children = children[0];
} }
else { else {
@ -131,7 +144,7 @@ var m = (function app(window, undefined) {
if (cached != null) { if (cached != null) {
if (parentCache && parentCache.nodes) { if (parentCache && parentCache.nodes) {
var offset = index - parentIndex; var offset = index - parentIndex;
var end = offset + (dataType === ARRAY ? data : cached.nodes).length; var end = offset + (isArray(data) ? data : cached.nodes).length;
clear(parentCache.nodes.slice(offset, end), parentCache.slice(offset, end)); clear(parentCache.nodes.slice(offset, end), parentCache.slice(offset, end));
} }
else if (cached.nodes) clear(cached.nodes, cached); else if (cached.nodes) clear(cached.nodes, cached);
@ -141,10 +154,10 @@ var m = (function app(window, undefined) {
cached.nodes = []; cached.nodes = [];
} }
if (dataType === ARRAY) { if (isArray(data)) {
//recursively flatten array //recursively flatten array
for (var i = 0; i < data.length; i++) { for (var i = 0; i < data.length; i++) {
if (type.call(data[i]) === ARRAY) { if (isArray(data[i])) {
data = data.concat.apply([], data); data = data.concat.apply([], data);
i--; //check current index again and flatten until there are no more nested arrays at that index i--; //check current index again and flatten until there are no more nested arrays at that index
} }
@ -241,7 +254,7 @@ var m = (function app(window, undefined) {
//the second clause (after the pipe) matches text nodes //the second clause (after the pipe) matches text nodes
subArrayCount += (item.match(/<[^\/]|\>\s*[^<]/g) || [0]).length; subArrayCount += (item.match(/<[^\/]|\>\s*[^<]/g) || [0]).length;
} }
else subArrayCount += type.call(item) === ARRAY ? item.length : 1; else subArrayCount += isArray(item) ? item.length : 1;
cached[cacheCount++] = item; cached[cacheCount++] = item;
} }
}); });
@ -262,7 +275,7 @@ var m = (function app(window, undefined) {
cached.nodes = nodes; cached.nodes = nodes;
} }
} }
else if (data != null && dataType === OBJECT) { else if (data != null && isObject(data)) {
var views = [], controllers = []; var views = [], controllers = [];
while (data.view) { while (data.view) {
var view = data.view.$original || data.view; var view = data.view.$original || data.view;
@ -288,14 +301,14 @@ var m = (function app(window, undefined) {
//if an element is different enough from the one in cache, recreate it //if an element is different enough from the one in cache, recreate it
if (data.tag != cached.tag || dataAttrKeys.sort().join() != Object.keys(cached.attrs).sort().join() || data.attrs.id != cached.attrs.id || data.attrs.key != cached.attrs.key || (m.redraw.strategy() == "all" && (!cached.configContext || cached.configContext.retain !== true)) || (m.redraw.strategy() == "diff" && cached.configContext && cached.configContext.retain === false)) { if (data.tag != cached.tag || dataAttrKeys.sort().join() != Object.keys(cached.attrs).sort().join() || data.attrs.id != cached.attrs.id || data.attrs.key != cached.attrs.key || (m.redraw.strategy() == "all" && (!cached.configContext || cached.configContext.retain !== true)) || (m.redraw.strategy() == "diff" && cached.configContext && cached.configContext.retain === false)) {
if (cached.nodes.length) clear(cached.nodes); if (cached.nodes.length) clear(cached.nodes);
if (cached.configContext && typeof cached.configContext.onunload === FUNCTION) cached.configContext.onunload(); if (cached.configContext && isFunction(cached.configContext.onunload)) cached.configContext.onunload();
if (cached.controllers) { if (cached.controllers) {
for (var i = 0, controller; controller = cached.controllers[i]; i++) { for (var i = 0, controller; controller = cached.controllers[i]; i++) {
if (typeof controller.onunload === FUNCTION) controller.onunload({preventDefault: noop}); if (isFunction(controller.onunload)) controller.onunload({preventDefault: noop});
} }
} }
} }
if (type.call(data.tag) != STRING) return; if (!isString(data.tag)) return;
var node, isNew = cached.nodes.length === 0; var node, isNew = cached.nodes.length === 0;
if (data.attrs.xmlns) namespace = data.attrs.xmlns; if (data.attrs.xmlns) namespace = data.attrs.xmlns;
@ -344,7 +357,7 @@ var m = (function app(window, undefined) {
if (shouldReattach === true && node != null) parentElement.insertBefore(node, parentElement.childNodes[index] || null); if (shouldReattach === true && node != null) parentElement.insertBefore(node, parentElement.childNodes[index] || null);
} }
//schedule configs to be called. They are called after `build` finishes running //schedule configs to be called. They are called after `build` finishes running
if (typeof data.attrs["config"] === FUNCTION) { if (isFunction(data.attrs["config"])) {
var context = cached.configContext = cached.configContext || {}; var context = cached.configContext = cached.configContext || {};
// bind // bind
@ -356,7 +369,7 @@ var m = (function app(window, undefined) {
configs.push(callback(data, [node, !isNew, context, cached])); configs.push(callback(data, [node, !isNew, context, cached]));
} }
} }
else if (typeof data != FUNCTION) { else if (!isFunction(data)) {
//handle text nodes //handle text nodes
var nodes; var nodes;
if (cached.nodes.length === 0) { if (cached.nodes.length === 0) {
@ -410,11 +423,11 @@ var m = (function app(window, undefined) {
//`config` isn't a real attributes, so ignore it //`config` isn't a real attributes, so ignore it
if (attrName === "config" || attrName == "key") continue; if (attrName === "config" || attrName == "key") continue;
//hook event handlers to the auto-redrawing system //hook event handlers to the auto-redrawing system
else if (typeof dataAttr === FUNCTION && attrName.indexOf("on") === 0) { else if (isFunction(dataAttr) && attrName.indexOf("on") === 0) {
node[attrName] = autoredraw(dataAttr, node); node[attrName] = autoredraw(dataAttr, node);
} }
//handle `style: {...}` //handle `style: {...}`
else if (attrName === "style" && dataAttr != null && type.call(dataAttr) === OBJECT) { else if (attrName === "style" && dataAttr != null && isObject(dataAttr)) {
for (var rule in dataAttr) { for (var rule in dataAttr) {
if (cachedAttr == null || cachedAttr[rule] !== dataAttr[rule]) node.style[rule] = dataAttr[rule]; if (cachedAttr == null || cachedAttr[rule] !== dataAttr[rule]) node.style[rule] = dataAttr[rule];
} }
@ -461,17 +474,17 @@ var m = (function app(window, undefined) {
if (nodes.length != 0) nodes.length = 0; if (nodes.length != 0) nodes.length = 0;
} }
function unload(cached) { function unload(cached) {
if (cached.configContext && typeof cached.configContext.onunload === FUNCTION) { if (cached.configContext && isFunction(cached.configContext.onunload)) {
cached.configContext.onunload(); cached.configContext.onunload();
cached.configContext.onunload = null; cached.configContext.onunload = null;
} }
if (cached.controllers) { if (cached.controllers) {
for (var i = 0, controller; controller = cached.controllers[i]; i++) { for (var i = 0, controller; controller = cached.controllers[i]; i++) {
if (typeof controller.onunload === FUNCTION) controller.onunload({preventDefault: noop}); if (isFunction(controller.onunload)) controller.onunload({preventDefault: noop});
} }
} }
if (cached.children) { if (cached.children) {
if (type.call(cached.children) === ARRAY) { if (isArray(cached.children)) {
for (var i = 0, child; child = cached.children[i]; i++) unload(child); for (var i = 0, child; child = cached.children[i]; i++) unload(child);
} }
else if (cached.children.tag) unload(cached.children); else if (cached.children.tag) unload(cached.children);
@ -563,7 +576,7 @@ var m = (function app(window, undefined) {
m.prop = function (store) { m.prop = function (store) {
//note: using non-strict equality check here because we're checking if store is null OR undefined //note: using non-strict equality check here because we're checking if store is null OR undefined
if (((store != null && type.call(store) === OBJECT) || typeof store === FUNCTION) && typeof store.then === FUNCTION) { if ((store != null && isObject(store) || isFunction(store)) && isFunction(store.then)) {
return propify(store); return propify(store);
} }
@ -577,7 +590,7 @@ var m = (function app(window, undefined) {
return (component.controller || noop).apply(this, args) || this; return (component.controller || noop).apply(this, args) || this;
}; };
var view = function(ctrl) { var view = function(ctrl) {
if (arguments.length > 1) args = args.concat([].slice.call(arguments, 1)); for (var i = 1; i < arguments.length; i++) args.push(arguments[i]);
return component.view.apply(component, args ? [ctrl].concat(args) : [ctrl]); return component.view.apply(component, args ? [ctrl].concat(args) : [ctrl]);
}; };
view.$original = component.view; view.$original = component.view;
@ -586,7 +599,8 @@ var m = (function app(window, undefined) {
return output; return output;
} }
m.component = function(component) { m.component = function(component) {
return parameterize(component, [].slice.call(arguments, 1)); for (var args = [], i = 1; i < arguments.length; i++) args[i - 1] = arguments[i];
return parameterize(component, args);
}; };
m.mount = m.module = function(root, component) { m.mount = m.module = function(root, component) {
if (!root) throw new Error("Please ensure the DOM element exists before rendering a template into it."); if (!root) throw new Error("Please ensure the DOM element exists before rendering a template into it.");
@ -607,7 +621,7 @@ var m = (function app(window, undefined) {
} }
else unloaders = []; else unloaders = [];
if (controllers[index] && typeof controllers[index].onunload === FUNCTION) { if (controllers[index] && isFunction(controllers[index].onunload)) {
controllers[index].onunload(event); controllers[index].onunload(event);
} }
@ -616,7 +630,7 @@ var m = (function app(window, undefined) {
m.startComputation(); m.startComputation();
roots[index] = root; roots[index] = root;
if (arguments.length > 2) component = subcomponent(component, [].slice.call(arguments, 2)); if (arguments.length > 2) component = subcomponent(component, [].slice.call(arguments, 2));
var currentComponent = topComponent = component = component || {controller: function() {}}; var currentComponent = topComponent = component = component || {controller: noop};
var constructor = component.controller || noop; var constructor = component.controller || noop;
var controller = new constructor; var controller = new constructor;
//controllers may call m.mount recursively (via m.route redirects, for example) //controllers may call m.mount recursively (via m.route redirects, for example)
@ -637,7 +651,7 @@ var m = (function app(window, undefined) {
try { try {
//lastRedrawId is a positive number if a second redraw is requested before the next animation frame //lastRedrawId is a positive number if a second redraw is requested before the next animation frame
//lastRedrawID is null if it's the first redraw and not an event handler //lastRedrawID is null if it's the first redraw and not an event handler
if (lastRedrawId && force !== true) { if (lastRedrawId && !force) {
//when setTimeout: only reschedule redraw if time between now and previous redraw is bigger than a frame, otherwise keep currently scheduled timeout //when setTimeout: only reschedule redraw if time between now and previous redraw is bigger than a frame, otherwise keep currently scheduled timeout
//when rAF: always reschedule redraw //when rAF: always reschedule redraw
if ($requestAnimationFrame === window.requestAnimationFrame || new Date - lastRedrawCallTime > FRAME_BUDGET) { if ($requestAnimationFrame === window.requestAnimationFrame || new Date - lastRedrawCallTime > FRAME_BUDGET) {
@ -705,7 +719,7 @@ var m = (function app(window, undefined) {
//m.route() //m.route()
if (arguments.length === 0) return currentRoute; if (arguments.length === 0) return currentRoute;
//m.route(el, defaultRoute, routes) //m.route(el, defaultRoute, routes)
else if (arguments.length === 3 && type.call(arg1) === STRING) { else if (arguments.length === 3 && isString(arg1)) {
redirect = function(source) { redirect = function(source) {
var path = currentRoute = normalizeRoute(source); var path = currentRoute = normalizeRoute(source);
if (!routeByValue(root, arg2, path)) { if (!routeByValue(root, arg2, path)) {
@ -739,7 +753,7 @@ var m = (function app(window, undefined) {
} }
} }
//m.route(route, params, shouldReplaceHistoryEntry) //m.route(route, params, shouldReplaceHistoryEntry)
else if (type.call(root) === STRING) { else if (isString(root)) {
var oldRoute = currentRoute; var oldRoute = currentRoute;
currentRoute = root; currentRoute = root;
arg1 || {}; arg1 || {};
@ -832,8 +846,8 @@ var m = (function app(window, undefined) {
var value = object[prop]; var value = object[prop];
var valueType = type.call(value); var valueType = type.call(value);
var pair = (value === null) ? encodeURIComponent(key) : var pair = (value === null) ? encodeURIComponent(key) :
valueType === OBJECT ? buildQueryString(value, key) : isObject(value) ? buildQueryString(value, key) :
valueType === ARRAY ? value.reduce(function(memo, item) { isArray(value) ? value.reduce(function(memo, item) {
if (!duplicates[key]) duplicates[key] = {}; if (!duplicates[key]) duplicates[key] = {};
if (!duplicates[key][item]) { if (!duplicates[key][item]) {
duplicates[key][item] = true; duplicates[key][item] = true;
@ -855,7 +869,7 @@ var m = (function app(window, undefined) {
var key = decodeURIComponent(pair[0]); var key = decodeURIComponent(pair[0]);
var value = pair.length == 2 ? decodeURIComponent(pair[1]) : null; var value = pair.length == 2 ? decodeURIComponent(pair[1]) : null;
if (params[key] != null) { if (params[key] != null) {
if (type.call(params[key]) !== ARRAY) params[key] = [params[key]]; if (!isArray(params[key])) params[key] = [params[key]];
params[key].push(value); params[key].push(value);
} }
else params[key] = value; else params[key] = value;
@ -937,7 +951,7 @@ var m = (function app(window, undefined) {
} }
function thennable(then, successCallback, failureCallback, notThennableCallback) { function thennable(then, successCallback, failureCallback, notThennableCallback) {
if (((promiseValue != null && type.call(promiseValue) === OBJECT) || typeof promiseValue === FUNCTION) && typeof then === FUNCTION) { if (((promiseValue != null && isObject(promiseValue)) || isFunction(promiseValue)) && isFunction(then)) {
try { try {
// count protects against abuse calls from spec checker // count protects against abuse calls from spec checker
var count = 0; var count = 0;
@ -981,10 +995,10 @@ var m = (function app(window, undefined) {
fire(); fire();
}, function() { }, function() {
try { try {
if (state === RESOLVING && typeof successCallback === FUNCTION) { if (state === RESOLVING && isFunction(successCallback)) {
promiseValue = successCallback(promiseValue); promiseValue = successCallback(promiseValue);
} }
else if (state === REJECTING && typeof failureCallback === FUNCTION) { else if (state === REJECTING && isFunction(failureCallback)) {
promiseValue = failureCallback(promiseValue); promiseValue = failureCallback(promiseValue);
state = RESOLVING; state = RESOLVING;
} }
@ -1101,13 +1115,13 @@ var m = (function app(window, undefined) {
if (options.deserialize === JSON.parse) { if (options.deserialize === JSON.parse) {
xhr.setRequestHeader("Accept", "application/json, text/*"); xhr.setRequestHeader("Accept", "application/json, text/*");
} }
if (typeof options.config === FUNCTION) { if (isFunction(options.config)) {
var maybeXhr = options.config(xhr, options); var maybeXhr = options.config(xhr, options);
if (maybeXhr != null) xhr = maybeXhr; if (maybeXhr != null) xhr = maybeXhr;
} }
var data = options.method === "GET" || !options.data ? "" : options.data; var data = options.method === "GET" || !options.data ? "" : options.data;
if (data && (type.call(data) != STRING && data.constructor != window.FormData)) { if (data && (!isString(data) && data.constructor != window.FormData)) {
throw "Request data should be either be a string or FormData. Check the `serialize` option in `m.request`"; throw "Request data should be either be a string or FormData. Check the `serialize` option in `m.request`";
} }
xhr.send(data); xhr.send(data);
@ -1153,8 +1167,10 @@ var m = (function app(window, undefined) {
var unwrap = (e.type === "load" ? xhrOptions.unwrapSuccess : xhrOptions.unwrapError) || identity; var unwrap = (e.type === "load" ? xhrOptions.unwrapSuccess : xhrOptions.unwrapError) || identity;
var response = unwrap(deserialize(extract(e.target, xhrOptions)), e.target); var response = unwrap(deserialize(extract(e.target, xhrOptions)), e.target);
if (e.type === "load") { if (e.type === "load") {
if (type.call(response) === ARRAY && xhrOptions.type) { if (isArray(response) && xhrOptions.type) {
for (var i = 0; i < response.length; i++) response[i] = new xhrOptions.type(response[i]); forEach(response, function (res, i) {
response[i] = new xhrOptions.type(res);
});
} }
else if (xhrOptions.type) response = new xhrOptions.type(response); else if (xhrOptions.type) response = new xhrOptions.type(response);
} }

2
mithril.min.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long