From b936dfb96bd232fd4d00491ee6cefd6d17ccd073 Mon Sep 17 00:00:00 2001 From: John-David Dalton Date: Thu, 3 May 2012 00:23:57 -0400 Subject: [PATCH] lodash: Optimize `template`. [jddalton] --- build/pre-compile.js | 3 +- lodash.js | 152 ++++++++++++++++++++++--------------------- 2 files changed, 81 insertions(+), 74 deletions(-) diff --git a/build/pre-compile.js b/build/pre-compile.js index f24896c34b..3200e854fc 100644 --- a/build/pre-compile.js +++ b/build/pre-compile.js @@ -84,6 +84,7 @@ 'isEqual', 'isFinite', 'setTimeout', + 'source', 'templateSettings', 'toArray', 'value', @@ -112,7 +113,7 @@ // remove whitespace from string literals source = source.replace(/'(?:(?=(\\?))\1.)*?'/g, function(string) { - return string.replace(/\[object |else if|function | in |return\s+[\w']|throw |use strict|var |'\\n'|\\n|\s+/g, function(match) { + return string.replace(/\[object |else if|function | in |return\s+[\w']|throw |use strict|var |['|]\\n[|']|\\n|\s+/g, function(match) { return match == false || match == '\\n' ? '' : match; }); }); diff --git a/lodash.js b/lodash.js index e4e94aa56d..60675b679d 100644 --- a/lodash.js +++ b/lodash.js @@ -8,24 +8,17 @@ ;(function(window, undefined) { 'use strict'; - /** Used to escape and unescape characters in templates */ + /** Used to escape characters in templates */ var escapes = { '\\': '\\', "'": "'", - 'r': '\r', - 'n': '\n', - 't': '\t', - 'u2028': '\u2028', - 'u2029': '\u2029' + '\n': 'n', + '\r': 'r', + '\t': 't', + '\u2028': 'u2028', + '\u2029': 'u2029' }; - // assign the result as keys and the keys as values - (function() { - for (var prop in escapes) { - escapes[escapes[prop]] = prop; - } - }()); - /** Detect free variable `exports` */ var freeExports = typeof exports == 'object' && exports && (typeof global == 'object' && global && global == global.global && (window = global), exports); @@ -36,18 +29,11 @@ /** Used to restore the original `_` reference in `noConflict` */ var oldDash = window._; - /** Used to replace unescape characters with their escaped counterpart */ - var reEscaper = /\\|'|\r|\n|\t|\u2028|\u2029/g; - - /** - * Used for `templateSettings` properties such as `escape`, `evaluate`, - * or `interpolate` with explicitly assigned falsey values to ensure no match - * is made. - */ - var reNoMatch = /.^/; + /** Used to store unique `reUnescaped` regexps */ + var reCache = {}; - /** Used to replace escaped characters with their unescaped counterpart */ - var reUnescaper = /\\(\\|'|r|n|t|u2028|u2029)/g; + /** Used to extract a regexp from its `source` property */ + var reSource = /^\/([\s\S]+?)\/[gim]+?$/; /** Object#toString result shortcuts */ var arrayClass = '[object Array]', @@ -65,6 +51,7 @@ /** Native method shortcuts */ var concat = ArrayProto.concat, hasOwnProperty = ObjProto.hasOwnProperty, + join = ArrayProto.join, push = ArrayProto.push, slice = ArrayProto.slice, toString = ObjProto.toString; @@ -225,6 +212,19 @@ /*--------------------------------------------------------------------------*/ + /** + * Used by `String#replace` to escape characters for inclusion in compiled + * string literals while skipping over template delimiters. + * + * @private + * @param {String} character The the character to escape. + * @param {String} [delimiter=''] The delimiter to skip over. + * @returns {String} Returns an escaped character or template delimiter. + */ + function escapeString(character, delimiter) { + return delimiter || '\\' + escapes[character]; + } + /** * Compiles iteration functions. * @@ -324,21 +324,6 @@ indexOf, Infinity, isArray, isEmpty, Math, slice, stringClass, toString); } - /** - * Unescapes characters, previously escaped for inclusion in compiled string - * literals, so they may compiled into function bodies. - * (Used for template interpolation, evaluation, or escaping) - * - * @private - * @param {String} string The string to unescape. - * @returns {String} Returns the unescaped string. - */ - function unescape(string) { - return string.replace(reUnescaper, function(match, escaped) { - return escapes[escaped]; - }); - } - /*--------------------------------------------------------------------------*/ /** @@ -2594,45 +2579,66 @@ function template(text, data, options) { options = defaults(options || {}, lodash.templateSettings); - // Compile the template source, taking care to escape characters that - // cannot be included in string literals and then unescape them in code - // blocks. - var source = "__p+='" + text - .replace(reEscaper, function(match) { - return '\\' + escapes[match]; - }) - .replace(options.escape || reNoMatch, function(match, code) { - return "'+\n((__t=(" + unescape(code) + "))==null?'':_.escape(__t))+\n'"; - }) - .replace(options.interpolate || reNoMatch, function(match, code) { - return "'+\n((__t=(" + unescape(code) + "))==null?'':__t)+\n'"; - }) - .replace(options.evaluate || reNoMatch, function(match, code) { - return "';\n" + unescape(code) + ";\n__p+='"; - }) + "';\n"; - - // if a variable is not specified, place data values in local scope - if (!options.variable) { - source = 'with (object || {}) {\n' + source + '\n}\n'; - } - - source = 'var __t, __j = Array.prototype.join, __p = "";' + - 'function print() { __p += __j.call(arguments, "") }\n' + - source + 'return __p'; + var result, + reEscapeDelimiter = options.escape, + reEvaluateDelimiter = options.evaluate, + reInterpolateDelimiter = options.interpolate, + id = reEscapeDelimiter + reEvaluateDelimiter + reInterpolateDelimiter, + reUnescaped = reCache[id], + variable = options.variable; + + // create and cache the `reUnescaped` regexp + if (!reUnescaped) { + reUnescaped = []; + if (reEscapeDelimiter) { + reUnescaped.push(reSource.exec(reEscapeDelimiter)[1]); + } + if (reEvaluateDelimiter) { + reUnescaped.push(reSource.exec(reEvaluateDelimiter)[1]); + } + if (reInterpolateDelimiter) { + reUnescaped.push(reSource.exec(reInterpolateDelimiter)[1]); + } + reUnescaped = reCache[id] = RegExp('\\\\|\'|\\r|\\n|\\t|\\u2028|\\u2029' + + (reUnescaped.length ? '|((?:' + reUnescaped.join(')|(?:') + '))' : ''), 'g'); + } - var render = Function(options.variable || 'object', '_', source); - if (data) { - return render(data, lodash); + // escape characters that cannot be included in string literals + text = text.replace(reUnescaped, escapeString); + + if (reEscapeDelimiter) { + text = text.replace(reEscapeDelimiter, "'+\n((__t=($1))==null?'':__e(__t))+\n'") + } + if (reInterpolateDelimiter) { + text = text.replace(reInterpolateDelimiter, "'+\n((__t=($1))==null?'':__t)+\n'"); + } + if (reEvaluateDelimiter) { + text = text.replace(reEvaluateDelimiter, "';\n$1;\n__p+='"); } - var template = function(data) { - return render.call(this, data, lodash); - }; + text = "__p='" + text + "';\n"; - // provide the compiled function source as a convenience for build time precompilation - template.source = 'function(' + (options.variable || 'object') + '){\n' + source + '\n}'; + // if `options.variable` is not specified, add `data` to the top of the scope chain + if (!variable) { + text = 'with (object || {}) {\n' + text + '\n}\n'; + } + + text = 'function(' + (variable || 'object') + '){\n' + + 'var __p, __t;\n' + + 'function print() { __p += __j.call(arguments, "") }\n' + + text + + 'return __p\n}'; - return template; + result = Function('_,__e,__j', 'return ' + text)(lodash, escape, join); + + if (data) { + return result(data); + } + // provide the compiled function's source via its `toString()` method, in + // supported environments, or the `source` property as a convenience for + // build time precompilation + result.source = text; + return result; } /*--------------------------------------------------------------------------*/