├── .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 | Method |
33 | Samples |
34 | Loops |
35 | FPS (Hz) |
36 |
37 |
38 |
39 |
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 |
--------------------------------------------------------------------------------