From 3b4074bfc7d4a3e2600846b990608279fc7c7cde Mon Sep 17 00:00:00 2001 From: John-David Dalton Date: Mon, 2 Jul 2012 00:07:22 -0400 Subject: [PATCH] Optimize `_.template` when no evaluate delimiters are used and optimize `_.bind` when partially applied in V8. Former-commit-id: 25489d41ba3cac7ac3f1414e09f1971a11a779be --- build.js | 20 +----- build/pre-compile.js | 10 ++- lodash.js | 156 ++++++++++++++++++++++++++++--------------- 3 files changed, 113 insertions(+), 73 deletions(-) diff --git a/build.js b/build.js index b1dd642c7..252b57bbf 100755 --- a/build.js +++ b/build.js @@ -301,7 +301,7 @@ * @returns {String} Returns the formatted source. */ function getFunctionSource(func) { - var source = (func + ''); + var source = func.source || (func + ''); return source.replace(/\n(?:.*)/g, function(match, index) { match = match.slice(1); return ( @@ -465,19 +465,6 @@ return removeFromCreateIterator(source, varName); } - /** - * Removes non-syntax critical whitespace from a string. - * - * @private - * @param {String} source The source to process. - * @returns {String} Returns the source with whitespace removed. - */ - function removeWhitespace(source) { - return source.replace(/\[object |else if|function | in |return\s+[\w']|throw |typeof |var |@ |\\\\n|\\n|\s+/g, function(match) { - return match == false || match == '\\n' ? '' : match; - }); - } - /*--------------------------------------------------------------------------*/ // Backbone build @@ -720,8 +707,8 @@ } else { // inline `iteratorTemplate` template - source = source.replace(/(( +)var iteratorTemplate *= *)([\s\S]+?\n\2.+?);\n/, (function() { - var code = getFunctionSource(lodash._iteratorTemplate.source); + source = source.replace(/(( +)var iteratorTemplate *= *)[\s\S]+?\n\2.+?;\n/, (function() { + var code = getFunctionSource(lodash._iteratorTemplate); // expand properties to avoid having to use a with-statement iteratorOptions.forEach(function(property) { @@ -735,7 +722,6 @@ .replace(/__p *\+= *' *';/g, '') .replace(/(__p *\+= *)' *' *\+/g, '$1') .replace(/\+\s*' *';/g, ';') - .replace(/';(?:\\n|\s)*} *'/g, "'}'") .replace(/(\{) *;|; *(\})/g, '$1$2') .replace(/\(\(__t *= *\( *([^)]+) *\)\) *== *null *\? *'' *: *__t\)/g, '$1'); diff --git a/build/pre-compile.js b/build/pre-compile.js index f9ec451c8..5d75c09ba 100644 --- a/build/pre-compile.js +++ b/build/pre-compile.js @@ -207,7 +207,10 @@ source = source.replace(RegExp('\\.(' + propWhitelist.join('|') + ')\\b', 'g'), "['$1']"); // remove brackets from `_.escape()` in `tokenizeEscape` - source = source.replace("_['escape'](\"", '_.escape("'); + source = source.replace(/_\['escape']\("/, '_.escape("'); + + // remove brackets from `_.escape()` in `_.template` + source = source.replace(/__e *= *_\['escape']/, '__e=_.escape'); // remove brackets from `result[length].value` in `_.sortBy` source = source.replace("result[length]['value']", 'result[length].value'); @@ -220,6 +223,11 @@ }); }); + // remove whitespace from `_.template` related regexpes + source = source.replace(/(?:reEmptyString\w+|reInsertVariable) *=.+/g, function(match) { + return match.replace(/ /g, ''); + }); + // remove newline from double-quoted string in `_.template` source = source.replace('"\';\\n"', '"\';"'); diff --git a/lodash.js b/lodash.js index 3d5d6cb39..a82c44bf4 100644 --- a/lodash.js +++ b/lodash.js @@ -8,10 +8,21 @@ ;(function(window, undefined) { 'use strict'; + /** + * Used to match potentially incorrect data object references, i.e. `obj.obj`, + * in compiled templates. The variables are assigned in `_.template`. + */ + var lastVariable, + reDoubleVariable; + /** Detect free variable `exports` */ var freeExports = typeof exports == 'object' && exports && (typeof global == 'object' && global && global == global.global && (window = global), exports); + /** Native prototype shortcuts */ + var ArrayProto = Array.prototype, + ObjectProto = Object.prototype; + /** * Detect the JScript [[DontEnum]] bug: * In IE < 9 an objects own properties, shadowing non-enumerable ones, are @@ -25,10 +36,20 @@ /** Used to restore the original `_` reference in `noConflict` */ var oldDash = window._; + /** Used to match empty strings in compiled templates */ + var reEmptyStringEvaluate = /\b__p \+= '';/g, + reEmptyStringInterpolate = /\b__p \+= '' \+/g, + reEmptyStringHybrid = /\b__t\) \+\n'';/g; + + /** Used to insert the data object variable into compiled templates */ + var reInsertVariable = /(?:__e|__t = )\(\s*(?![\s"']|this\.)/g; + /** Used to detect if a method is native */ - var reNative = RegExp('^' + ({}.valueOf + '') - .replace(/[.*+?^=!:${}()|[\]\/\\]/g, '\\$&') - .replace(/valueOf|for [^\]]+/g, '.+?') + '$'); + var reNative = RegExp('^' + + (ObjectProto.valueOf + '') + .replace(/[.*+?^=!:${}()|[\]\/\\]/g, '\\$&') + .replace(/valueOf|for [^\]]+/g, '.+?') + '$' + ); /** Used to match tokens in template text */ var reToken = /__token__(\d+)/g; @@ -54,12 +75,41 @@ /** Used to store tokenized template text snippets */ var tokenized = []; + /* Detect if `Function#bind` exists and is inferred to be fast (i.e. all but V8) */ + var useNativeBind = nativeBind && /\n|Opera/.test(nativeBind + toString.call(window.opera)); + /** Detect if sourceURL syntax is usable without erroring */ try { // Adobe's and Narwhal's JS engines will error var useSourceURL = (Function('//@')(), true); } catch(e){ } + /** Native method shortcuts */ + var concat = ArrayProto.concat, + hasOwnProperty = ObjectProto.hasOwnProperty, + push = ArrayProto.push, + slice = ArrayProto.slice, + toString = ObjectProto.toString; + + /* Native method shortcuts for methods with the same name as other `lodash` methods */ + var nativeBind = reNative.test(nativeBind = slice.bind) && nativeBind, + nativeIsArray = reNative.test(nativeIsArray = Array.isArray) && nativeIsArray, + nativeIsFinite = window.isFinite, + nativeKeys = reNative.test(nativeKeys = Object.keys) && nativeKeys; + + /** Object#toString result shortcuts */ + var arrayClass = '[object Array]', + boolClass = '[object Boolean]', + dateClass = '[object Date]', + funcClass = '[object Function]', + numberClass = '[object Number]', + regexpClass = '[object RegExp]', + stringClass = '[object String]'; + + /** Timer shortcuts */ + var clearTimeout = window.clearTimeout, + setTimeout = window.setTimeout; + /** * Used to escape characters for inclusion in HTML. * The `>` and `/` characters don't require escaping in HTML and have no @@ -94,39 +144,6 @@ '\u2029': 'u2029' }; - /** Object#toString result shortcuts */ - var arrayClass = '[object Array]', - boolClass = '[object Boolean]', - dateClass = '[object Date]', - funcClass = '[object Function]', - numberClass = '[object Number]', - regexpClass = '[object RegExp]', - stringClass = '[object String]'; - - /** Native prototype shortcuts */ - var ArrayProto = Array.prototype, - ObjectProto = Object.prototype; - - /** Native method shortcuts */ - var concat = ArrayProto.concat, - hasOwnProperty = ObjectProto.hasOwnProperty, - push = ArrayProto.push, - slice = ArrayProto.slice, - toString = ObjectProto.toString; - - /* Used if `Function#bind` exists and is inferred to be fast (i.e. all but V8) */ - var nativeBind = reNative.test(nativeBind = slice.bind) && - /\n|Opera/.test(nativeBind + toString.call(window.opera)) && nativeBind; - - /* Native method shortcuts for methods with the same name as other `lodash` methods */ - var nativeIsArray = reNative.test(nativeIsArray = Array.isArray) && nativeIsArray, - nativeIsFinite = window.isFinite, - nativeKeys = reNative.test(nativeKeys = Object.keys) && nativeKeys; - - /** Timer shortcuts */ - var clearTimeout = window.clearTimeout, - setTimeout = window.setTimeout; - /*--------------------------------------------------------------------------*/ /** @@ -158,8 +175,8 @@ } /** - * By default, Lo-Dash uses ERB-style template delimiters, change the - * following template settings to use alternative delimiters. + * By default, Lo-Dash uses embedded Ruby (ERB) style template delimiters, + * change the following template settings to use alternative delimiters. * * @static * @memberOf _ @@ -227,7 +244,7 @@ ' <% if (objectBranch) { %>\nif (length === length >>> 0) {<% } %>\n' + ' <%= arrayBranch.beforeLoop %>;\n' + ' while (<%= arrayBranch.loopExp %>) {\n' + - ' <%= arrayBranch.inLoop %>;\n' + + ' <%= arrayBranch.inLoop %>\n' + ' }' + ' <% if (objectBranch) { %>\n}\n<% }' + '}' + @@ -254,7 +271,7 @@ // the the `prototype` property of functions regardless of its // [[Enumerable]] value. ' if (!(skipProto && index == \'prototype\')<% if (useHas) { %> && <%= hasExp %><% } %>) {\n' + - ' <%= objectBranch.inLoop %>;\n' + + ' <%= objectBranch.inLoop %>\n' + ' }' + ' <% } %>\n' + ' }' + @@ -271,7 +288,7 @@ ' if (shadowed[k] == \'constructor\') {' + ' %>!(ctor && ctor.prototype === <%= iteratedObject %>) && <%' + ' } %><%= hasExp %>) {\n' + - ' <%= objectBranch.inLoop %>;\n' + + ' <%= objectBranch.inLoop %>\n' + ' }<%' + ' }' + ' }' + @@ -340,7 +357,7 @@ } }; - /** Reusable iterator options for `invoke`, `map`, `pluck`, and `sortBy` */ + /** Reusable iterator options for `invoke`, `map`, `pluck`, and `sortBy` */ var mapIteratorOptions = { 'init': '', 'exit': 'if (!collection) return []', @@ -568,7 +585,7 @@ */ function tokenizeEscape(match, value) { var index = tokenized.length; - tokenized[index] = "'+\n_.escape(" + value + ") +\n'"; + tokenized[index] = "' +\n__e(" + value + ") +\n'"; return token + index; } @@ -582,7 +599,7 @@ */ function tokenizeInterpolate(match, value) { var index = tokenized.length; - tokenized[index] = "'+\n((__t = (" + value + ")) == null ? '' : __t) +\n'"; + tokenized[index] = "' +\n((__t = (" + value + ")) == null ? '' : __t) +\n'"; return token + index; } @@ -1859,24 +1876,24 @@ * * // basic bind * var func = function(greeting) { - * return greeting + ': ' + this.name; + * return greeting + ' ' + this.name; * }; * * func = _.bind(func, { 'name': 'moe' }, 'hi'); * func(); - * // => 'hi: moe' + * // => 'hi moe' * * // lazy bind * var object = { * 'name': 'moe', * 'greet': function(greeting) { - * return greeting + ': ' + this.name; + * return greeting + ' ' + this.name; * } * }; * * var func = _.bind(object, 'greet', 'hi'); * func(); - * // => 'hi: moe' + * // => 'hi moe' * * object.greet = function(greeting) { * return greeting + ', ' + this.name + '!'; @@ -1894,8 +1911,8 @@ methodName = thisArg; thisArg = func; } - // use if `Function#bind` is faster - else if (nativeBind) { + // use if native `Function#bind` is faster + else if (useNativeBind || (nativeBind && arguments.length > 2)) { return nativeBind.call.apply(nativeBind, arguments); } @@ -1929,7 +1946,6 @@ } return func.apply(thisBinding, args); } - return bound; } @@ -3185,7 +3201,8 @@ // https://github.com/olado/doT options || (options = {}); - var isEvaluating, + var isEscaping, + isEvaluating, isInterpolating, result, defaults = lodash.templateSettings, @@ -3207,7 +3224,7 @@ // tokenize delimiters to avoid escaping them if (escapeDelimiter) { - text = text.replace(escapeDelimiter, tokenizeEscape); + isEscaping = text != (text = text.replace(escapeDelimiter, tokenizeEscape)); } if (interpolateDelimiter) { isInterpolating = text != (text = text.replace(interpolateDelimiter, tokenizeInterpolate)); @@ -3225,10 +3242,35 @@ // clear stored code snippets tokenized.length = 0; - // if `options.variable` is not specified, add `data` to the top of the scope chain + // strip concating empty strings + if (isInterpolating) { + text = text.replace(reEmptyStringInterpolate, '__p \+='); + } + if (isEvaluating) { + text = text.replace(reEmptyStringEvaluate, ''); + if (isInterpolating) { + text = text.replace(reEmptyStringHybrid, '__t);'); + } + } + if (!variable) { variable = defaults.variable; - text = 'with (' + variable + ' || {}) {\n' + text + '\n}\n'; + + // if `options.variable` is not specified or the template contains "evaluate" + // delimiters, add the data object to the top of the scope chain + if (isEvaluating || !variable) { + text = 'with (' + variable + ' || {}) {\n' + text + '\n}\n'; + } + // else insert data object references to avoid using a with-statement + else { + if (variable != lastVariable) { + lastVariable = variable; + reDoubleVariable = RegExp('([(\\s])(' + variable + '\\.' + variable + ')\\b', 'g'); + } + text = text + .replace(reInsertVariable, '$&' + variable + '.') + .replace(reDoubleVariable, '$1($2 || ' + variable + ')'); + } } text = 'function(' + variable + ') {\n' + @@ -3237,6 +3279,10 @@ ? ', __t' : '' ) + + (isEscaping + ? ', __e = _.escape' + : '' + ) + (isEvaluating ? ', __j = Array.prototype.join;\n' + 'function print() { __p += __j.call(arguments, \'\') }\n'