├── .gitattributes ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── array-observe.js ├── array-observe.min.js ├── array-observe.min.map ├── benchmark ├── benchmarks.js └── index.html ├── benchmarks.md ├── bower.json ├── package.json ├── readme.md └── test ├── index.html └── tests.js /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | 3 | # Windows image file caches 4 | Thumbs.db 5 | ehthumbs.db 6 | 7 | # Folder config file 8 | Desktop.ini 9 | 10 | # Recycle Bin used on file shares 11 | $RECYCLE.BIN/ 12 | 13 | # Windows Installer files 14 | *.cab 15 | *.msi 16 | *.msm 17 | *.msp 18 | 19 | # Windows shortcuts 20 | *.lnk 21 | 22 | # ========================= 23 | # Operating System Files 24 | # ========================= 25 | 26 | # OSX 27 | # ========================= 28 | 29 | .DS_Store 30 | .AppleDouble 31 | .LSOverride 32 | 33 | # Thumbnails 34 | ._* 35 | 36 | # Files that might appear on external disk 37 | .Spotlight-V100 38 | .Trashes 39 | 40 | # Directories potentially created on remote AFP share 41 | .AppleDB 42 | .AppleDesktop 43 | Network Trash Folder 44 | Temporary Items 45 | .apdisk 46 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .gitignore 2 | .gitattributes 3 | *.min.* 4 | bower.json 5 | 6 | # Windows image file caches 7 | Thumbs.db 8 | ehthumbs.db 9 | 10 | # Folder config file 11 | Desktop.ini 12 | 13 | # Recycle Bin used on file shares 14 | $RECYCLE.BIN/ 15 | 16 | # Windows Installer files 17 | *.cab 18 | *.msi 19 | *.msm 20 | *.msp 21 | 22 | # Windows shortcuts 23 | *.lnk 24 | 25 | # ========================= 26 | # Operating System Files 27 | # ========================= 28 | 29 | # OSX 30 | # ========================= 31 | 32 | .DS_Store 33 | .AppleDouble 34 | .LSOverride 35 | 36 | # Thumbnails 37 | ._* 38 | 39 | # Files that might appear on external disk 40 | .Spotlight-V100 41 | .Trashes 42 | 43 | # Directories potentially created on remote AFP share 44 | .AppleDB 45 | .AppleDesktop 46 | Network Trash Folder 47 | Temporary Items 48 | .apdisk 49 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "4.2" 4 | - "0.12" 5 | - "0.10" 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Massimo Artizzu (MaxArt2501) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /array-observe.js: -------------------------------------------------------------------------------- 1 | Object.observe && !Array.observe && (function(O, A) { 2 | "use strict"; 3 | 4 | var notifier = O.getNotifier, 5 | perform = "performChange", 6 | original = "_original", 7 | type = "splice"; 8 | 9 | var wrappers = { 10 | push: function push(item) { 11 | var args = arguments, 12 | ret = push[original].apply(this, args); 13 | 14 | notifier(this)[perform](type, function() { 15 | return { 16 | index: ret - args.length, 17 | addedCount: args.length, 18 | removed: [] 19 | }; 20 | }); 21 | 22 | return ret; 23 | }, 24 | unshift: function unshift(item) { 25 | var args = arguments, 26 | ret = unshift[original].apply(this, args); 27 | 28 | notifier(this)[perform](type, function() { 29 | return { 30 | index: 0, 31 | addedCount: args.length, 32 | removed: [] 33 | }; 34 | }); 35 | 36 | return ret; 37 | }, 38 | pop: function pop() { 39 | var len = this.length, 40 | item = pop[original].call(this); 41 | 42 | if (this.length !== len) 43 | notifier(this)[perform](type, function() { 44 | return { 45 | index: this.length, 46 | addedCount: 0, 47 | removed: [ item ] 48 | }; 49 | }, this); 50 | 51 | return item; 52 | }, 53 | shift: function shift() { 54 | var len = this.length, 55 | item = shift[original].call(this); 56 | 57 | if (this.length !== len) 58 | notifier(this)[perform](type, function() { 59 | return { 60 | index: 0, 61 | addedCount: 0, 62 | removed: [ item ] 63 | }; 64 | }, this); 65 | 66 | return item; 67 | }, 68 | splice: function splice(start, deleteCount) { 69 | var args = arguments, 70 | removed = splice[original].apply(this, args); 71 | 72 | if (removed.length || args.length > 2) 73 | notifier(this)[perform](type, function() { 74 | return { 75 | index: start, 76 | addedCount: args.length - 2, 77 | removed: removed 78 | }; 79 | }, this); 80 | 81 | return removed; 82 | } 83 | }; 84 | 85 | for (var wrapper in wrappers) { 86 | wrappers[wrapper][original] = A.prototype[wrapper]; 87 | A.prototype[wrapper] = wrappers[wrapper]; 88 | } 89 | 90 | A.observe = function(object, handler) { 91 | return O.observe(object, handler, [ "add", "update", "delete", type ]); 92 | }; 93 | A.unobserve = O.unobserve; 94 | 95 | })(Object, Array); 96 | -------------------------------------------------------------------------------- /array-observe.min.js: -------------------------------------------------------------------------------- 1 | Object.observe&&!Array.observe&&function(t,e){"use strict";var n=t.getNotifier,r="performChange",i="_original",o="splice";var u={push:function h(t){var e=arguments,u=h[i].apply(this,e);n(this)[r](o,function(){return{index:u-e.length,addedCount:e.length,removed:[]}});return u},unshift:function d(t){var e=arguments,u=d[i].apply(this,e);n(this)[r](o,function(){return{index:0,addedCount:e.length,removed:[]}});return u},pop:function a(){var t=this.length,e=a[i].call(this);if(this.length!==t)n(this)[r](o,function(){return{index:this.length,addedCount:0,removed:[e]}},this);return e},shift:function l(){var t=this.length,e=l[i].call(this);if(this.length!==t)n(this)[r](o,function(){return{index:0,addedCount:0,removed:[e]}},this);return e},splice:function f(t,e){var u=arguments,s=f[i].apply(this,u);if(s.length||u.length>2)n(this)[r](o,function(){return{index:t,addedCount:u.length-2,removed:s}},this);return s}};for(var s in u){u[s][i]=e.prototype[s];e.prototype[s]=u[s]}e.observe=function(e,n){return t.observe(e,n,["add","update","delete",o])};e.unobserve=t.unobserve}(Object,Array); 2 | //# sourceMappingURL=array-observe.min.map -------------------------------------------------------------------------------- /array-observe.min.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"array-observe.min.js","sources":["array-observe.js"],"names":["Object","observe","Array","O","A","notifier","getNotifier","perform","original","type","wrappers","push","item","args","arguments","ret","apply","this","index","length","addedCount","removed","unshift","pop","len","call","shift","splice","start","deleteCount","wrapper","prototype","object","handler","unobserve"],"mappings":"AAAAA,OAAOC,UAAYC,MAAMD,SAAW,SAAUE,EAAGC,GACjD,YAEA,IAAIC,GAAWF,EAAEG,YACbC,EAAU,gBACVC,EAAW,YACXC,EAAO,QAEX,IAAIC,IACAC,KAAM,QAASA,GAAKC,GAChB,GAAIC,GAAOC,UACPC,EAAMJ,EAAKH,GAAUQ,MAAMC,KAAMJ,EAErCR,GAASY,MAAMV,GAASE,EAAM,WAC1B,OACIS,MAAOH,EAAMF,EAAKM,OAClBC,WAAYP,EAAKM,OACjBE,aAIR,OAAON,IAEXO,QAAS,QAASA,GAAQV,GACtB,GAAIC,GAAOC,UACPC,EAAMO,EAAQd,GAAUQ,MAAMC,KAAMJ,EAExCR,GAASY,MAAMV,GAASE,EAAM,WAC1B,OACIS,MAAO,EACPE,WAAYP,EAAKM,OACjBE,aAIR,OAAON,IAEXQ,IAAK,QAASA,KACV,GAAIC,GAAMP,KAAKE,OACXP,EAAOW,EAAIf,GAAUiB,KAAKR,KAE9B,IAAIA,KAAKE,SAAWK,EAChBnB,EAASY,MAAMV,GAASE,EAAM,WAC1B,OACIS,MAAOD,KAAKE,OACZC,WAAY,EACZC,SAAWT,KAEhBK,KAEP,OAAOL,IAEXc,MAAO,QAASA,KACZ,GAAIF,GAAMP,KAAKE,OACXP,EAAOc,EAAMlB,GAAUiB,KAAKR,KAEhC,IAAIA,KAAKE,SAAWK,EAChBnB,EAASY,MAAMV,GAASE,EAAM,WAC1B,OACIS,MAAO,EACPE,WAAY,EACZC,SAAWT,KAEhBK,KAEP,OAAOL,IAEXe,OAAQ,QAASA,GAAOC,EAAOC,GAC3B,GAAIhB,GAAOC,UACPO,EAAUM,EAAOnB,GAAUQ,MAAMC,KAAMJ,EAE3C,IAAIQ,EAAQF,QAAUN,EAAKM,OAAS,EAChCd,EAASY,MAAMV,GAASE,EAAM,WAC1B,OACIS,MAAOU,EACPR,WAAYP,EAAKM,OAAS,EAC1BE,QAASA,IAEdJ,KAEP,OAAOI,IAIf,KAAK,GAAIS,KAAWpB,GAAU,CAC1BA,EAASoB,GAAStB,GAAYJ,EAAE2B,UAAUD,EAC1C1B,GAAE2B,UAAUD,GAAWpB,EAASoB,GAGpC1B,EAAEH,QAAU,SAAS+B,EAAQC,GACzB,MAAO9B,GAAEF,QAAQ+B,EAAQC,GAAW,MAAO,SAAU,SAAUxB,IAEnEL,GAAE8B,UAAY/B,EAAE+B,WAEblC,OAAQE"} -------------------------------------------------------------------------------- /benchmark/benchmarks.js: -------------------------------------------------------------------------------- 1 | 2 | (function(root, tests) { 3 | if (!root.original) root.original = { 4 | push: Array.prototype.push, 5 | pop: Array.prototype.pop, 6 | unshift: Array.prototype.unshift, 7 | shift: Array.prototype.shift, 8 | splice: Array.prototype.splice 9 | }; 10 | 11 | if (typeof define === "function" && define.amd) 12 | define(["expect", "object-observe"], tests); 13 | else if (typeof exports === "object") 14 | tests(require("benchmark"), root, require("object.observe"), require("../array-observe.js")); 15 | else tests(root.Benchmark, root); 16 | })(typeof global !== "undefined" ? global : this, function(Benchmark, root) { 17 | "use strict"; 18 | 19 | var onDOM = typeof document !== "undefined"; 20 | 21 | if (!Array.prototype.push._original) { 22 | var message = "This environment already supports Array.observe"; 23 | 24 | if (onDOM) document.body.innerHTML = message; 25 | else console.log(message); 26 | 27 | return; 28 | } 29 | 30 | root.wrapped = {}; 31 | root.original = {}; 32 | root.foo = function(array) { array[100]--; }; 33 | 34 | var methods = "push pop unshift shift splice".split(" "); 35 | for (var i = 0; i < methods.length; i++) { 36 | root.wrapped[methods[i]] = Array.prototype[methods[i]]; 37 | root.original[methods[i]] = Array.prototype[methods[i]]._original; 38 | } 39 | 40 | var padspace = (new Array(81)).join(" "); 41 | function padL(text, length) { 42 | return (padspace + text).slice(-length); 43 | } 44 | function padR(text, length) { 45 | return (text + padspace).slice(0, length); 46 | } 47 | 48 | 49 | if (onDOM) { 50 | var parent = document.getElementsByTagName("tbody")[0]; 51 | 52 | var initBench = function() { 53 | var row = this.row = document.createElement("tr"), 54 | cell = document.createElement("td"); 55 | cell.innerHTML = this.name; 56 | row.appendChild(cell); 57 | 58 | this.cells = []; 59 | cell = document.createElement("td"); 60 | cell.className = "samples"; 61 | this.cells.push(cell); 62 | row.appendChild(cell); 63 | cell = document.createElement("td"); 64 | cell.className = "count"; 65 | this.cells.push(cell); 66 | row.appendChild(cell); 67 | cell = document.createElement("td"); 68 | cell.className = "frequency"; 69 | this.cells.push(cell); 70 | row.appendChild(cell); 71 | 72 | parent.appendChild(row); 73 | }, 74 | onCycle = function() { 75 | this.cells[0].innerHTML = ++this.samples; 76 | this.cells[1].innerHTML = this.count; 77 | this.cells[2].innerHTML = this.hz.toFixed(2); 78 | }; 79 | } else { 80 | var writeOut = typeof process === "undefined" || !process.stdout || !process.stdout.isTTY 81 | ? function(text) { console.log(text); } 82 | : function(text) { process.stdout.write(text); }; 83 | writeOut("\x1b[1;37mMethod Samples Loops FPS (Hz)\n"); 84 | writeOut( "---------------------------------------------------------"); 85 | var initBench = function() { 86 | writeOut("\n\x1b[1;30m" + padR(this.name, 57)); 87 | }, 88 | onCycle = function() { 89 | writeOut("\x1b[38D\x1b[1;31m" + padL(++this.samples, 9) + "\x1b[1;36m" + padL(this.count, 12) 90 | + "\x1b[1;32m" + padL(this.hz.toFixed(2), 17) + "\x1b[0;37m"); 91 | }; 92 | } 93 | 94 | function errorBench(e) { 95 | console.log(e); 96 | } 97 | 98 | var benches = []; 99 | 100 | function generateBenchGroup(method, func) { 101 | var options = { 102 | setup: "\ 103 | var array = [];\n\ 104 | Array.prototype." + method + " = original." + method + ";\n", 105 | teardown: " foo(array);", // To prevent dead code removal 106 | fn: func, 107 | onStart: initBench, 108 | onCycle: onCycle, 109 | onError: errorBench, 110 | onComplete: nextBench, 111 | async: true, 112 | samples: 0 113 | }; 114 | benches.push(new Benchmark("Native ." + method, options)); 115 | 116 | options.setup = "\ 117 | var array = [];\n\ 118 | Array.prototype." + method + " = wrapped." + method + ";\n"; 119 | benches.push(new Benchmark("Wrapped ." + method, options)); 120 | } 121 | 122 | generateBenchGroup("push", " array.push();"); 123 | generateBenchGroup("pop", "\ 124 | array[0] = item;\n\ 125 | var item = array.pop();\n"); 126 | 127 | generateBenchGroup("unshift", " array.unshift();"); 128 | generateBenchGroup("shift", "\ 129 | array[0] = item;\n\ 130 | var item = array.shift();\n"); 131 | 132 | generateBenchGroup("splice", " array.splice(0, 1, 1);"); 133 | 134 | var index = 0; 135 | 136 | function nextBench() { 137 | if (index >= benches.length) return; 138 | 139 | benches[index++].run(); 140 | } 141 | 142 | nextBench(); 143 | 144 | }); 145 | -------------------------------------------------------------------------------- /benchmark/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Array.observe polyfill benchmarks 6 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
MethodSamplesLoopsFPS (Hz)
40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /benchmarks.md: -------------------------------------------------------------------------------- 1 | Benchmarks 2 | ========== 3 | 4 | As mentioned in the [readme](readme.md), wrapped methods are roughly from 6 times to 400 times slower, but the good part is that they're fast enough for most cases anyway. In any case, you can judge by yourself. 5 | 6 | The numbers under the "Native" and "Wrapped" columns are the times the methods get executed in a second, respectively in their native and wrapped versions. The "Times slower" column is nothing more than the ratio between those values, showing how many times the wrapped version is slower than its native counterpart. 7 | 8 | ## Desktop/server 9 | 10 | Test platforms: 11 | * node.js, Edge, IE and Firefox on Windows 10 64 bit (Intel Core i7-4702, 2.2 GHz, 8 GB RAM) 12 | 13 | ### Firefox 42 14 | 15 | | Method | Native | Wrapped | Times slower | 16 | |:--------|------------:|----------:|-------------:| 17 | | push | 25417829.66 | 543275.63 | 46.79 | 18 | | pop | 7860866.30 | 677851.76 | 11.60 | 19 | | unshift | 14044179.39 | 510869.77 | 27.49 | 20 | | shift | 6914513.50 | 655179.43 | 10.55 | 21 | | splice | 2995672.07 | 497946.07 | 6.02 | 22 | 23 | ### Edge 20 24 | 25 | | Method | Native | Wrapped | Times slower | 26 | |:--------|------------:|----------:|-------------:| 27 | | push | 54767474.31 | 224397.80 | 244.06 | 28 | | pop | 17836266.04 | 319777.57 | 55.78 | 29 | | unshift | 42150329.96 | 287599.60 | 146.56 | 30 | | shift | 22004442.66 | 308611.56 | 71.30 | 31 | | splice | 7153927.19 | 154216.82 | 46.39 | 32 | 33 | ### Internet Explorer 11 34 | 35 | | Method | Native | Wrapped | Times slower | 36 | |:--------|------------:|----------:|-------------:| 37 | | push | 67207010.31 | 153079.86 | 439.03 | 38 | | pop | 19568671.29 | 226766.50 | 86.29 | 39 | | unshift | 61131116.39 | 153108.35 | 399.27 | 40 | | shift | 32824464.61 | 266664.56 | 123.09 | 41 | | splice | 7139044.25 | 127588.32 | 55.95 | 42 | 43 | ### Node.js 0.10.40 44 | 45 | | Method | Native | Wrapped | Times slower | 46 | |:--------|------------:|----------:|-------------:| 47 | | push | 45629360.74 | 344638.00 | 132.40 | 48 | | pop | 48703376.85 | 351396.30 | 138.60 | 49 | | unshift | 35820837.05 | 337823.47 | 106.03 | 50 | | shift | 19675249.29 | 338654.35 | 58.10 | 51 | | splice | 10476493.27 | 327468.59 | 31.99 | 52 | 53 | 54 | ## Mobile 55 | 56 | Test platforms: 57 | * Firefox 40 on Samsung Galaxy Note 3 with Android 5.0 (Qualcomm Snapdragon 800, quad core, 2.3 GHz, 3 GB RAM) 58 | * Safari 8 on iPhone 5 with iOS 8.4 (Apple A6, dual core, 1.3 GHz, 1 GB RAM) 59 | * IE Mobile on Nokia Lumia 820 with Windows Phone 8.1 (Qualcomm Snapdragon S4 Plus, 1.5 GHz, 1 GB RAM) 60 | 61 | ### Firefox 40 62 | 63 | | Method | Native | Wrapped | Times slower | 64 | |:--------|-----------:|---------:|-------------:| 65 | | push | 6693456.04 | 39537.94 | 169.29 | 66 | | pop | 3198455.25 | 28403.75 | 112.61 | 67 | | unshift | 4958252.73 | 37789.83 | 131.21 | 68 | | shift | 1562613.21 | 15324.80 | 101.97 | 69 | | splice | 927863.70 | 32838.00 | 28.26 | 70 | 71 | ### Safari 8 72 | 73 | | Method | Native | Wrapped | Times slower | 74 | |:--------|------------:|----------:|-------------:| 75 | | push | 5368891.57 | 133325.58 | 40.27 | 76 | | pop | 39911211.76 | 229963.86 | 173.55 | 77 | | unshift | 5833209.88 | 129235.29 | 45.14 | 78 | | shift | 2396270.59 | 144764.71 | 16.55 | 79 | | splice | 1824675.32 | 107976.19 | 16.90 | 80 | 81 | ### IE Mobile 82 | 83 | | Method | Native | Wrapped | Times slower | 84 | |:--------|------------:|----------:|-------------:| 85 | | push | 13773883.19 | 17019.24 | 809.31 | 86 | | pop | 5193937.67 | 23500.54 | 221.01 | 87 | | unshift | 7586075.99 | 17224.12 | 440.43 | 88 | | shift | 2927335.62 | 23124.06 | 126.59 | 89 | | splice | 1133894.17 | 13962.84 | 81.21 | 90 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "array.observe", 3 | "version": "0.0.1", 4 | "homepage": "https://github.com/MaxArt2501/array-observe", 5 | "authors": [ 6 | "Massimo Artizzu " 7 | ], 8 | "description": "Array.observe polyfill based on ES7 spec", 9 | "main": "array-observe.min.js", 10 | "moduleType": [ 11 | "globals" 12 | ], 13 | "keywords": [ 14 | "observe", 15 | "data-binding", 16 | "Object.observe" 17 | ], 18 | "license": "MIT", 19 | "ignore": [ 20 | "**/.*", 21 | "node_modules", 22 | "bower_components", 23 | "benchmark", 24 | "test" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "array.observe", 3 | "version": "0.0.1", 4 | "description": "Array.observe polyfill based on ES7 spec", 5 | "main": "array-observe.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "scripts": { 10 | "test": "node_modules/.bin/mocha", 11 | "benchmark": "node benchmark/benchmarks.js" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/MaxArt2501/array-observe.git" 16 | }, 17 | "keywords": [ 18 | "observe", 19 | "data-binding", 20 | "Object.observe" 21 | ], 22 | "author": { 23 | "name": "Massimo Artizzu", 24 | "email": "maxart.x@gmail.com" 25 | }, 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/MaxArt2501/array-observe/issues" 29 | }, 30 | "homepage": "https://github.com/MaxArt2501/array-observe", 31 | "devDependencies": { 32 | "benchmark": "^1.0.0", 33 | "expect.js": "^0.3.1", 34 | "mocha": "^2.1.0", 35 | "object.observe": "^0.2.4" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | Array.observe polyfill 2 | ====================== 3 | 4 | This polyfill is actually meant to be a companion to the [Object.observe polyfill](https://github.com/MaxArt2501/object-observe) (or any other [`Object.observe`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/observe) shim that doesn't already support `Array.observe` and doesn't natively detect the `"splice"` event) and **it comes with a big warning** (read the [Under the hood](#under-the-hood) section). 5 | 6 | The reason of this split is because of its obtrusive nature. 7 | 8 | ## `Object.observe` isn't a proposed spec anymore 9 | 10 | You might have read this around, but back in November `Object.observe` proposal was withdrawn from TC39. This also means that `Object.observe` will be pulled from Chrome and other V8-based environments, and that would imply that *developers shouldn't rely on it anymore*. Web development evolved in the direction of functional programming and immutable objects, so that's where we all should look at. 11 | 12 | Read more on the page for [`Object.observe`](https://github.com/MaxArt2501/object-observe). 13 | 14 | ## Installation 15 | 16 | This polyfill extends the native `Array` and doesn't have any dependencies, so loading it is pretty straightforward: 17 | 18 | ```html 19 | 20 | ``` 21 | 22 | Use `array-observe.min.js` for the minified version. 23 | 24 | Using bower: 25 | 26 | ```bash 27 | $ bower install array.observe 28 | ``` 29 | 30 | Or in node.js: 31 | 32 | ```bash 33 | $ npm install array.observe 34 | ``` 35 | 36 | The environment **must** already support `Object.observe` (either natively or via polyfill), or else the shim won't be installed. 37 | 38 | 39 | ## The "splice" event 40 | 41 | According to the [spec](http://arv.github.io/ecmascript-object-observe/#Array.observe), `Array.observe` is basically like calling `Object.observe` with an `acceptTypes` option set to `[ "add", "update", "delete", "splice" ]`, where the first three types are already supported by the aforementioned polyfill, and the fourth one isn't. 42 | 43 | The `"splice"` change can't be easily detected by a "dirty checking" loop (it can only be approximated by some kind of "distance" algorithm, which is usually computationally expensive) but, still according to the spec, it's triggered by certain array operations that modify the array itself. Namely, those are the `push`, `pop`, `shift`, `unshift` and `splice` methods, plus others like these: 44 | 45 | ```js 46 | // Suppose beginning with `array` being an empty array 47 | 48 | // Defining elements on indexes greater or equal to `array.length` 49 | array[0] = "foo"; 50 | 51 | // Altering the `length` property (either increasing or decreasing it) 52 | array.length = 3; 53 | 54 | // Using `Object.assign`. The following will actually produce two separate 55 | // "splice" events - basically equivalent to 56 | // array[3] = "bar"; 57 | // array[4] = "baz"; 58 | Object.assign(array, { 3: "bar", 4: "baz" }); 59 | ``` 60 | 61 | Additionally, the `"splice"` event is also triggered when one of the mentioned array methods are called on a non-array object: 62 | 63 | ```js 64 | var object = {}; 65 | 66 | Array.prototype.push.call(object, "foo"); 67 | // => object === { 0: "foo", length: 1 } 68 | ``` 69 | 70 | 71 | ## Under the hood 72 | 73 | This polyfill wraps the native array `push`, `pop`, `shift`, `unshift` and `splice` methods so that they do a `performChange` call on the array's notifier. *It's precisely [what the spec says](http://arv.github.io/ecmascript-object-observe/#Array-changes) should happen*. 74 | 75 | Unfortunately, this is certainly an obtrusive way to generate `"splice"` changes, not to mention it reduces the performance of said methods. Benchmarks show that wrapped methods are from 6 times (for `splice`) to 400 times slower (for `push`) - YMMV depending on the executing environment (see the [Benchmarks](#benchmarks) section later and some [results](benchmarks.md)). 76 | 77 | In order to apply the polyfill also when calling array methods on generic objects, the methods are wrapped *directly on `Array.prototype`*, thus affecting *all* the arrays, even when not observed. 78 | 79 | Moreover, this polyfill doesn't trigger a `"splice"` change when performing an array operation that does *not* use one of the above methods. That is handled normally in `Object.observe`, firing the usual `"add"`, `"update"` and `"delete"` events. 80 | 81 | ### Workaround for performance 82 | 83 | If you don't want the array methods to be wrapped for *every* array, you can restore the original array methods using the `_original` property defined on the wrapped methods. You can attach the wrapped method only to the objects you want to observe, redefining `Array.observe` with something like this: 84 | 85 | ```js 86 | var wrapped = {}, 87 | methods = [ "push", "pop", "unshift", "shift", "splice" ]; 88 | 89 | // Restoring the original array methods, and saving the wrapped ones 90 | methods.forEach(function(method) { 91 | wrapped[method] = Array.prototype[method]; 92 | Array.prototype[method] = wrapped[method]._original; 93 | }); 94 | 95 | Array.observe = (function(observe) { 96 | return function(array, handler) { 97 | // Applying the wrapped methods to the observed array 98 | methods.forEach(function(method) { 99 | if (method in array) 100 | array[method] = wrapped[method]; 101 | }); 102 | observe(array, handler); 103 | }; 104 | })(Array.observe); 105 | ``` 106 | 107 | 108 | ## Tests 109 | 110 | Tests are performed using [mocha](http://mochajs.org/) and assertions are made using [expect](https://github.com/Automattic/expect.js), which are set as development dependencies. Assuming you're in the project's root directory, if you want to run the tests after installing the package, just do 111 | 112 | ```bash 113 | cd node_modules/array.observe 114 | npm install 115 | ``` 116 | 117 | Then you can execute `npm run test` or, if you have mocha installed globally, just `mocha` from the package's root directory. 118 | 119 | For client side testing, just open [index.html](test/index.html) in your browser of choice. 120 | 121 | 122 | ## Benchmarks 123 | 124 | Some benchmarks have been created, using [benchmark.js](http://benchmarkjs.com/), testing the performances of the wrapped array methods (see some [results](benchmarks.md)). 125 | 126 | After having installed the development dependencies (see above), open the [index.html](../benchmark/index.html) file in the benchmark/ directory in your browser of choice. To test node.js < 0.11.13, run `npm run benchmark`. 127 | 128 | The benchmarks won't start if `Array.observe` is natively supported. 129 | 130 | 131 | ## License 132 | 133 | MIT. See [LICENSE](LICENSE) for more details. 134 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Array.observe polyfill tests 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /test/tests.js: -------------------------------------------------------------------------------- 1 | (function(root, tests) { 2 | if (typeof define === "function" && define.amd) 3 | define(["expect", "object-observe", "array-observe"], tests); 4 | else if (typeof exports === "object") 5 | tests(require("expect.js"), require("object.observe"), require("../array-observe.js")); 6 | else tests(root.expect); 7 | })(this, function(expect) { 8 | "use strict"; 9 | 10 | function looseIndexOf(pivot, array) { 11 | for (var i = 0, r = -1; i < array.length && r < 0; i++) 12 | if (expect.eql(array[i], pivot)) r = i; 13 | return r; 14 | }; 15 | 16 | expect.Assertion.prototype.looselyContain = function(obj) { 17 | this.assert(~looseIndexOf(obj, this.obj), 18 | function(){ return 'expected ' + expect.stringify(this.obj) + ' to contain ' + expect.stringify(obj) }, 19 | function(){ return 'expected ' + expect.stringify(this.obj) + ' to not contain ' + expect.stringify(obj) } 20 | ); 21 | return this; 22 | }; 23 | 24 | if (typeof Object.observe === "function") { 25 | console.log(/\{ \[native code\] \}/.test(Object.observe.toString()) 26 | ? "Object.observe is natively supported by the environment" 27 | : "Object.observe has been correctly polyfilled"); 28 | } else console.log("Object.observe has NOT been polyfilled"); 29 | 30 | if (typeof Array.observe === "function") { 31 | console.log(/\{ \[native code\] \}/.test(Array.observe.toString()) 32 | ? "Array.observe is natively supported by the environment" 33 | : "Array.observe has been correctly polyfilled"); 34 | } else console.log("Array.observe has NOT been polyfilled"); 35 | 36 | var timeout = 30; 37 | 38 | describe("Array.observe", function() { 39 | it("should notify 'add', 'update' and 'delete' changes", function(done) { 40 | function handler(changes) { 41 | try { 42 | expect(tested).to.be(false); 43 | expect(changes).to.have.length(3) 44 | .and.to.looselyContain({ type: "add", name: "test", object: array }) 45 | .and.to.looselyContain({ type: "update", name: "0", object: array, oldValue: "foo" }) 46 | .and.to.looselyContain({ type: "delete", name: "foo", object: array, oldValue: "bar" }); 47 | tested = true; 48 | } catch (e) { done(e); } 49 | } 50 | 51 | var array = [ "foo" ], 52 | tested = false; 53 | array.foo = "bar"; 54 | Array.observe(array, handler); 55 | 56 | array.test = "Hello!"; 57 | array[0] = "bar"; 58 | delete array.foo; 59 | 60 | Array.unobserve(array, handler); 61 | 62 | setTimeout(function() { 63 | try { 64 | expect(tested).to.be(true); 65 | done(); 66 | } catch (e) { done(e); } 67 | }, timeout); 68 | }); 69 | 70 | it("should notify 'splice' changes when using 'push'", function(done) { 71 | function handler(changes) { 72 | try { 73 | expect(tested).to.be(false); 74 | expect(changes).to.have.length(1) 75 | .and.to.looselyContain({ type: "splice", object: array, addedCount: 2, removed: [], index: 1 }); 76 | tested = true; 77 | } catch (e) { done(e); } 78 | } 79 | 80 | var array = [ "foo" ], 81 | tested = false; 82 | Array.observe(array, handler); 83 | 84 | var len = array.push("bar", "baz"); 85 | 86 | Array.unobserve(array, handler); 87 | 88 | expect(len).to.be(3); 89 | expect(array).to.eql([ "foo", "bar", "baz" ]); 90 | 91 | setTimeout(function() { 92 | try { 93 | expect(tested).to.be(true); 94 | done(); 95 | } catch (e) { done(e); } 96 | }, timeout); 97 | }); 98 | 99 | it("should notify 'splice' changes when using 'pop'", function(done) { 100 | function handler(changes) { 101 | try { 102 | expect(tested).to.be(false); 103 | expect(changes).to.have.length(1) 104 | .and.to.looselyContain({ type: "splice", object: array, addedCount: 0, removed: [ "bar" ], index: 1 }); 105 | tested = true; 106 | } catch (e) { done(e); } 107 | } 108 | 109 | var array = [ "foo", "bar" ], 110 | tested = false; 111 | Array.observe(array, handler); 112 | 113 | var item = array.pop(); 114 | 115 | Array.unobserve(array, handler); 116 | 117 | expect(item).to.be("bar"); 118 | 119 | setTimeout(function() { 120 | try { 121 | expect(tested).to.be(true); 122 | done(); 123 | } catch (e) { done(e); } 124 | }, timeout); 125 | }); 126 | 127 | it("should not notify 'splice' changes when popping an empty array", function(done) { 128 | function handler() { 129 | done(new Error("expected no changes")); 130 | } 131 | 132 | var array = []; 133 | Array.observe(array, handler); 134 | 135 | array.pop(); 136 | 137 | Array.unobserve(array, handler); 138 | 139 | setTimeout(done, timeout); 140 | }); 141 | 142 | it("should notify 'splice' changes when using 'unshift'", function(done) { 143 | function handler(changes) { 144 | try { 145 | expect(tested).to.be(false); 146 | expect(changes).to.have.length(1) 147 | .and.to.looselyContain({ type: "splice", object: array, addedCount: 2, removed: [], index: 0 }); 148 | tested = true; 149 | } catch (e) { done(e); } 150 | } 151 | 152 | var array = [ "foo" ], 153 | tested = false; 154 | Array.observe(array, handler); 155 | 156 | var len = array.unshift("bar", "baz"); 157 | 158 | Array.unobserve(array, handler); 159 | 160 | expect(len).to.be(3); 161 | expect(array).to.eql([ "bar", "baz", "foo" ]); 162 | 163 | setTimeout(function() { 164 | try { 165 | expect(tested).to.be(true); 166 | done(); 167 | } catch (e) { done(e); } 168 | }, timeout); 169 | }); 170 | 171 | it("should notify 'splice' changes when using 'shift'", function(done) { 172 | function handler(changes) { 173 | try { 174 | expect(tested).to.be(false); 175 | expect(changes).to.have.length(1) 176 | .and.to.looselyContain({ type: "splice", object: array, addedCount: 0, removed: [ "foo" ], index: 0 }); 177 | tested = true; 178 | } catch (e) { done(e); } 179 | } 180 | 181 | var array = [ "foo", "bar" ], 182 | tested = false; 183 | Array.observe(array, handler); 184 | 185 | var item = array.shift(); 186 | 187 | Array.unobserve(array, handler); 188 | 189 | expect(item).to.be("foo"); 190 | 191 | setTimeout(function() { 192 | try { 193 | expect(tested).to.be(true); 194 | done(); 195 | } catch (e) { done(e); } 196 | }, timeout); 197 | }); 198 | 199 | it("should not notify 'splice' changes when shifting an empty array", function(done) { 200 | function handler() { 201 | done(new Error("expected no changes")); 202 | } 203 | 204 | var array = []; 205 | Array.observe(array, handler); 206 | 207 | array.shift(); 208 | 209 | Array.unobserve(array, handler); 210 | 211 | setTimeout(done, timeout); 212 | }); 213 | 214 | it("should notify 'splice' changes when using 'splice'", function(done) { 215 | function handler(changes) { 216 | try { 217 | expect(tested).to.be(false); 218 | expect(changes).to.have.length(1) 219 | .and.to.looselyContain({ type: "splice", object: array, addedCount: 2, removed: [ "bar" ], index: 1 }); 220 | tested = true; 221 | } catch (e) { done(e); } 222 | } 223 | 224 | var array = [ "foo", "bar", "baz" ], 225 | tested = false; 226 | Array.observe(array, handler); 227 | 228 | var removed = array.splice(1, 1, "lorem", "ipsum"); 229 | 230 | Array.unobserve(array, handler); 231 | 232 | expect(removed).to.eql([ "bar" ]); 233 | expect(array).to.eql([ "foo", "lorem", "ipsum", "baz" ]); 234 | 235 | setTimeout(function() { 236 | try { 237 | expect(tested).to.be(true); 238 | done(); 239 | } catch (e) { done(e); } 240 | }, timeout); 241 | }); 242 | 243 | it("should not notify 'splice' changes when splicing with no effect", function(done) { 244 | function handler() { 245 | done(new Error("expected no changes")); 246 | } 247 | 248 | var array = [ "foo" ]; 249 | Array.observe(array, handler); 250 | 251 | array.splice(2, 1); 252 | array.splice(1, 0); 253 | 254 | Array.unobserve(array, handler); 255 | 256 | setTimeout(done, timeout); 257 | }); 258 | 259 | it("should be stopped when using 'Object.unobserve'", function(done) { 260 | function handler() { 261 | done(new Error("expected no changes")); 262 | } 263 | 264 | var array = []; 265 | Array.observe(array, handler); 266 | Object.unobserve(array, handler); 267 | 268 | array.push("foo"); 269 | 270 | setTimeout(done, timeout); 271 | }); 272 | }); 273 | 274 | describe("Array.unobserve", function() { 275 | it("should work just like 'Object.unobserve'", function(done) { 276 | function handler(changes) { 277 | try { 278 | expect(tested).to.be(false); 279 | expect(changes).to.have.length(1); 280 | expect(changes[0]).to.eql({ type: "add", name: "foo", object: obj }); 281 | tested = true; 282 | } catch (e) { done(e); } 283 | } 284 | 285 | var obj = {}, 286 | tested = false; 287 | Object.observe(obj, handler); 288 | 289 | obj.foo = 6; 290 | 291 | Array.unobserve(obj, handler); 292 | 293 | obj.foo = 28; 294 | 295 | setTimeout(function() { 296 | try { 297 | expect(tested).to.be(true); 298 | done(); 299 | } catch (e) { done(e); } 300 | }, timeout); 301 | }); 302 | }); 303 | 304 | }); 305 | --------------------------------------------------------------------------------