├── .gitignore ├── README.md ├── config.js ├── fixtures ├── es5 │ ├── angular.js │ ├── babylon-dist.js │ ├── backbone.js │ ├── ember.debug.js │ ├── jquery.js │ └── react-with-addons.js └── es6 │ ├── angular-compiler.js │ ├── import.js │ └── material-ui-core.js ├── index.js ├── mem.js ├── package.json ├── parse.js ├── util.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | npm-debug.log 2 | node_modules 3 | isolate*.log 4 | cpuprofile/ 5 | *.bytecode 6 | *.asm -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # parser_performance 2 | 3 | > Parses various [fixtures](/fixtures) and outputs parse times over various iterations 4 | 5 | ## Run 6 | 7 | ### build a production release of babel-parser 8 | ``` 9 | cd babel 10 | NODE_ENV=production BABEL_ENV=production gulp build-rollup 11 | ``` 12 | 13 | ### Run parser performance 14 | It is recommended to clone `parser_performance` next to `babel` repository 15 | ```sh 16 | git clone git@github.com:babel/parser_performance.git 17 | yarn 18 | PARSER_ALL=1 yarn run test // performance test 19 | yarn run memory // memory usage test 20 | ``` 21 | 22 | ## Performance PRs 23 | 24 | Check the [performance](https://github.com/babel/babel/issues?utf8=%E2%9C%93&q=label%3A"area%3A+perf"%20is%3Aboth) label in the babel repo for some examples. 25 | 26 | ## Performance Test 27 | ```sh 28 | # Run performance test with all fixtures on local babel build 29 | yarn run test 30 | 31 | # Run performance test on ember.js fixture and compared to baseline babel parser 32 | FILE=ember PARSER=babel,dev yarn run test 33 | 34 | # Run performance test on all parsers and all files 35 | PARSER_ALL=1 yarn run test 36 | 37 | # Specify a custom babel parser path and run performance test on all files 38 | BABEL_PARSER_PATH=relative/path/from/parser_performance/to/babel-parser yarn run test 39 | ``` 40 | ## Perf Tips 41 | 42 | Microbenchmarks don't help that much, should test the real thing? (Also I don't know what I'm talking about) 43 | 44 | - Caching, `Set.has` vs. `[].indexOf(val)`, hoisting, GC issues 45 | - Make sure node shapes are the same (should be automated) https://github.com/babel/notes/issues/9 46 | 47 | ## Checking Performance 48 | 49 | ### Install/Use Node 12 50 | 51 | ```sh 52 | nvm use 12 53 | node -v 54 | ``` 55 | 56 | ### Install NIM 57 | 58 | > https://chrome.google.com/webstore/detail/nodejs-v8-inspector-manag/gnhhdgbaldcilmgcpfddgdbkhjohddkj?hl=en 59 | 60 | It's a chrome Extension that helps automatically open the devtools when running --inspect 61 | 62 | ### Use `node --prof` 63 | 64 | > https://nodejs.org/en/docs/guides/simple-profiling/ 65 | 66 | ```sh 67 | node --prof script.js 68 | node --prof-process isolate*.log 69 | # node --prof-process isolate*.log > out.txt 70 | ``` 71 | 72 | With @babel/parser: 73 | 74 | ```sh 75 | node --prof ./node_modules/@babel/parser/bin/babel-parser.js fixtures/es5/ember.debug.js > /dev/null 76 | node --prof-process isolate*.log 77 | ``` 78 | 79 | ### Use `npm run cpu-prof` 80 | 81 | Node.js 12 introduces [`--cpu-prof`](https://nodejs.org/api/cli.html#cli_cpu_prof) to starts V8 CPU Profiler on start up. 82 | 83 | ```sh 84 | # Generate CPU Profile running dev parser on ember, 85 | # This command will output a cpu profile inside the ./cpuprofile directory, i.e. `CPU.20190906.174010.51327.0.001.cpuprofile` 86 | PARSER=dev FILE=ember npm run cpu-prof 87 | 88 | ``` 89 | 90 | [Load](https://developers.google.com/web/tools/chrome-devtools/evaluate-performance/reference#load) generated cpu profile to Chrome Devtools, and [analyze](https://developers.google.com/web/tools/chrome-devtools/evaluate-performance/reference#analyze) the performance recording. 91 | 92 | ### Use `node --trace-opt` 93 | 94 | ```sh 95 | node --trace-opt script.js | grep myFunc 96 | node --trace-opt ./node_modules/@babel/parser/bin/babel-parser.js fixtures/es5/ember.debug.js 97 | ``` 98 | 99 | ### Use `node --inspect-brk` 100 | 101 | > https://medium.com/@paul_irish/debugging-node-js-nightlies-with-chrome-devtools-7c4a1b95ae27 102 | 103 | Point node to the @babel/parser script and pass in a file to parse 104 | 105 | > In this case I am running node in `parser` with `parser_performance/` in sibling folder 106 | 107 | ```sh 108 | cd parser 109 | 110 | # node --inspect-brk script.js 111 | node --inspect-brk ./bin/babel-parser.js ../parser_performance/fixtures/es5/angular.js 112 | ``` 113 | 114 | If you have install NIM, it should open up chrome and show this view: (if not you can open the url shown in the console yourself) 115 | 116 | ![Imgur](http://i.imgur.com/i7YIyrH.png) 117 | 118 | Then click on the "Profiler" Tab 119 | 120 | ![Imgur](http://i.imgur.com/MI0IrZ9.png) 121 | 122 | Then click "Start" 123 | 124 | ![Imgur](http://i.imgur.com/XGKKjRy.png) 125 | 126 | Wait a little bit and click "Stop", and you will be redirect to this screen 127 | 128 | ![Imgur](http://i.imgur.com/9wYUfXV.png) 129 | 130 | ### Use `npm run print-bytecode` 131 | 132 | ```sh 133 | # Use develop babel to parse material-ui-core fixture, output the bytecode 134 | # generated by ignition interpreter to `parse.bytecode` 135 | FILE=material npm run print-bytecode 136 | 137 | # Specify `PARSER` to use baseline babel or other parsers 138 | FILE=material PARSER=babel npm run print-bytecode 139 | ``` 140 | 141 | ### User `npm run print-code` 142 | ```sh 143 | # Use develop babel to parse material-ui-core fixture, output the optimized dissembly code 144 | # generated by turbofan compiler to `parse.asm` 145 | FILE=material npm run print-code 146 | ``` 147 | 148 | ## Some Links 149 | 150 | - https://jsperf.com/ 151 | - https://esbench.com/bench 152 | - https://github.com/vhf/v8-bailout-reasons 153 | - https://community.risingstack.com/how-to-find-node-js-performance-optimization-killers/ 154 | - https://github.com/GoogleChrome/devtools-docs/issues/53 155 | - https://gist.github.com/kevincennis/0cd2138c78a07412ef21 156 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | const babelDevPath = process.env.BABEL_PARSER_PATH || "../babel/packages/babel-parser"; 2 | 3 | const babelParse = require("@babel/parser").parse; 4 | const babelDevParse = require(babelDevPath).parse; 5 | const acornParse = require("acorn").parse; 6 | const esprimaParse = require("esprima").parse; 7 | const meriyahParse = require("meriyah").parseModule; 8 | 9 | exports.files = [ 10 | "./fixtures/es5/angular.js", 11 | "./fixtures/es5/ember.debug.js", 12 | "./fixtures/es5/babylon-dist.js", 13 | "./fixtures/es5/jquery.js", 14 | "./fixtures/es5/backbone.js", 15 | "./fixtures/es5/react-with-addons.js", 16 | "./fixtures/es6/angular-compiler.js", 17 | "./fixtures/es6/material-ui-core.js", 18 | ].filter(file => { 19 | return !process.env.FILE || file.includes(process.env.FILE) 20 | }); 21 | 22 | exports.benchmarkOptions = { 23 | minSamples: 16000 24 | }; 25 | 26 | const parsers = { 27 | acorn: { 28 | parse: acornParse, 29 | options: { sourceType: "module", locations: true } 30 | }, 31 | babel: { 32 | parse: babelParse, 33 | options: { sourceType: "module" } 34 | }, 35 | dev: { 36 | parse: babelDevParse, 37 | options: { sourceType: "module" } 38 | }, 39 | esprima: { 40 | parse: esprimaParse, 41 | options: { sourceType: "module", loc: true, comment: true, attachComment: true } 42 | }, 43 | meriyah: { 44 | parse: meriyahParse, 45 | options: { loc: true } 46 | }, 47 | }; 48 | 49 | const parserSelection = (function () { 50 | if (process.env.PARSER_ALL) { 51 | return Object.keys(parsers); 52 | } 53 | if (process.env.PARSER) { 54 | return process.env.PARSER.split(","); 55 | } else { 56 | return ["dev"]; 57 | } 58 | })(); 59 | 60 | exports.parsers = Object.keys(parsers).filter(key => { 61 | return parserSelection.includes(key); 62 | }).reduce((p, key) => { 63 | p[key] = parsers[key]; 64 | return p; 65 | }, {}); 66 | -------------------------------------------------------------------------------- /fixtures/es5/backbone.js: -------------------------------------------------------------------------------- 1 | // Backbone.js 1.3.3 2 | 3 | // (c) 2010-2016 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors 4 | // Backbone may be freely distributed under the MIT license. 5 | // For all details and documentation: 6 | // http://backbonejs.org 7 | 8 | (function(factory) { 9 | 10 | // Establish the root object, `window` (`self`) in the browser, or `global` on the server. 11 | // We use `self` instead of `window` for `WebWorker` support. 12 | var root = (typeof self == 'object' && self.self === self && self) || 13 | (typeof global == 'object' && global.global === global && global); 14 | 15 | // Set up Backbone appropriately for the environment. Start with AMD. 16 | if (typeof define === 'function' && define.amd) { 17 | define(['underscore', 'jquery', 'exports'], function(_, $, exports) { 18 | // Export global even in AMD case in case this script is loaded with 19 | // others that may still expect a global Backbone. 20 | root.Backbone = factory(root, exports, _, $); 21 | }); 22 | 23 | // Next for Node.js or CommonJS. jQuery may not be needed as a module. 24 | } else if (typeof exports !== 'undefined') { 25 | var _ = require('underscore'), $; 26 | try { $ = require('jquery'); } catch (e) {} 27 | factory(root, exports, _, $); 28 | 29 | // Finally, as a browser global. 30 | } else { 31 | root.Backbone = factory(root, {}, root._, (root.jQuery || root.Zepto || root.ender || root.$)); 32 | } 33 | 34 | })(function(root, Backbone, _, $) { 35 | 36 | // Initial Setup 37 | // ------------- 38 | 39 | // Save the previous value of the `Backbone` variable, so that it can be 40 | // restored later on, if `noConflict` is used. 41 | var previousBackbone = root.Backbone; 42 | 43 | // Create a local reference to a common array method we'll want to use later. 44 | var slice = Array.prototype.slice; 45 | 46 | // Current version of the library. Keep in sync with `package.json`. 47 | Backbone.VERSION = '1.3.3'; 48 | 49 | // For Backbone's purposes, jQuery, Zepto, Ender, or My Library (kidding) owns 50 | // the `$` variable. 51 | Backbone.$ = $; 52 | 53 | // Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable 54 | // to its previous owner. Returns a reference to this Backbone object. 55 | Backbone.noConflict = function() { 56 | root.Backbone = previousBackbone; 57 | return this; 58 | }; 59 | 60 | // Turn on `emulateHTTP` to support legacy HTTP servers. Setting this option 61 | // will fake `"PATCH"`, `"PUT"` and `"DELETE"` requests via the `_method` parameter and 62 | // set a `X-Http-Method-Override` header. 63 | Backbone.emulateHTTP = false; 64 | 65 | // Turn on `emulateJSON` to support legacy servers that can't deal with direct 66 | // `application/json` requests ... this will encode the body as 67 | // `application/x-www-form-urlencoded` instead and will send the model in a 68 | // form param named `model`. 69 | Backbone.emulateJSON = false; 70 | 71 | // Proxy Backbone class methods to Underscore functions, wrapping the model's 72 | // `attributes` object or collection's `models` array behind the scenes. 73 | // 74 | // collection.filter(function(model) { return model.get('age') > 10 }); 75 | // collection.each(this.addView); 76 | // 77 | // `Function#apply` can be slow so we use the method's arg count, if we know it. 78 | var addMethod = function(length, method, attribute) { 79 | switch (length) { 80 | case 1: return function() { 81 | return _[method](this[attribute]); 82 | }; 83 | case 2: return function(value) { 84 | return _[method](this[attribute], value); 85 | }; 86 | case 3: return function(iteratee, context) { 87 | return _[method](this[attribute], cb(iteratee, this), context); 88 | }; 89 | case 4: return function(iteratee, defaultVal, context) { 90 | return _[method](this[attribute], cb(iteratee, this), defaultVal, context); 91 | }; 92 | default: return function() { 93 | var args = slice.call(arguments); 94 | args.unshift(this[attribute]); 95 | return _[method].apply(_, args); 96 | }; 97 | } 98 | }; 99 | var addUnderscoreMethods = function(Class, methods, attribute) { 100 | _.each(methods, function(length, method) { 101 | if (_[method]) Class.prototype[method] = addMethod(length, method, attribute); 102 | }); 103 | }; 104 | 105 | // Support `collection.sortBy('attr')` and `collection.findWhere({id: 1})`. 106 | var cb = function(iteratee, instance) { 107 | if (_.isFunction(iteratee)) return iteratee; 108 | if (_.isObject(iteratee) && !instance._isModel(iteratee)) return modelMatcher(iteratee); 109 | if (_.isString(iteratee)) return function(model) { return model.get(iteratee); }; 110 | return iteratee; 111 | }; 112 | var modelMatcher = function(attrs) { 113 | var matcher = _.matches(attrs); 114 | return function(model) { 115 | return matcher(model.attributes); 116 | }; 117 | }; 118 | 119 | // Backbone.Events 120 | // --------------- 121 | 122 | // A module that can be mixed in to *any object* in order to provide it with 123 | // a custom event channel. You may bind a callback to an event with `on` or 124 | // remove with `off`; `trigger`-ing an event fires all callbacks in 125 | // succession. 126 | // 127 | // var object = {}; 128 | // _.extend(object, Backbone.Events); 129 | // object.on('expand', function(){ alert('expanded'); }); 130 | // object.trigger('expand'); 131 | // 132 | var Events = Backbone.Events = {}; 133 | 134 | // Regular expression used to split event strings. 135 | var eventSplitter = /\s+/; 136 | 137 | // Iterates over the standard `event, callback` (as well as the fancy multiple 138 | // space-separated events `"change blur", callback` and jQuery-style event 139 | // maps `{event: callback}`). 140 | var eventsApi = function(iteratee, events, name, callback, opts) { 141 | var i = 0, names; 142 | if (name && typeof name === 'object') { 143 | // Handle event maps. 144 | if (callback !== void 0 && 'context' in opts && opts.context === void 0) opts.context = callback; 145 | for (names = _.keys(name); i < names.length ; i++) { 146 | events = eventsApi(iteratee, events, names[i], name[names[i]], opts); 147 | } 148 | } else if (name && eventSplitter.test(name)) { 149 | // Handle space-separated event names by delegating them individually. 150 | for (names = name.split(eventSplitter); i < names.length; i++) { 151 | events = iteratee(events, names[i], callback, opts); 152 | } 153 | } else { 154 | // Finally, standard events. 155 | events = iteratee(events, name, callback, opts); 156 | } 157 | return events; 158 | }; 159 | 160 | // Bind an event to a `callback` function. Passing `"all"` will bind 161 | // the callback to all events fired. 162 | Events.on = function(name, callback, context) { 163 | return internalOn(this, name, callback, context); 164 | }; 165 | 166 | // Guard the `listening` argument from the public API. 167 | var internalOn = function(obj, name, callback, context, listening) { 168 | obj._events = eventsApi(onApi, obj._events || {}, name, callback, { 169 | context: context, 170 | ctx: obj, 171 | listening: listening 172 | }); 173 | 174 | if (listening) { 175 | var listeners = obj._listeners || (obj._listeners = {}); 176 | listeners[listening.id] = listening; 177 | } 178 | 179 | return obj; 180 | }; 181 | 182 | // Inversion-of-control versions of `on`. Tell *this* object to listen to 183 | // an event in another object... keeping track of what it's listening to 184 | // for easier unbinding later. 185 | Events.listenTo = function(obj, name, callback) { 186 | if (!obj) return this; 187 | var id = obj._listenId || (obj._listenId = _.uniqueId('l')); 188 | var listeningTo = this._listeningTo || (this._listeningTo = {}); 189 | var listening = listeningTo[id]; 190 | 191 | // This object is not listening to any other events on `obj` yet. 192 | // Setup the necessary references to track the listening callbacks. 193 | if (!listening) { 194 | var thisId = this._listenId || (this._listenId = _.uniqueId('l')); 195 | listening = listeningTo[id] = {obj: obj, objId: id, id: thisId, listeningTo: listeningTo, count: 0}; 196 | } 197 | 198 | // Bind callbacks on obj, and keep track of them on listening. 199 | internalOn(obj, name, callback, this, listening); 200 | return this; 201 | }; 202 | 203 | // The reducing API that adds a callback to the `events` object. 204 | var onApi = function(events, name, callback, options) { 205 | if (callback) { 206 | var handlers = events[name] || (events[name] = []); 207 | var context = options.context, ctx = options.ctx, listening = options.listening; 208 | if (listening) listening.count++; 209 | 210 | handlers.push({callback: callback, context: context, ctx: context || ctx, listening: listening}); 211 | } 212 | return events; 213 | }; 214 | 215 | // Remove one or many callbacks. If `context` is null, removes all 216 | // callbacks with that function. If `callback` is null, removes all 217 | // callbacks for the event. If `name` is null, removes all bound 218 | // callbacks for all events. 219 | Events.off = function(name, callback, context) { 220 | if (!this._events) return this; 221 | this._events = eventsApi(offApi, this._events, name, callback, { 222 | context: context, 223 | listeners: this._listeners 224 | }); 225 | return this; 226 | }; 227 | 228 | // Tell this object to stop listening to either specific events ... or 229 | // to every object it's currently listening to. 230 | Events.stopListening = function(obj, name, callback) { 231 | var listeningTo = this._listeningTo; 232 | if (!listeningTo) return this; 233 | 234 | var ids = obj ? [obj._listenId] : _.keys(listeningTo); 235 | 236 | for (var i = 0; i < ids.length; i++) { 237 | var listening = listeningTo[ids[i]]; 238 | 239 | // If listening doesn't exist, this object is not currently 240 | // listening to obj. Break out early. 241 | if (!listening) break; 242 | 243 | listening.obj.off(name, callback, this); 244 | } 245 | 246 | return this; 247 | }; 248 | 249 | // The reducing API that removes a callback from the `events` object. 250 | var offApi = function(events, name, callback, options) { 251 | if (!events) return; 252 | 253 | var i = 0, listening; 254 | var context = options.context, listeners = options.listeners; 255 | 256 | // Delete all events listeners and "drop" events. 257 | if (!name && !callback && !context) { 258 | var ids = _.keys(listeners); 259 | for (; i < ids.length; i++) { 260 | listening = listeners[ids[i]]; 261 | delete listeners[listening.id]; 262 | delete listening.listeningTo[listening.objId]; 263 | } 264 | return; 265 | } 266 | 267 | var names = name ? [name] : _.keys(events); 268 | for (; i < names.length; i++) { 269 | name = names[i]; 270 | var handlers = events[name]; 271 | 272 | // Bail out if there are no events stored. 273 | if (!handlers) break; 274 | 275 | // Replace events if there are any remaining. Otherwise, clean up. 276 | var remaining = []; 277 | for (var j = 0; j < handlers.length; j++) { 278 | var handler = handlers[j]; 279 | if ( 280 | callback && callback !== handler.callback && 281 | callback !== handler.callback._callback || 282 | context && context !== handler.context 283 | ) { 284 | remaining.push(handler); 285 | } else { 286 | listening = handler.listening; 287 | if (listening && --listening.count === 0) { 288 | delete listeners[listening.id]; 289 | delete listening.listeningTo[listening.objId]; 290 | } 291 | } 292 | } 293 | 294 | // Update tail event if the list has any events. Otherwise, clean up. 295 | if (remaining.length) { 296 | events[name] = remaining; 297 | } else { 298 | delete events[name]; 299 | } 300 | } 301 | return events; 302 | }; 303 | 304 | // Bind an event to only be triggered a single time. After the first time 305 | // the callback is invoked, its listener will be removed. If multiple events 306 | // are passed in using the space-separated syntax, the handler will fire 307 | // once for each event, not once for a combination of all events. 308 | Events.once = function(name, callback, context) { 309 | // Map the event into a `{event: once}` object. 310 | var events = eventsApi(onceMap, {}, name, callback, _.bind(this.off, this)); 311 | if (typeof name === 'string' && context == null) callback = void 0; 312 | return this.on(events, callback, context); 313 | }; 314 | 315 | // Inversion-of-control versions of `once`. 316 | Events.listenToOnce = function(obj, name, callback) { 317 | // Map the event into a `{event: once}` object. 318 | var events = eventsApi(onceMap, {}, name, callback, _.bind(this.stopListening, this, obj)); 319 | return this.listenTo(obj, events); 320 | }; 321 | 322 | // Reduces the event callbacks into a map of `{event: onceWrapper}`. 323 | // `offer` unbinds the `onceWrapper` after it has been called. 324 | var onceMap = function(map, name, callback, offer) { 325 | if (callback) { 326 | var once = map[name] = _.once(function() { 327 | offer(name, once); 328 | callback.apply(this, arguments); 329 | }); 330 | once._callback = callback; 331 | } 332 | return map; 333 | }; 334 | 335 | // Trigger one or many events, firing all bound callbacks. Callbacks are 336 | // passed the same arguments as `trigger` is, apart from the event name 337 | // (unless you're listening on `"all"`, which will cause your callback to 338 | // receive the true name of the event as the first argument). 339 | Events.trigger = function(name) { 340 | if (!this._events) return this; 341 | 342 | var length = Math.max(0, arguments.length - 1); 343 | var args = Array(length); 344 | for (var i = 0; i < length; i++) args[i] = arguments[i + 1]; 345 | 346 | eventsApi(triggerApi, this._events, name, void 0, args); 347 | return this; 348 | }; 349 | 350 | // Handles triggering the appropriate event callbacks. 351 | var triggerApi = function(objEvents, name, callback, args) { 352 | if (objEvents) { 353 | var events = objEvents[name]; 354 | var allEvents = objEvents.all; 355 | if (events && allEvents) allEvents = allEvents.slice(); 356 | if (events) triggerEvents(events, args); 357 | if (allEvents) triggerEvents(allEvents, [name].concat(args)); 358 | } 359 | return objEvents; 360 | }; 361 | 362 | // A difficult-to-believe, but optimized internal dispatch function for 363 | // triggering events. Tries to keep the usual cases speedy (most internal 364 | // Backbone events have 3 arguments). 365 | var triggerEvents = function(events, args) { 366 | var ev, i = -1, l = events.length, a1 = args[0], a2 = args[1], a3 = args[2]; 367 | switch (args.length) { 368 | case 0: while (++i < l) (ev = events[i]).callback.call(ev.ctx); return; 369 | case 1: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1); return; 370 | case 2: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2); return; 371 | case 3: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2, a3); return; 372 | default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args); return; 373 | } 374 | }; 375 | 376 | // Aliases for backwards compatibility. 377 | Events.bind = Events.on; 378 | Events.unbind = Events.off; 379 | 380 | // Allow the `Backbone` object to serve as a global event bus, for folks who 381 | // want global "pubsub" in a convenient place. 382 | _.extend(Backbone, Events); 383 | 384 | // Backbone.Model 385 | // -------------- 386 | 387 | // Backbone **Models** are the basic data object in the framework -- 388 | // frequently representing a row in a table in a database on your server. 389 | // A discrete chunk of data and a bunch of useful, related methods for 390 | // performing computations and transformations on that data. 391 | 392 | // Create a new model with the specified attributes. A client id (`cid`) 393 | // is automatically generated and assigned for you. 394 | var Model = Backbone.Model = function(attributes, options) { 395 | var attrs = attributes || {}; 396 | options || (options = {}); 397 | this.cid = _.uniqueId(this.cidPrefix); 398 | this.attributes = {}; 399 | if (options.collection) this.collection = options.collection; 400 | if (options.parse) attrs = this.parse(attrs, options) || {}; 401 | var defaults = _.result(this, 'defaults'); 402 | attrs = _.defaults(_.extend({}, defaults, attrs), defaults); 403 | this.set(attrs, options); 404 | this.changed = {}; 405 | this.initialize.apply(this, arguments); 406 | }; 407 | 408 | // Attach all inheritable methods to the Model prototype. 409 | _.extend(Model.prototype, Events, { 410 | 411 | // A hash of attributes whose current and previous value differ. 412 | changed: null, 413 | 414 | // The value returned during the last failed validation. 415 | validationError: null, 416 | 417 | // The default name for the JSON `id` attribute is `"id"`. MongoDB and 418 | // CouchDB users may want to set this to `"_id"`. 419 | idAttribute: 'id', 420 | 421 | // The prefix is used to create the client id which is used to identify models locally. 422 | // You may want to override this if you're experiencing name clashes with model ids. 423 | cidPrefix: 'c', 424 | 425 | // Initialize is an empty function by default. Override it with your own 426 | // initialization logic. 427 | initialize: function(){}, 428 | 429 | // Return a copy of the model's `attributes` object. 430 | toJSON: function(options) { 431 | return _.clone(this.attributes); 432 | }, 433 | 434 | // Proxy `Backbone.sync` by default -- but override this if you need 435 | // custom syncing semantics for *this* particular model. 436 | sync: function() { 437 | return Backbone.sync.apply(this, arguments); 438 | }, 439 | 440 | // Get the value of an attribute. 441 | get: function(attr) { 442 | return this.attributes[attr]; 443 | }, 444 | 445 | // Get the HTML-escaped value of an attribute. 446 | escape: function(attr) { 447 | return _.escape(this.get(attr)); 448 | }, 449 | 450 | // Returns `true` if the attribute contains a value that is not null 451 | // or undefined. 452 | has: function(attr) { 453 | return this.get(attr) != null; 454 | }, 455 | 456 | // Special-cased proxy to underscore's `_.matches` method. 457 | matches: function(attrs) { 458 | return !!_.iteratee(attrs, this)(this.attributes); 459 | }, 460 | 461 | // Set a hash of model attributes on the object, firing `"change"`. This is 462 | // the core primitive operation of a model, updating the data and notifying 463 | // anyone who needs to know about the change in state. The heart of the beast. 464 | set: function(key, val, options) { 465 | if (key == null) return this; 466 | 467 | // Handle both `"key", value` and `{key: value}` -style arguments. 468 | var attrs; 469 | if (typeof key === 'object') { 470 | attrs = key; 471 | options = val; 472 | } else { 473 | (attrs = {})[key] = val; 474 | } 475 | 476 | options || (options = {}); 477 | 478 | // Run validation. 479 | if (!this._validate(attrs, options)) return false; 480 | 481 | // Extract attributes and options. 482 | var unset = options.unset; 483 | var silent = options.silent; 484 | var changes = []; 485 | var changing = this._changing; 486 | this._changing = true; 487 | 488 | if (!changing) { 489 | this._previousAttributes = _.clone(this.attributes); 490 | this.changed = {}; 491 | } 492 | 493 | var current = this.attributes; 494 | var changed = this.changed; 495 | var prev = this._previousAttributes; 496 | 497 | // For each `set` attribute, update or delete the current value. 498 | for (var attr in attrs) { 499 | val = attrs[attr]; 500 | if (!_.isEqual(current[attr], val)) changes.push(attr); 501 | if (!_.isEqual(prev[attr], val)) { 502 | changed[attr] = val; 503 | } else { 504 | delete changed[attr]; 505 | } 506 | unset ? delete current[attr] : current[attr] = val; 507 | } 508 | 509 | // Update the `id`. 510 | if (this.idAttribute in attrs) this.id = this.get(this.idAttribute); 511 | 512 | // Trigger all relevant attribute changes. 513 | if (!silent) { 514 | if (changes.length) this._pending = options; 515 | for (var i = 0; i < changes.length; i++) { 516 | this.trigger('change:' + changes[i], this, current[changes[i]], options); 517 | } 518 | } 519 | 520 | // You might be wondering why there's a `while` loop here. Changes can 521 | // be recursively nested within `"change"` events. 522 | if (changing) return this; 523 | if (!silent) { 524 | while (this._pending) { 525 | options = this._pending; 526 | this._pending = false; 527 | this.trigger('change', this, options); 528 | } 529 | } 530 | this._pending = false; 531 | this._changing = false; 532 | return this; 533 | }, 534 | 535 | // Remove an attribute from the model, firing `"change"`. `unset` is a noop 536 | // if the attribute doesn't exist. 537 | unset: function(attr, options) { 538 | return this.set(attr, void 0, _.extend({}, options, {unset: true})); 539 | }, 540 | 541 | // Clear all attributes on the model, firing `"change"`. 542 | clear: function(options) { 543 | var attrs = {}; 544 | for (var key in this.attributes) attrs[key] = void 0; 545 | return this.set(attrs, _.extend({}, options, {unset: true})); 546 | }, 547 | 548 | // Determine if the model has changed since the last `"change"` event. 549 | // If you specify an attribute name, determine if that attribute has changed. 550 | hasChanged: function(attr) { 551 | if (attr == null) return !_.isEmpty(this.changed); 552 | return _.has(this.changed, attr); 553 | }, 554 | 555 | // Return an object containing all the attributes that have changed, or 556 | // false if there are no changed attributes. Useful for determining what 557 | // parts of a view need to be updated and/or what attributes need to be 558 | // persisted to the server. Unset attributes will be set to undefined. 559 | // You can also pass an attributes object to diff against the model, 560 | // determining if there *would be* a change. 561 | changedAttributes: function(diff) { 562 | if (!diff) return this.hasChanged() ? _.clone(this.changed) : false; 563 | var old = this._changing ? this._previousAttributes : this.attributes; 564 | var changed = {}; 565 | for (var attr in diff) { 566 | var val = diff[attr]; 567 | if (_.isEqual(old[attr], val)) continue; 568 | changed[attr] = val; 569 | } 570 | return _.size(changed) ? changed : false; 571 | }, 572 | 573 | // Get the previous value of an attribute, recorded at the time the last 574 | // `"change"` event was fired. 575 | previous: function(attr) { 576 | if (attr == null || !this._previousAttributes) return null; 577 | return this._previousAttributes[attr]; 578 | }, 579 | 580 | // Get all of the attributes of the model at the time of the previous 581 | // `"change"` event. 582 | previousAttributes: function() { 583 | return _.clone(this._previousAttributes); 584 | }, 585 | 586 | // Fetch the model from the server, merging the response with the model's 587 | // local attributes. Any changed attributes will trigger a "change" event. 588 | fetch: function(options) { 589 | options = _.extend({parse: true}, options); 590 | var model = this; 591 | var success = options.success; 592 | options.success = function(resp) { 593 | var serverAttrs = options.parse ? model.parse(resp, options) : resp; 594 | if (!model.set(serverAttrs, options)) return false; 595 | if (success) success.call(options.context, model, resp, options); 596 | model.trigger('sync', model, resp, options); 597 | }; 598 | wrapError(this, options); 599 | return this.sync('read', this, options); 600 | }, 601 | 602 | // Set a hash of model attributes, and sync the model to the server. 603 | // If the server returns an attributes hash that differs, the model's 604 | // state will be `set` again. 605 | save: function(key, val, options) { 606 | // Handle both `"key", value` and `{key: value}` -style arguments. 607 | var attrs; 608 | if (key == null || typeof key === 'object') { 609 | attrs = key; 610 | options = val; 611 | } else { 612 | (attrs = {})[key] = val; 613 | } 614 | 615 | options = _.extend({validate: true, parse: true}, options); 616 | var wait = options.wait; 617 | 618 | // If we're not waiting and attributes exist, save acts as 619 | // `set(attr).save(null, opts)` with validation. Otherwise, check if 620 | // the model will be valid when the attributes, if any, are set. 621 | if (attrs && !wait) { 622 | if (!this.set(attrs, options)) return false; 623 | } else if (!this._validate(attrs, options)) { 624 | return false; 625 | } 626 | 627 | // After a successful server-side save, the client is (optionally) 628 | // updated with the server-side state. 629 | var model = this; 630 | var success = options.success; 631 | var attributes = this.attributes; 632 | options.success = function(resp) { 633 | // Ensure attributes are restored during synchronous saves. 634 | model.attributes = attributes; 635 | var serverAttrs = options.parse ? model.parse(resp, options) : resp; 636 | if (wait) serverAttrs = _.extend({}, attrs, serverAttrs); 637 | if (serverAttrs && !model.set(serverAttrs, options)) return false; 638 | if (success) success.call(options.context, model, resp, options); 639 | model.trigger('sync', model, resp, options); 640 | }; 641 | wrapError(this, options); 642 | 643 | // Set temporary attributes if `{wait: true}` to properly find new ids. 644 | if (attrs && wait) this.attributes = _.extend({}, attributes, attrs); 645 | 646 | var method = this.isNew() ? 'create' : (options.patch ? 'patch' : 'update'); 647 | if (method === 'patch' && !options.attrs) options.attrs = attrs; 648 | var xhr = this.sync(method, this, options); 649 | 650 | // Restore attributes. 651 | this.attributes = attributes; 652 | 653 | return xhr; 654 | }, 655 | 656 | // Destroy this model on the server if it was already persisted. 657 | // Optimistically removes the model from its collection, if it has one. 658 | // If `wait: true` is passed, waits for the server to respond before removal. 659 | destroy: function(options) { 660 | options = options ? _.clone(options) : {}; 661 | var model = this; 662 | var success = options.success; 663 | var wait = options.wait; 664 | 665 | var destroy = function() { 666 | model.stopListening(); 667 | model.trigger('destroy', model, model.collection, options); 668 | }; 669 | 670 | options.success = function(resp) { 671 | if (wait) destroy(); 672 | if (success) success.call(options.context, model, resp, options); 673 | if (!model.isNew()) model.trigger('sync', model, resp, options); 674 | }; 675 | 676 | var xhr = false; 677 | if (this.isNew()) { 678 | _.defer(options.success); 679 | } else { 680 | wrapError(this, options); 681 | xhr = this.sync('delete', this, options); 682 | } 683 | if (!wait) destroy(); 684 | return xhr; 685 | }, 686 | 687 | // Default URL for the model's representation on the server -- if you're 688 | // using Backbone's restful methods, override this to change the endpoint 689 | // that will be called. 690 | url: function() { 691 | var base = 692 | _.result(this, 'urlRoot') || 693 | _.result(this.collection, 'url') || 694 | urlError(); 695 | if (this.isNew()) return base; 696 | var id = this.get(this.idAttribute); 697 | return base.replace(/[^\/]$/, '$&/') + encodeURIComponent(id); 698 | }, 699 | 700 | // **parse** converts a response into the hash of attributes to be `set` on 701 | // the model. The default implementation is just to pass the response along. 702 | parse: function(resp, options) { 703 | return resp; 704 | }, 705 | 706 | // Create a new model with identical attributes to this one. 707 | clone: function() { 708 | return new this.constructor(this.attributes); 709 | }, 710 | 711 | // A model is new if it has never been saved to the server, and lacks an id. 712 | isNew: function() { 713 | return !this.has(this.idAttribute); 714 | }, 715 | 716 | // Check if the model is currently in a valid state. 717 | isValid: function(options) { 718 | return this._validate({}, _.extend({}, options, {validate: true})); 719 | }, 720 | 721 | // Run validation against the next complete set of model attributes, 722 | // returning `true` if all is well. Otherwise, fire an `"invalid"` event. 723 | _validate: function(attrs, options) { 724 | if (!options.validate || !this.validate) return true; 725 | attrs = _.extend({}, this.attributes, attrs); 726 | var error = this.validationError = this.validate(attrs, options) || null; 727 | if (!error) return true; 728 | this.trigger('invalid', this, error, _.extend(options, {validationError: error})); 729 | return false; 730 | } 731 | 732 | }); 733 | 734 | // Underscore methods that we want to implement on the Model, mapped to the 735 | // number of arguments they take. 736 | var modelMethods = {keys: 1, values: 1, pairs: 1, invert: 1, pick: 0, 737 | omit: 0, chain: 1, isEmpty: 1}; 738 | 739 | // Mix in each Underscore method as a proxy to `Model#attributes`. 740 | addUnderscoreMethods(Model, modelMethods, 'attributes'); 741 | 742 | // Backbone.Collection 743 | // ------------------- 744 | 745 | // If models tend to represent a single row of data, a Backbone Collection is 746 | // more analogous to a table full of data ... or a small slice or page of that 747 | // table, or a collection of rows that belong together for a particular reason 748 | // -- all of the messages in this particular folder, all of the documents 749 | // belonging to this particular author, and so on. Collections maintain 750 | // indexes of their models, both in order, and for lookup by `id`. 751 | 752 | // Create a new **Collection**, perhaps to contain a specific type of `model`. 753 | // If a `comparator` is specified, the Collection will maintain 754 | // its models in sort order, as they're added and removed. 755 | var Collection = Backbone.Collection = function(models, options) { 756 | options || (options = {}); 757 | if (options.model) this.model = options.model; 758 | if (options.comparator !== void 0) this.comparator = options.comparator; 759 | this._reset(); 760 | this.initialize.apply(this, arguments); 761 | if (models) this.reset(models, _.extend({silent: true}, options)); 762 | }; 763 | 764 | // Default options for `Collection#set`. 765 | var setOptions = {add: true, remove: true, merge: true}; 766 | var addOptions = {add: true, remove: false}; 767 | 768 | // Splices `insert` into `array` at index `at`. 769 | var splice = function(array, insert, at) { 770 | at = Math.min(Math.max(at, 0), array.length); 771 | var tail = Array(array.length - at); 772 | var length = insert.length; 773 | var i; 774 | for (i = 0; i < tail.length; i++) tail[i] = array[i + at]; 775 | for (i = 0; i < length; i++) array[i + at] = insert[i]; 776 | for (i = 0; i < tail.length; i++) array[i + length + at] = tail[i]; 777 | }; 778 | 779 | // Define the Collection's inheritable methods. 780 | _.extend(Collection.prototype, Events, { 781 | 782 | // The default model for a collection is just a **Backbone.Model**. 783 | // This should be overridden in most cases. 784 | model: Model, 785 | 786 | // Initialize is an empty function by default. Override it with your own 787 | // initialization logic. 788 | initialize: function(){}, 789 | 790 | // The JSON representation of a Collection is an array of the 791 | // models' attributes. 792 | toJSON: function(options) { 793 | return this.map(function(model) { return model.toJSON(options); }); 794 | }, 795 | 796 | // Proxy `Backbone.sync` by default. 797 | sync: function() { 798 | return Backbone.sync.apply(this, arguments); 799 | }, 800 | 801 | // Add a model, or list of models to the set. `models` may be Backbone 802 | // Models or raw JavaScript objects to be converted to Models, or any 803 | // combination of the two. 804 | add: function(models, options) { 805 | return this.set(models, _.extend({merge: false}, options, addOptions)); 806 | }, 807 | 808 | // Remove a model, or a list of models from the set. 809 | remove: function(models, options) { 810 | options = _.extend({}, options); 811 | var singular = !_.isArray(models); 812 | models = singular ? [models] : models.slice(); 813 | var removed = this._removeModels(models, options); 814 | if (!options.silent && removed.length) { 815 | options.changes = {added: [], merged: [], removed: removed}; 816 | this.trigger('update', this, options); 817 | } 818 | return singular ? removed[0] : removed; 819 | }, 820 | 821 | // Update a collection by `set`-ing a new list of models, adding new ones, 822 | // removing models that are no longer present, and merging models that 823 | // already exist in the collection, as necessary. Similar to **Model#set**, 824 | // the core operation for updating the data contained by the collection. 825 | set: function(models, options) { 826 | if (models == null) return; 827 | 828 | options = _.extend({}, setOptions, options); 829 | if (options.parse && !this._isModel(models)) { 830 | models = this.parse(models, options) || []; 831 | } 832 | 833 | var singular = !_.isArray(models); 834 | models = singular ? [models] : models.slice(); 835 | 836 | var at = options.at; 837 | if (at != null) at = +at; 838 | if (at > this.length) at = this.length; 839 | if (at < 0) at += this.length + 1; 840 | 841 | var set = []; 842 | var toAdd = []; 843 | var toMerge = []; 844 | var toRemove = []; 845 | var modelMap = {}; 846 | 847 | var add = options.add; 848 | var merge = options.merge; 849 | var remove = options.remove; 850 | 851 | var sort = false; 852 | var sortable = this.comparator && at == null && options.sort !== false; 853 | var sortAttr = _.isString(this.comparator) ? this.comparator : null; 854 | 855 | // Turn bare objects into model references, and prevent invalid models 856 | // from being added. 857 | var model, i; 858 | for (i = 0; i < models.length; i++) { 859 | model = models[i]; 860 | 861 | // If a duplicate is found, prevent it from being added and 862 | // optionally merge it into the existing model. 863 | var existing = this.get(model); 864 | if (existing) { 865 | if (merge && model !== existing) { 866 | var attrs = this._isModel(model) ? model.attributes : model; 867 | if (options.parse) attrs = existing.parse(attrs, options); 868 | existing.set(attrs, options); 869 | toMerge.push(existing); 870 | if (sortable && !sort) sort = existing.hasChanged(sortAttr); 871 | } 872 | if (!modelMap[existing.cid]) { 873 | modelMap[existing.cid] = true; 874 | set.push(existing); 875 | } 876 | models[i] = existing; 877 | 878 | // If this is a new, valid model, push it to the `toAdd` list. 879 | } else if (add) { 880 | model = models[i] = this._prepareModel(model, options); 881 | if (model) { 882 | toAdd.push(model); 883 | this._addReference(model, options); 884 | modelMap[model.cid] = true; 885 | set.push(model); 886 | } 887 | } 888 | } 889 | 890 | // Remove stale models. 891 | if (remove) { 892 | for (i = 0; i < this.length; i++) { 893 | model = this.models[i]; 894 | if (!modelMap[model.cid]) toRemove.push(model); 895 | } 896 | if (toRemove.length) this._removeModels(toRemove, options); 897 | } 898 | 899 | // See if sorting is needed, update `length` and splice in new models. 900 | var orderChanged = false; 901 | var replace = !sortable && add && remove; 902 | if (set.length && replace) { 903 | orderChanged = this.length !== set.length || _.some(this.models, function(m, index) { 904 | return m !== set[index]; 905 | }); 906 | this.models.length = 0; 907 | splice(this.models, set, 0); 908 | this.length = this.models.length; 909 | } else if (toAdd.length) { 910 | if (sortable) sort = true; 911 | splice(this.models, toAdd, at == null ? this.length : at); 912 | this.length = this.models.length; 913 | } 914 | 915 | // Silently sort the collection if appropriate. 916 | if (sort) this.sort({silent: true}); 917 | 918 | // Unless silenced, it's time to fire all appropriate add/sort/update events. 919 | if (!options.silent) { 920 | for (i = 0; i < toAdd.length; i++) { 921 | if (at != null) options.index = at + i; 922 | model = toAdd[i]; 923 | model.trigger('add', model, this, options); 924 | } 925 | if (sort || orderChanged) this.trigger('sort', this, options); 926 | if (toAdd.length || toRemove.length || toMerge.length) { 927 | options.changes = { 928 | added: toAdd, 929 | removed: toRemove, 930 | merged: toMerge 931 | }; 932 | this.trigger('update', this, options); 933 | } 934 | } 935 | 936 | // Return the added (or merged) model (or models). 937 | return singular ? models[0] : models; 938 | }, 939 | 940 | // When you have more items than you want to add or remove individually, 941 | // you can reset the entire set with a new list of models, without firing 942 | // any granular `add` or `remove` events. Fires `reset` when finished. 943 | // Useful for bulk operations and optimizations. 944 | reset: function(models, options) { 945 | options = options ? _.clone(options) : {}; 946 | for (var i = 0; i < this.models.length; i++) { 947 | this._removeReference(this.models[i], options); 948 | } 949 | options.previousModels = this.models; 950 | this._reset(); 951 | models = this.add(models, _.extend({silent: true}, options)); 952 | if (!options.silent) this.trigger('reset', this, options); 953 | return models; 954 | }, 955 | 956 | // Add a model to the end of the collection. 957 | push: function(model, options) { 958 | return this.add(model, _.extend({at: this.length}, options)); 959 | }, 960 | 961 | // Remove a model from the end of the collection. 962 | pop: function(options) { 963 | var model = this.at(this.length - 1); 964 | return this.remove(model, options); 965 | }, 966 | 967 | // Add a model to the beginning of the collection. 968 | unshift: function(model, options) { 969 | return this.add(model, _.extend({at: 0}, options)); 970 | }, 971 | 972 | // Remove a model from the beginning of the collection. 973 | shift: function(options) { 974 | var model = this.at(0); 975 | return this.remove(model, options); 976 | }, 977 | 978 | // Slice out a sub-array of models from the collection. 979 | slice: function() { 980 | return slice.apply(this.models, arguments); 981 | }, 982 | 983 | // Get a model from the set by id, cid, model object with id or cid 984 | // properties, or an attributes object that is transformed through modelId. 985 | get: function(obj) { 986 | if (obj == null) return void 0; 987 | return this._byId[obj] || 988 | this._byId[this.modelId(obj.attributes || obj)] || 989 | obj.cid && this._byId[obj.cid]; 990 | }, 991 | 992 | // Returns `true` if the model is in the collection. 993 | has: function(obj) { 994 | return this.get(obj) != null; 995 | }, 996 | 997 | // Get the model at the given index. 998 | at: function(index) { 999 | if (index < 0) index += this.length; 1000 | return this.models[index]; 1001 | }, 1002 | 1003 | // Return models with matching attributes. Useful for simple cases of 1004 | // `filter`. 1005 | where: function(attrs, first) { 1006 | return this[first ? 'find' : 'filter'](attrs); 1007 | }, 1008 | 1009 | // Return the first model with matching attributes. Useful for simple cases 1010 | // of `find`. 1011 | findWhere: function(attrs) { 1012 | return this.where(attrs, true); 1013 | }, 1014 | 1015 | // Force the collection to re-sort itself. You don't need to call this under 1016 | // normal circumstances, as the set will maintain sort order as each item 1017 | // is added. 1018 | sort: function(options) { 1019 | var comparator = this.comparator; 1020 | if (!comparator) throw new Error('Cannot sort a set without a comparator'); 1021 | options || (options = {}); 1022 | 1023 | var length = comparator.length; 1024 | if (_.isFunction(comparator)) comparator = _.bind(comparator, this); 1025 | 1026 | // Run sort based on type of `comparator`. 1027 | if (length === 1 || _.isString(comparator)) { 1028 | this.models = this.sortBy(comparator); 1029 | } else { 1030 | this.models.sort(comparator); 1031 | } 1032 | if (!options.silent) this.trigger('sort', this, options); 1033 | return this; 1034 | }, 1035 | 1036 | // Pluck an attribute from each model in the collection. 1037 | pluck: function(attr) { 1038 | return this.map(attr + ''); 1039 | }, 1040 | 1041 | // Fetch the default set of models for this collection, resetting the 1042 | // collection when they arrive. If `reset: true` is passed, the response 1043 | // data will be passed through the `reset` method instead of `set`. 1044 | fetch: function(options) { 1045 | options = _.extend({parse: true}, options); 1046 | var success = options.success; 1047 | var collection = this; 1048 | options.success = function(resp) { 1049 | var method = options.reset ? 'reset' : 'set'; 1050 | collection[method](resp, options); 1051 | if (success) success.call(options.context, collection, resp, options); 1052 | collection.trigger('sync', collection, resp, options); 1053 | }; 1054 | wrapError(this, options); 1055 | return this.sync('read', this, options); 1056 | }, 1057 | 1058 | // Create a new instance of a model in this collection. Add the model to the 1059 | // collection immediately, unless `wait: true` is passed, in which case we 1060 | // wait for the server to agree. 1061 | create: function(model, options) { 1062 | options = options ? _.clone(options) : {}; 1063 | var wait = options.wait; 1064 | model = this._prepareModel(model, options); 1065 | if (!model) return false; 1066 | if (!wait) this.add(model, options); 1067 | var collection = this; 1068 | var success = options.success; 1069 | options.success = function(m, resp, callbackOpts) { 1070 | if (wait) collection.add(m, callbackOpts); 1071 | if (success) success.call(callbackOpts.context, m, resp, callbackOpts); 1072 | }; 1073 | model.save(null, options); 1074 | return model; 1075 | }, 1076 | 1077 | // **parse** converts a response into a list of models to be added to the 1078 | // collection. The default implementation is just to pass it through. 1079 | parse: function(resp, options) { 1080 | return resp; 1081 | }, 1082 | 1083 | // Create a new collection with an identical list of models as this one. 1084 | clone: function() { 1085 | return new this.constructor(this.models, { 1086 | model: this.model, 1087 | comparator: this.comparator 1088 | }); 1089 | }, 1090 | 1091 | // Define how to uniquely identify models in the collection. 1092 | modelId: function(attrs) { 1093 | return attrs[this.model.prototype.idAttribute || 'id']; 1094 | }, 1095 | 1096 | // Private method to reset all internal state. Called when the collection 1097 | // is first initialized or reset. 1098 | _reset: function() { 1099 | this.length = 0; 1100 | this.models = []; 1101 | this._byId = {}; 1102 | }, 1103 | 1104 | // Prepare a hash of attributes (or other model) to be added to this 1105 | // collection. 1106 | _prepareModel: function(attrs, options) { 1107 | if (this._isModel(attrs)) { 1108 | if (!attrs.collection) attrs.collection = this; 1109 | return attrs; 1110 | } 1111 | options = options ? _.clone(options) : {}; 1112 | options.collection = this; 1113 | var model = new this.model(attrs, options); 1114 | if (!model.validationError) return model; 1115 | this.trigger('invalid', this, model.validationError, options); 1116 | return false; 1117 | }, 1118 | 1119 | // Internal method called by both remove and set. 1120 | _removeModels: function(models, options) { 1121 | var removed = []; 1122 | for (var i = 0; i < models.length; i++) { 1123 | var model = this.get(models[i]); 1124 | if (!model) continue; 1125 | 1126 | var index = this.indexOf(model); 1127 | this.models.splice(index, 1); 1128 | this.length--; 1129 | 1130 | // Remove references before triggering 'remove' event to prevent an 1131 | // infinite loop. #3693 1132 | delete this._byId[model.cid]; 1133 | var id = this.modelId(model.attributes); 1134 | if (id != null) delete this._byId[id]; 1135 | 1136 | if (!options.silent) { 1137 | options.index = index; 1138 | model.trigger('remove', model, this, options); 1139 | } 1140 | 1141 | removed.push(model); 1142 | this._removeReference(model, options); 1143 | } 1144 | return removed; 1145 | }, 1146 | 1147 | // Method for checking whether an object should be considered a model for 1148 | // the purposes of adding to the collection. 1149 | _isModel: function(model) { 1150 | return model instanceof Model; 1151 | }, 1152 | 1153 | // Internal method to create a model's ties to a collection. 1154 | _addReference: function(model, options) { 1155 | this._byId[model.cid] = model; 1156 | var id = this.modelId(model.attributes); 1157 | if (id != null) this._byId[id] = model; 1158 | model.on('all', this._onModelEvent, this); 1159 | }, 1160 | 1161 | // Internal method to sever a model's ties to a collection. 1162 | _removeReference: function(model, options) { 1163 | delete this._byId[model.cid]; 1164 | var id = this.modelId(model.attributes); 1165 | if (id != null) delete this._byId[id]; 1166 | if (this === model.collection) delete model.collection; 1167 | model.off('all', this._onModelEvent, this); 1168 | }, 1169 | 1170 | // Internal method called every time a model in the set fires an event. 1171 | // Sets need to update their indexes when models change ids. All other 1172 | // events simply proxy through. "add" and "remove" events that originate 1173 | // in other collections are ignored. 1174 | _onModelEvent: function(event, model, collection, options) { 1175 | if (model) { 1176 | if ((event === 'add' || event === 'remove') && collection !== this) return; 1177 | if (event === 'destroy') this.remove(model, options); 1178 | if (event === 'change') { 1179 | var prevId = this.modelId(model.previousAttributes()); 1180 | var id = this.modelId(model.attributes); 1181 | if (prevId !== id) { 1182 | if (prevId != null) delete this._byId[prevId]; 1183 | if (id != null) this._byId[id] = model; 1184 | } 1185 | } 1186 | } 1187 | this.trigger.apply(this, arguments); 1188 | } 1189 | 1190 | }); 1191 | 1192 | // Underscore methods that we want to implement on the Collection. 1193 | // 90% of the core usefulness of Backbone Collections is actually implemented 1194 | // right here: 1195 | var collectionMethods = {forEach: 3, each: 3, map: 3, collect: 3, reduce: 0, 1196 | foldl: 0, inject: 0, reduceRight: 0, foldr: 0, find: 3, detect: 3, filter: 3, 1197 | select: 3, reject: 3, every: 3, all: 3, some: 3, any: 3, include: 3, includes: 3, 1198 | contains: 3, invoke: 0, max: 3, min: 3, toArray: 1, size: 1, first: 3, 1199 | head: 3, take: 3, initial: 3, rest: 3, tail: 3, drop: 3, last: 3, 1200 | without: 0, difference: 0, indexOf: 3, shuffle: 1, lastIndexOf: 3, 1201 | isEmpty: 1, chain: 1, sample: 3, partition: 3, groupBy: 3, countBy: 3, 1202 | sortBy: 3, indexBy: 3, findIndex: 3, findLastIndex: 3}; 1203 | 1204 | // Mix in each Underscore method as a proxy to `Collection#models`. 1205 | addUnderscoreMethods(Collection, collectionMethods, 'models'); 1206 | 1207 | // Backbone.View 1208 | // ------------- 1209 | 1210 | // Backbone Views are almost more convention than they are actual code. A View 1211 | // is simply a JavaScript object that represents a logical chunk of UI in the 1212 | // DOM. This might be a single item, an entire list, a sidebar or panel, or 1213 | // even the surrounding frame which wraps your whole app. Defining a chunk of 1214 | // UI as a **View** allows you to define your DOM events declaratively, without 1215 | // having to worry about render order ... and makes it easy for the view to 1216 | // react to specific changes in the state of your models. 1217 | 1218 | // Creating a Backbone.View creates its initial element outside of the DOM, 1219 | // if an existing element is not provided... 1220 | var View = Backbone.View = function(options) { 1221 | this.cid = _.uniqueId('view'); 1222 | _.extend(this, _.pick(options, viewOptions)); 1223 | this._ensureElement(); 1224 | this.initialize.apply(this, arguments); 1225 | }; 1226 | 1227 | // Cached regex to split keys for `delegate`. 1228 | var delegateEventSplitter = /^(\S+)\s*(.*)$/; 1229 | 1230 | // List of view options to be set as properties. 1231 | var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName', 'events']; 1232 | 1233 | // Set up all inheritable **Backbone.View** properties and methods. 1234 | _.extend(View.prototype, Events, { 1235 | 1236 | // The default `tagName` of a View's element is `"div"`. 1237 | tagName: 'div', 1238 | 1239 | // jQuery delegate for element lookup, scoped to DOM elements within the 1240 | // current view. This should be preferred to global lookups where possible. 1241 | $: function(selector) { 1242 | return this.$el.find(selector); 1243 | }, 1244 | 1245 | // Initialize is an empty function by default. Override it with your own 1246 | // initialization logic. 1247 | initialize: function(){}, 1248 | 1249 | // **render** is the core function that your view should override, in order 1250 | // to populate its element (`this.el`), with the appropriate HTML. The 1251 | // convention is for **render** to always return `this`. 1252 | render: function() { 1253 | return this; 1254 | }, 1255 | 1256 | // Remove this view by taking the element out of the DOM, and removing any 1257 | // applicable Backbone.Events listeners. 1258 | remove: function() { 1259 | this._removeElement(); 1260 | this.stopListening(); 1261 | return this; 1262 | }, 1263 | 1264 | // Remove this view's element from the document and all event listeners 1265 | // attached to it. Exposed for subclasses using an alternative DOM 1266 | // manipulation API. 1267 | _removeElement: function() { 1268 | this.$el.remove(); 1269 | }, 1270 | 1271 | // Change the view's element (`this.el` property) and re-delegate the 1272 | // view's events on the new element. 1273 | setElement: function(element) { 1274 | this.undelegateEvents(); 1275 | this._setElement(element); 1276 | this.delegateEvents(); 1277 | return this; 1278 | }, 1279 | 1280 | // Creates the `this.el` and `this.$el` references for this view using the 1281 | // given `el`. `el` can be a CSS selector or an HTML string, a jQuery 1282 | // context or an element. Subclasses can override this to utilize an 1283 | // alternative DOM manipulation API and are only required to set the 1284 | // `this.el` property. 1285 | _setElement: function(el) { 1286 | this.$el = el instanceof Backbone.$ ? el : Backbone.$(el); 1287 | this.el = this.$el[0]; 1288 | }, 1289 | 1290 | // Set callbacks, where `this.events` is a hash of 1291 | // 1292 | // *{"event selector": "callback"}* 1293 | // 1294 | // { 1295 | // 'mousedown .title': 'edit', 1296 | // 'click .button': 'save', 1297 | // 'click .open': function(e) { ... } 1298 | // } 1299 | // 1300 | // pairs. Callbacks will be bound to the view, with `this` set properly. 1301 | // Uses event delegation for efficiency. 1302 | // Omitting the selector binds the event to `this.el`. 1303 | delegateEvents: function(events) { 1304 | events || (events = _.result(this, 'events')); 1305 | if (!events) return this; 1306 | this.undelegateEvents(); 1307 | for (var key in events) { 1308 | var method = events[key]; 1309 | if (!_.isFunction(method)) method = this[method]; 1310 | if (!method) continue; 1311 | var match = key.match(delegateEventSplitter); 1312 | this.delegate(match[1], match[2], _.bind(method, this)); 1313 | } 1314 | return this; 1315 | }, 1316 | 1317 | // Add a single event listener to the view's element (or a child element 1318 | // using `selector`). This only works for delegate-able events: not `focus`, 1319 | // `blur`, and not `change`, `submit`, and `reset` in Internet Explorer. 1320 | delegate: function(eventName, selector, listener) { 1321 | this.$el.on(eventName + '.delegateEvents' + this.cid, selector, listener); 1322 | return this; 1323 | }, 1324 | 1325 | // Clears all callbacks previously bound to the view by `delegateEvents`. 1326 | // You usually don't need to use this, but may wish to if you have multiple 1327 | // Backbone views attached to the same DOM element. 1328 | undelegateEvents: function() { 1329 | if (this.$el) this.$el.off('.delegateEvents' + this.cid); 1330 | return this; 1331 | }, 1332 | 1333 | // A finer-grained `undelegateEvents` for removing a single delegated event. 1334 | // `selector` and `listener` are both optional. 1335 | undelegate: function(eventName, selector, listener) { 1336 | this.$el.off(eventName + '.delegateEvents' + this.cid, selector, listener); 1337 | return this; 1338 | }, 1339 | 1340 | // Produces a DOM element to be assigned to your view. Exposed for 1341 | // subclasses using an alternative DOM manipulation API. 1342 | _createElement: function(tagName) { 1343 | return document.createElement(tagName); 1344 | }, 1345 | 1346 | // Ensure that the View has a DOM element to render into. 1347 | // If `this.el` is a string, pass it through `$()`, take the first 1348 | // matching element, and re-assign it to `el`. Otherwise, create 1349 | // an element from the `id`, `className` and `tagName` properties. 1350 | _ensureElement: function() { 1351 | if (!this.el) { 1352 | var attrs = _.extend({}, _.result(this, 'attributes')); 1353 | if (this.id) attrs.id = _.result(this, 'id'); 1354 | if (this.className) attrs['class'] = _.result(this, 'className'); 1355 | this.setElement(this._createElement(_.result(this, 'tagName'))); 1356 | this._setAttributes(attrs); 1357 | } else { 1358 | this.setElement(_.result(this, 'el')); 1359 | } 1360 | }, 1361 | 1362 | // Set attributes from a hash on this view's element. Exposed for 1363 | // subclasses using an alternative DOM manipulation API. 1364 | _setAttributes: function(attributes) { 1365 | this.$el.attr(attributes); 1366 | } 1367 | 1368 | }); 1369 | 1370 | // Backbone.sync 1371 | // ------------- 1372 | 1373 | // Override this function to change the manner in which Backbone persists 1374 | // models to the server. You will be passed the type of request, and the 1375 | // model in question. By default, makes a RESTful Ajax request 1376 | // to the model's `url()`. Some possible customizations could be: 1377 | // 1378 | // * Use `setTimeout` to batch rapid-fire updates into a single request. 1379 | // * Send up the models as XML instead of JSON. 1380 | // * Persist models via WebSockets instead of Ajax. 1381 | // 1382 | // Turn on `Backbone.emulateHTTP` in order to send `PUT` and `DELETE` requests 1383 | // as `POST`, with a `_method` parameter containing the true HTTP method, 1384 | // as well as all requests with the body as `application/x-www-form-urlencoded` 1385 | // instead of `application/json` with the model in a param named `model`. 1386 | // Useful when interfacing with server-side languages like **PHP** that make 1387 | // it difficult to read the body of `PUT` requests. 1388 | Backbone.sync = function(method, model, options) { 1389 | var type = methodMap[method]; 1390 | 1391 | // Default options, unless specified. 1392 | _.defaults(options || (options = {}), { 1393 | emulateHTTP: Backbone.emulateHTTP, 1394 | emulateJSON: Backbone.emulateJSON 1395 | }); 1396 | 1397 | // Default JSON-request options. 1398 | var params = {type: type, dataType: 'json'}; 1399 | 1400 | // Ensure that we have a URL. 1401 | if (!options.url) { 1402 | params.url = _.result(model, 'url') || urlError(); 1403 | } 1404 | 1405 | // Ensure that we have the appropriate request data. 1406 | if (options.data == null && model && (method === 'create' || method === 'update' || method === 'patch')) { 1407 | params.contentType = 'application/json'; 1408 | params.data = JSON.stringify(options.attrs || model.toJSON(options)); 1409 | } 1410 | 1411 | // For older servers, emulate JSON by encoding the request into an HTML-form. 1412 | if (options.emulateJSON) { 1413 | params.contentType = 'application/x-www-form-urlencoded'; 1414 | params.data = params.data ? {model: params.data} : {}; 1415 | } 1416 | 1417 | // For older servers, emulate HTTP by mimicking the HTTP method with `_method` 1418 | // And an `X-HTTP-Method-Override` header. 1419 | if (options.emulateHTTP && (type === 'PUT' || type === 'DELETE' || type === 'PATCH')) { 1420 | params.type = 'POST'; 1421 | if (options.emulateJSON) params.data._method = type; 1422 | var beforeSend = options.beforeSend; 1423 | options.beforeSend = function(xhr) { 1424 | xhr.setRequestHeader('X-HTTP-Method-Override', type); 1425 | if (beforeSend) return beforeSend.apply(this, arguments); 1426 | }; 1427 | } 1428 | 1429 | // Don't process data on a non-GET request. 1430 | if (params.type !== 'GET' && !options.emulateJSON) { 1431 | params.processData = false; 1432 | } 1433 | 1434 | // Pass along `textStatus` and `errorThrown` from jQuery. 1435 | var error = options.error; 1436 | options.error = function(xhr, textStatus, errorThrown) { 1437 | options.textStatus = textStatus; 1438 | options.errorThrown = errorThrown; 1439 | if (error) error.call(options.context, xhr, textStatus, errorThrown); 1440 | }; 1441 | 1442 | // Make the request, allowing the user to override any Ajax options. 1443 | var xhr = options.xhr = Backbone.ajax(_.extend(params, options)); 1444 | model.trigger('request', model, xhr, options); 1445 | return xhr; 1446 | }; 1447 | 1448 | // Map from CRUD to HTTP for our default `Backbone.sync` implementation. 1449 | var methodMap = { 1450 | 'create': 'POST', 1451 | 'update': 'PUT', 1452 | 'patch': 'PATCH', 1453 | 'delete': 'DELETE', 1454 | 'read': 'GET' 1455 | }; 1456 | 1457 | // Set the default implementation of `Backbone.ajax` to proxy through to `$`. 1458 | // Override this if you'd like to use a different library. 1459 | Backbone.ajax = function() { 1460 | return Backbone.$.ajax.apply(Backbone.$, arguments); 1461 | }; 1462 | 1463 | // Backbone.Router 1464 | // --------------- 1465 | 1466 | // Routers map faux-URLs to actions, and fire events when routes are 1467 | // matched. Creating a new one sets its `routes` hash, if not set statically. 1468 | var Router = Backbone.Router = function(options) { 1469 | options || (options = {}); 1470 | if (options.routes) this.routes = options.routes; 1471 | this._bindRoutes(); 1472 | this.initialize.apply(this, arguments); 1473 | }; 1474 | 1475 | // Cached regular expressions for matching named param parts and splatted 1476 | // parts of route strings. 1477 | var optionalParam = /\((.*?)\)/g; 1478 | var namedParam = /(\(\?)?:\w+/g; 1479 | var splatParam = /\*\w+/g; 1480 | var escapeRegExp = /[\-{}\[\]+?.,\\\^$|#\s]/g; 1481 | 1482 | // Set up all inheritable **Backbone.Router** properties and methods. 1483 | _.extend(Router.prototype, Events, { 1484 | 1485 | // Initialize is an empty function by default. Override it with your own 1486 | // initialization logic. 1487 | initialize: function(){}, 1488 | 1489 | // Manually bind a single named route to a callback. For example: 1490 | // 1491 | // this.route('search/:query/p:num', 'search', function(query, num) { 1492 | // ... 1493 | // }); 1494 | // 1495 | route: function(route, name, callback) { 1496 | if (!_.isRegExp(route)) route = this._routeToRegExp(route); 1497 | if (_.isFunction(name)) { 1498 | callback = name; 1499 | name = ''; 1500 | } 1501 | if (!callback) callback = this[name]; 1502 | var router = this; 1503 | Backbone.history.route(route, function(fragment) { 1504 | var args = router._extractParameters(route, fragment); 1505 | if (router.execute(callback, args, name) !== false) { 1506 | router.trigger.apply(router, ['route:' + name].concat(args)); 1507 | router.trigger('route', name, args); 1508 | Backbone.history.trigger('route', router, name, args); 1509 | } 1510 | }); 1511 | return this; 1512 | }, 1513 | 1514 | // Execute a route handler with the provided parameters. This is an 1515 | // excellent place to do pre-route setup or post-route cleanup. 1516 | execute: function(callback, args, name) { 1517 | if (callback) callback.apply(this, args); 1518 | }, 1519 | 1520 | // Simple proxy to `Backbone.history` to save a fragment into the history. 1521 | navigate: function(fragment, options) { 1522 | Backbone.history.navigate(fragment, options); 1523 | return this; 1524 | }, 1525 | 1526 | // Bind all defined routes to `Backbone.history`. We have to reverse the 1527 | // order of the routes here to support behavior where the most general 1528 | // routes can be defined at the bottom of the route map. 1529 | _bindRoutes: function() { 1530 | if (!this.routes) return; 1531 | this.routes = _.result(this, 'routes'); 1532 | var route, routes = _.keys(this.routes); 1533 | while ((route = routes.pop()) != null) { 1534 | this.route(route, this.routes[route]); 1535 | } 1536 | }, 1537 | 1538 | // Convert a route string into a regular expression, suitable for matching 1539 | // against the current location hash. 1540 | _routeToRegExp: function(route) { 1541 | route = route.replace(escapeRegExp, '\\$&') 1542 | .replace(optionalParam, '(?:$1)?') 1543 | .replace(namedParam, function(match, optional) { 1544 | return optional ? match : '([^/?]+)'; 1545 | }) 1546 | .replace(splatParam, '([^?]*?)'); 1547 | return new RegExp('^' + route + '(?:\\?([\\s\\S]*))?$'); 1548 | }, 1549 | 1550 | // Given a route, and a URL fragment that it matches, return the array of 1551 | // extracted decoded parameters. Empty or unmatched parameters will be 1552 | // treated as `null` to normalize cross-browser behavior. 1553 | _extractParameters: function(route, fragment) { 1554 | var params = route.exec(fragment).slice(1); 1555 | return _.map(params, function(param, i) { 1556 | // Don't decode the search params. 1557 | if (i === params.length - 1) return param || null; 1558 | return param ? decodeURIComponent(param) : null; 1559 | }); 1560 | } 1561 | 1562 | }); 1563 | 1564 | // Backbone.History 1565 | // ---------------- 1566 | 1567 | // Handles cross-browser history management, based on either 1568 | // [pushState](http://diveintohtml5.info/history.html) and real URLs, or 1569 | // [onhashchange](https://developer.mozilla.org/en-US/docs/DOM/window.onhashchange) 1570 | // and URL fragments. If the browser supports neither (old IE, natch), 1571 | // falls back to polling. 1572 | var History = Backbone.History = function() { 1573 | this.handlers = []; 1574 | this.checkUrl = _.bind(this.checkUrl, this); 1575 | 1576 | // Ensure that `History` can be used outside of the browser. 1577 | if (typeof window !== 'undefined') { 1578 | this.location = window.location; 1579 | this.history = window.history; 1580 | } 1581 | }; 1582 | 1583 | // Cached regex for stripping a leading hash/slash and trailing space. 1584 | var routeStripper = /^[#\/]|\s+$/g; 1585 | 1586 | // Cached regex for stripping leading and trailing slashes. 1587 | var rootStripper = /^\/+|\/+$/g; 1588 | 1589 | // Cached regex for stripping urls of hash. 1590 | var pathStripper = /#.*$/; 1591 | 1592 | // Has the history handling already been started? 1593 | History.started = false; 1594 | 1595 | // Set up all inheritable **Backbone.History** properties and methods. 1596 | _.extend(History.prototype, Events, { 1597 | 1598 | // The default interval to poll for hash changes, if necessary, is 1599 | // twenty times a second. 1600 | interval: 50, 1601 | 1602 | // Are we at the app root? 1603 | atRoot: function() { 1604 | var path = this.location.pathname.replace(/[^\/]$/, '$&/'); 1605 | return path === this.root && !this.getSearch(); 1606 | }, 1607 | 1608 | // Does the pathname match the root? 1609 | matchRoot: function() { 1610 | var path = this.decodeFragment(this.location.pathname); 1611 | var rootPath = path.slice(0, this.root.length - 1) + '/'; 1612 | return rootPath === this.root; 1613 | }, 1614 | 1615 | // Unicode characters in `location.pathname` are percent encoded so they're 1616 | // decoded for comparison. `%25` should not be decoded since it may be part 1617 | // of an encoded parameter. 1618 | decodeFragment: function(fragment) { 1619 | return decodeURI(fragment.replace(/%25/g, '%2525')); 1620 | }, 1621 | 1622 | // In IE6, the hash fragment and search params are incorrect if the 1623 | // fragment contains `?`. 1624 | getSearch: function() { 1625 | var match = this.location.href.replace(/#.*/, '').match(/\?.+/); 1626 | return match ? match[0] : ''; 1627 | }, 1628 | 1629 | // Gets the true hash value. Cannot use location.hash directly due to bug 1630 | // in Firefox where location.hash will always be decoded. 1631 | getHash: function(window) { 1632 | var match = (window || this).location.href.match(/#(.*)$/); 1633 | return match ? match[1] : ''; 1634 | }, 1635 | 1636 | // Get the pathname and search params, without the root. 1637 | getPath: function() { 1638 | var path = this.decodeFragment( 1639 | this.location.pathname + this.getSearch() 1640 | ).slice(this.root.length - 1); 1641 | return path.charAt(0) === '/' ? path.slice(1) : path; 1642 | }, 1643 | 1644 | // Get the cross-browser normalized URL fragment from the path or hash. 1645 | getFragment: function(fragment) { 1646 | if (fragment == null) { 1647 | if (this._usePushState || !this._wantsHashChange) { 1648 | fragment = this.getPath(); 1649 | } else { 1650 | fragment = this.getHash(); 1651 | } 1652 | } 1653 | return fragment.replace(routeStripper, ''); 1654 | }, 1655 | 1656 | // Start the hash change handling, returning `true` if the current URL matches 1657 | // an existing route, and `false` otherwise. 1658 | start: function(options) { 1659 | if (History.started) throw new Error('Backbone.history has already been started'); 1660 | History.started = true; 1661 | 1662 | // Figure out the initial configuration. Do we need an iframe? 1663 | // Is pushState desired ... is it available? 1664 | this.options = _.extend({root: '/'}, this.options, options); 1665 | this.root = this.options.root; 1666 | this._wantsHashChange = this.options.hashChange !== false; 1667 | this._hasHashChange = 'onhashchange' in window && (document.documentMode === void 0 || document.documentMode > 7); 1668 | this._useHashChange = this._wantsHashChange && this._hasHashChange; 1669 | this._wantsPushState = !!this.options.pushState; 1670 | this._hasPushState = !!(this.history && this.history.pushState); 1671 | this._usePushState = this._wantsPushState && this._hasPushState; 1672 | this.fragment = this.getFragment(); 1673 | 1674 | // Normalize root to always include a leading and trailing slash. 1675 | this.root = ('/' + this.root + '/').replace(rootStripper, '/'); 1676 | 1677 | // Transition from hashChange to pushState or vice versa if both are 1678 | // requested. 1679 | if (this._wantsHashChange && this._wantsPushState) { 1680 | 1681 | // If we've started off with a route from a `pushState`-enabled 1682 | // browser, but we're currently in a browser that doesn't support it... 1683 | if (!this._hasPushState && !this.atRoot()) { 1684 | var rootPath = this.root.slice(0, -1) || '/'; 1685 | this.location.replace(rootPath + '#' + this.getPath()); 1686 | // Return immediately as browser will do redirect to new url 1687 | return true; 1688 | 1689 | // Or if we've started out with a hash-based route, but we're currently 1690 | // in a browser where it could be `pushState`-based instead... 1691 | } else if (this._hasPushState && this.atRoot()) { 1692 | this.navigate(this.getHash(), {replace: true}); 1693 | } 1694 | 1695 | } 1696 | 1697 | // Proxy an iframe to handle location events if the browser doesn't 1698 | // support the `hashchange` event, HTML5 history, or the user wants 1699 | // `hashChange` but not `pushState`. 1700 | if (!this._hasHashChange && this._wantsHashChange && !this._usePushState) { 1701 | this.iframe = document.createElement('iframe'); 1702 | this.iframe.src = 'javascript:0'; 1703 | this.iframe.style.display = 'none'; 1704 | this.iframe.tabIndex = -1; 1705 | var body = document.body; 1706 | // Using `appendChild` will throw on IE < 9 if the document is not ready. 1707 | var iWindow = body.insertBefore(this.iframe, body.firstChild).contentWindow; 1708 | iWindow.document.open(); 1709 | iWindow.document.close(); 1710 | iWindow.location.hash = '#' + this.fragment; 1711 | } 1712 | 1713 | // Add a cross-platform `addEventListener` shim for older browsers. 1714 | var addEventListener = window.addEventListener || function(eventName, listener) { 1715 | return attachEvent('on' + eventName, listener); 1716 | }; 1717 | 1718 | // Depending on whether we're using pushState or hashes, and whether 1719 | // 'onhashchange' is supported, determine how we check the URL state. 1720 | if (this._usePushState) { 1721 | addEventListener('popstate', this.checkUrl, false); 1722 | } else if (this._useHashChange && !this.iframe) { 1723 | addEventListener('hashchange', this.checkUrl, false); 1724 | } else if (this._wantsHashChange) { 1725 | this._checkUrlInterval = setInterval(this.checkUrl, this.interval); 1726 | } 1727 | 1728 | if (!this.options.silent) return this.loadUrl(); 1729 | }, 1730 | 1731 | // Disable Backbone.history, perhaps temporarily. Not useful in a real app, 1732 | // but possibly useful for unit testing Routers. 1733 | stop: function() { 1734 | // Add a cross-platform `removeEventListener` shim for older browsers. 1735 | var removeEventListener = window.removeEventListener || function(eventName, listener) { 1736 | return detachEvent('on' + eventName, listener); 1737 | }; 1738 | 1739 | // Remove window listeners. 1740 | if (this._usePushState) { 1741 | removeEventListener('popstate', this.checkUrl, false); 1742 | } else if (this._useHashChange && !this.iframe) { 1743 | removeEventListener('hashchange', this.checkUrl, false); 1744 | } 1745 | 1746 | // Clean up the iframe if necessary. 1747 | if (this.iframe) { 1748 | document.body.removeChild(this.iframe); 1749 | this.iframe = null; 1750 | } 1751 | 1752 | // Some environments will throw when clearing an undefined interval. 1753 | if (this._checkUrlInterval) clearInterval(this._checkUrlInterval); 1754 | History.started = false; 1755 | }, 1756 | 1757 | // Add a route to be tested when the fragment changes. Routes added later 1758 | // may override previous routes. 1759 | route: function(route, callback) { 1760 | this.handlers.unshift({route: route, callback: callback}); 1761 | }, 1762 | 1763 | // Checks the current URL to see if it has changed, and if it has, 1764 | // calls `loadUrl`, normalizing across the hidden iframe. 1765 | checkUrl: function(e) { 1766 | var current = this.getFragment(); 1767 | 1768 | // If the user pressed the back button, the iframe's hash will have 1769 | // changed and we should use that for comparison. 1770 | if (current === this.fragment && this.iframe) { 1771 | current = this.getHash(this.iframe.contentWindow); 1772 | } 1773 | 1774 | if (current === this.fragment) return false; 1775 | if (this.iframe) this.navigate(current); 1776 | this.loadUrl(); 1777 | }, 1778 | 1779 | // Attempt to load the current URL fragment. If a route succeeds with a 1780 | // match, returns `true`. If no defined routes matches the fragment, 1781 | // returns `false`. 1782 | loadUrl: function(fragment) { 1783 | // If the root doesn't match, no routes can match either. 1784 | if (!this.matchRoot()) return false; 1785 | fragment = this.fragment = this.getFragment(fragment); 1786 | return _.some(this.handlers, function(handler) { 1787 | if (handler.route.test(fragment)) { 1788 | handler.callback(fragment); 1789 | return true; 1790 | } 1791 | }); 1792 | }, 1793 | 1794 | // Save a fragment into the hash history, or replace the URL state if the 1795 | // 'replace' option is passed. You are responsible for properly URL-encoding 1796 | // the fragment in advance. 1797 | // 1798 | // The options object can contain `trigger: true` if you wish to have the 1799 | // route callback be fired (not usually desirable), or `replace: true`, if 1800 | // you wish to modify the current URL without adding an entry to the history. 1801 | navigate: function(fragment, options) { 1802 | if (!History.started) return false; 1803 | if (!options || options === true) options = {trigger: !!options}; 1804 | 1805 | // Normalize the fragment. 1806 | fragment = this.getFragment(fragment || ''); 1807 | 1808 | // Don't include a trailing slash on the root. 1809 | var rootPath = this.root; 1810 | if (fragment === '' || fragment.charAt(0) === '?') { 1811 | rootPath = rootPath.slice(0, -1) || '/'; 1812 | } 1813 | var url = rootPath + fragment; 1814 | 1815 | // Strip the hash and decode for matching. 1816 | fragment = this.decodeFragment(fragment.replace(pathStripper, '')); 1817 | 1818 | if (this.fragment === fragment) return; 1819 | this.fragment = fragment; 1820 | 1821 | // If pushState is available, we use it to set the fragment as a real URL. 1822 | if (this._usePushState) { 1823 | this.history[options.replace ? 'replaceState' : 'pushState']({}, document.title, url); 1824 | 1825 | // If hash changes haven't been explicitly disabled, update the hash 1826 | // fragment to store history. 1827 | } else if (this._wantsHashChange) { 1828 | this._updateHash(this.location, fragment, options.replace); 1829 | if (this.iframe && fragment !== this.getHash(this.iframe.contentWindow)) { 1830 | var iWindow = this.iframe.contentWindow; 1831 | 1832 | // Opening and closing the iframe tricks IE7 and earlier to push a 1833 | // history entry on hash-tag change. When replace is true, we don't 1834 | // want this. 1835 | if (!options.replace) { 1836 | iWindow.document.open(); 1837 | iWindow.document.close(); 1838 | } 1839 | 1840 | this._updateHash(iWindow.location, fragment, options.replace); 1841 | } 1842 | 1843 | // If you've told us that you explicitly don't want fallback hashchange- 1844 | // based history, then `navigate` becomes a page refresh. 1845 | } else { 1846 | return this.location.assign(url); 1847 | } 1848 | if (options.trigger) return this.loadUrl(fragment); 1849 | }, 1850 | 1851 | // Update the hash location, either replacing the current entry, or adding 1852 | // a new one to the browser history. 1853 | _updateHash: function(location, fragment, replace) { 1854 | if (replace) { 1855 | var href = location.href.replace(/(javascript:|#).*$/, ''); 1856 | location.replace(href + '#' + fragment); 1857 | } else { 1858 | // Some browsers require that `hash` contains a leading #. 1859 | location.hash = '#' + fragment; 1860 | } 1861 | } 1862 | 1863 | }); 1864 | 1865 | // Create the default Backbone.history. 1866 | Backbone.history = new History; 1867 | 1868 | // Helpers 1869 | // ------- 1870 | 1871 | // Helper function to correctly set up the prototype chain for subclasses. 1872 | // Similar to `goog.inherits`, but uses a hash of prototype properties and 1873 | // class properties to be extended. 1874 | var extend = function(protoProps, staticProps) { 1875 | var parent = this; 1876 | var child; 1877 | 1878 | // The constructor function for the new subclass is either defined by you 1879 | // (the "constructor" property in your `extend` definition), or defaulted 1880 | // by us to simply call the parent constructor. 1881 | if (protoProps && _.has(protoProps, 'constructor')) { 1882 | child = protoProps.constructor; 1883 | } else { 1884 | child = function(){ return parent.apply(this, arguments); }; 1885 | } 1886 | 1887 | // Add static properties to the constructor function, if supplied. 1888 | _.extend(child, parent, staticProps); 1889 | 1890 | // Set the prototype chain to inherit from `parent`, without calling 1891 | // `parent`'s constructor function and add the prototype properties. 1892 | child.prototype = _.create(parent.prototype, protoProps); 1893 | child.prototype.constructor = child; 1894 | 1895 | // Set a convenience property in case the parent's prototype is needed 1896 | // later. 1897 | child.__super__ = parent.prototype; 1898 | 1899 | return child; 1900 | }; 1901 | 1902 | // Set up inheritance for the model, collection, router, view and history. 1903 | Model.extend = Collection.extend = Router.extend = View.extend = History.extend = extend; 1904 | 1905 | // Throw an error when a URL is needed, and none is supplied. 1906 | var urlError = function() { 1907 | throw new Error('A "url" property or function must be specified'); 1908 | }; 1909 | 1910 | // Wrap an optional error callback with a fallback error event. 1911 | var wrapError = function(model, options) { 1912 | var error = options.error; 1913 | options.error = function(resp) { 1914 | if (error) error.call(options.context, model, resp, options); 1915 | model.trigger('error', model, resp, options); 1916 | }; 1917 | }; 1918 | 1919 | return Backbone; 1920 | }); 1921 | -------------------------------------------------------------------------------- /fixtures/es6/import.js: -------------------------------------------------------------------------------- 1 | import("/file/1.js"); 2 | import("/file/2.js"); 3 | import("/file/3.js"); 4 | import("/file/4.js"); 5 | import("/file/5.js"); 6 | import("/file/6.js"); 7 | import("/file/7.js"); 8 | import("/file/8.js"); 9 | import("/file/9.js"); 10 | import("/file/10.js"); 11 | import("/file/11.js"); 12 | import("/file/12.js"); 13 | import("/file/13.js"); 14 | import("/file/14.js"); 15 | import("/file/15.js"); 16 | import("/file/16.js"); 17 | import("/file/17.js"); 18 | import("/file/18.js"); 19 | import("/file/19.js"); 20 | import("/file/20.js"); 21 | import("/file/21.js"); 22 | import("/file/22.js"); 23 | import("/file/23.js"); 24 | import("/file/24.js"); 25 | import("/file/25.js"); 26 | import("/file/26.js"); 27 | import("/file/27.js"); 28 | import("/file/28.js"); 29 | import("/file/29.js"); 30 | import("/file/30.js"); 31 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const Table = require("cli-table"); 3 | const Benchmark = require("benchmark"); 4 | const { parsers, files, benchmarkOptions } = require("./config"); 5 | const { test } = require("./util"); 6 | 7 | console.log(`Node: ${process.version}`); 8 | 9 | const head = ["fixture"]; 10 | for (let i in parsers) { 11 | head.push(i); 12 | } 13 | 14 | const table = new Table({ 15 | head, 16 | style: { 17 | head: ["bold"] 18 | } 19 | }); 20 | 21 | if (!global.gc) { 22 | console.error( 23 | "Garbage collection unavailable. Pass --expose-gc " + 24 | "when launching node to enable forced garbage collection." 25 | ); 26 | process.exit(); 27 | } 28 | 29 | files.forEach(file => { 30 | const code = fs.readFileSync(file, "utf-8"); 31 | const suite = new Benchmark.Suite(file.replace(/\.\/fixtures\//, ""), benchmarkOptions); 32 | for (let i in parsers) { 33 | const { parse, options } = parsers[i]; 34 | 35 | // warmup 36 | test(parse, options, code, 5); 37 | global.gc(); 38 | suite.add(i, () => { 39 | parse(code, options); 40 | }); 41 | } 42 | const result = [suite.name]; 43 | suite.on("cycle", function(event) { 44 | { 45 | // separate scope so we can cleanup all this afterwards 46 | const bench = event.target; 47 | const factor = bench.hz < 100 ? 100 : 1; 48 | const timeMs = bench.stats.mean * 1000; 49 | const time = (timeMs < 10)? `${Math.round(timeMs*1000)/1000}ms` : `${Math.round(timeMs)}ms`; 50 | const msg = `${Math.round(bench.hz * factor) / 51 | factor} ops/sec ±${Math.round(bench.stats.rme * 100) / 52 | 100}% (${time})`; 53 | result.push(msg); 54 | } 55 | global.gc(); 56 | }); 57 | 58 | console.log(`Running benchmark for ${suite.name} ...`); 59 | global.gc(); 60 | suite.run({ async: false }); 61 | global.gc(); // gc is disabled so ensure we run it 62 | table.push(result); 63 | }); 64 | global.gc(); // gc is disabled so ensure we run it 65 | console.log(table.toString()); 66 | -------------------------------------------------------------------------------- /mem.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const Table = require("cli-table"); 3 | const { parsers, files } = require("./config"); 4 | const { test } = require("./util"); 5 | 6 | /* START CONFIG */ 7 | const iterations = 5; 8 | /* END CONFIG */ 9 | 10 | console.log(`Node: ${process.version}`); 11 | 12 | const head = ["fixture"]; 13 | for (let i in parsers) { 14 | head.push(`${i} x${iterations}`); 15 | } 16 | 17 | const table = new Table({ 18 | head, 19 | style: { 20 | head: ["bold"] 21 | } 22 | }); 23 | 24 | if (!global.gc) { 25 | console.error( 26 | "Garbage collection unavailable. Pass --expose-gc " + 27 | "when launching node to enable forced garbage collection." 28 | ); 29 | process.exit(); 30 | } 31 | 32 | files.forEach(file => { 33 | const name = file.replace(/\.\/fixtures\//, ""); 34 | console.log(`Running benchmark for ${name} ...`); 35 | const code = fs.readFileSync(file, "utf-8"); 36 | const result = [name]; 37 | for (let i in parsers) { 38 | const { parse, options } = parsers[i]; 39 | 40 | // warmup 41 | test(parse, options, code, 1); 42 | global.gc(); 43 | } 44 | for (let i in parsers) { 45 | const { parse, options } = parsers[i]; 46 | 47 | global.gc(); 48 | let oldSize = process.memoryUsage(); 49 | 50 | test(parse, options, code, iterations); 51 | 52 | const memory = process.memoryUsage(); 53 | const heapUsed = memory.heapUsed - oldSize.heapUsed; 54 | const msg = `heap: ${Math.round((heapUsed / 1024 / 1024) * 100) / 100} MiB`; 55 | result.push(msg); 56 | global.gc(); 57 | } 58 | table.push(result); 59 | }); 60 | 61 | console.log(table.toString()); 62 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "babylon_performance", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "bootstrap": "git clone git@github.com:babel/babylon.git ../babylon", 8 | "cpu-prof": "node --cpu-prof --cpu-prof-dir=./cpuprofile --expose-gc --max-old-space-size=2000 --max-semi-space-size=1500 --predictable index.js", 9 | "test": "node --expose-gc --max-old-space-size=2000 --max-semi-space-size=1500 --predictable index.js", 10 | "print-bytecode": "node --expose-gc --max-old-space-size=2000 --max-semi-space-size=1500 --predictable --print-bytecode parse.js > parse.bytecode.txt", 11 | "print-code": "node --expose-gc --max-old-space-size=2000 --max-semi-space-size=1500 --predictable --print-opt-code --code-comments --redirect-code-traces --redirect-code-traces-to=./parse.asm parse.js", 12 | "memory": "node --expose-gc --max-old-space-size=2000 --max-semi-space-size=1500 --predictable mem.js" 13 | }, 14 | "author": "", 15 | "license": "MIT", 16 | "dependencies": { 17 | "@babel/parser": "^7.6.0", 18 | "acorn": "^7.1.1", 19 | "cli-table": "^0.3.1", 20 | "esprima": "^4.0.1", 21 | "meriyah": "^1.6.15" 22 | }, 23 | "devDependencies": { 24 | "benchmark": "^2.1.4", 25 | "microtime": "^3.0.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /parse.js: -------------------------------------------------------------------------------- 1 | const { parsers, files } = require("./config"); 2 | const fs = require("fs"); 3 | const parseRound = 5; 4 | 5 | files.forEach(file => { 6 | const code = fs.readFileSync(file, "utf-8"); 7 | for (let i in parsers) { 8 | const { parse, options } = parsers[i]; 9 | for (let j = 0; j < parseRound; j++) { 10 | parse(code, options); 11 | } 12 | } 13 | }); 14 | -------------------------------------------------------------------------------- /util.js: -------------------------------------------------------------------------------- 1 | exports.test = function test(parse, options, input, iterations) { 2 | for (let i = 0; i < iterations; i++) { 3 | parse(input, options); 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@babel/parser@^7.6.0": 6 | version "7.6.0" 7 | resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.6.0.tgz#3e05d0647432a8326cb28d0de03895ae5a57f39b" 8 | integrity sha512-+o2q111WEx4srBs7L9eJmcwi655eD8sXniLqMB93TBK9GrNzGrxDWSjiqz2hLU0Ha8MTXFIP0yd9fNdP+m43ZQ== 9 | 10 | acorn@^7.1.1: 11 | version "7.1.1" 12 | resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.1.1.tgz#e35668de0b402f359de515c5482a1ab9f89a69bf" 13 | integrity sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg== 14 | 15 | benchmark@^2.1.4: 16 | version "2.1.4" 17 | resolved "https://registry.yarnpkg.com/benchmark/-/benchmark-2.1.4.tgz#09f3de31c916425d498cc2ee565a0ebf3c2a5629" 18 | integrity sha1-CfPeMckWQl1JjMLuVloOvzwqVik= 19 | dependencies: 20 | lodash "^4.17.4" 21 | platform "^1.3.3" 22 | 23 | cli-table@^0.3.1: 24 | version "0.3.1" 25 | resolved "https://registry.yarnpkg.com/cli-table/-/cli-table-0.3.1.tgz#f53b05266a8b1a0b934b3d0821e6e2dc5914ae23" 26 | integrity sha1-9TsFJmqLGguTSz0IIebi3FkUriM= 27 | dependencies: 28 | colors "1.0.3" 29 | 30 | colors@1.0.3: 31 | version "1.0.3" 32 | resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b" 33 | integrity sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs= 34 | 35 | esprima@^4.0.1: 36 | version "4.0.1" 37 | resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" 38 | integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== 39 | 40 | lodash@^4.17.4: 41 | version "4.17.19" 42 | resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b" 43 | integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ== 44 | 45 | meriyah@^1.6.15: 46 | version "1.6.17" 47 | resolved "https://registry.yarnpkg.com/meriyah/-/meriyah-1.6.17.tgz#eed3425fca2ee97f07d4ccf30e6fa9be340a790d" 48 | integrity sha512-X5ib1VX0F601zf2UeCXagkDqfFPSh032BdlfiIoummb2MVQGteGLum7Tx7VCxR3X1JsbQXWltaCHebrcI4NkNg== 49 | 50 | microtime@^3.0.0: 51 | version "3.0.0" 52 | resolved "https://registry.yarnpkg.com/microtime/-/microtime-3.0.0.tgz#d140914bde88aa89b4f9fd2a18620b435af0f39b" 53 | integrity sha512-SirJr7ZL4ow2iWcb54bekS4aWyBQNVcEDBiwAz9D/sTgY59A+uE8UJU15cp5wyZmPBwg/3zf8lyCJ5NUe1nVlQ== 54 | dependencies: 55 | node-addon-api "^1.2.0" 56 | node-gyp-build "^3.8.0" 57 | 58 | node-addon-api@^1.2.0: 59 | version "1.7.1" 60 | resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-1.7.1.tgz#cf813cd69bb8d9100f6bdca6755fc268f54ac492" 61 | integrity sha512-2+DuKodWvwRTrCfKOeR24KIc5unKjOh8mz17NCzVnHWfjAdDqbfbjqh7gUT+BkXBRQM52+xCHciKWonJ3CbJMQ== 62 | 63 | node-gyp-build@^3.8.0: 64 | version "3.9.0" 65 | resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-3.9.0.tgz#53a350187dd4d5276750da21605d1cb681d09e25" 66 | integrity sha512-zLcTg6P4AbcHPq465ZMFNXx7XpKKJh+7kkN699NiQWisR2uWYOWNWqRHAmbnmKiL4e9aLSlmy5U7rEMUXV59+A== 67 | 68 | platform@^1.3.3: 69 | version "1.3.5" 70 | resolved "https://registry.yarnpkg.com/platform/-/platform-1.3.5.tgz#fb6958c696e07e2918d2eeda0f0bc9448d733444" 71 | integrity sha512-TuvHS8AOIZNAlE77WUDiR4rySV/VMptyMfcfeoMgs4P8apaZM3JrnbzBiixKUv+XR6i+BXrQh8WAnjaSPFO65Q== 72 | --------------------------------------------------------------------------------