免疫とは
不変(英語の不変)は、作成後に状態を変更できないオブジェクトです。 そのようなオブジェクトの変更の結果は常に新しいオブジェクトになりますが、古いオブジェクトは変更されません。
var mutableArr = [1, 2, 3, 4]; arr.push(5); console.log(mutableArr); // [1, 2, 3, 4, 5] //Use seamless-immutable.js var immutableArr = Immutable([1, 2, 3, 4]); var newImmutableArr = immutableArr.concat([5]); console.log(immutableArr); //[1, 2, 3, 4]; console.log(newImmutableArr); //[1, 2, 3, 4, 5];
ディープコピーについては説明していません。オブジェクトにネストされた構造がある場合、変更されていないネストされたオブジェクトはすべて再利用されます。
//Use seamless-immutable.js var state = Immutable({ style : { color : { r : 128, g : 64, b : 32 }, font : { family : 'sans-serif', size : 14 } }, text : 'Example', bounds : { size : { width : 100, height : 200 }, position : { x : 300, y : 400 } } }); var nextState = state.setIn(['style', 'color', 'r'], 99); state.bounds === nextState.bounds; //true state.text === nextState.text; //true state.style.font === state.style.font; //true
メモリでは、オブジェクトは次のように表されます。
本当ですか? JavaScript不変データ
シンプルで迅速な変更追跡
この機能は、現在人気のあるVirtualDOM ( React 、 Mithril 、 Riot )とともに積極的に使用され、Webページの再描画を加速します。
上記のstate
例を見てください。 state
オブジェクトを変更した後、それをnextState
オブジェクトと比較し、正確に何が変更されたかを調べる必要があります。 不変性はタスクを大幅に簡素化します。 state
埋め込まれた各オブジェクトの各フィールドの値をnextState
の対応する値と比較するnextState
に、対応するオブジェクトへのリンクを単純に比較して、ネストされた比較ブランチ全体を除外できます。
state === nextState //false state.text === nextState.text //true state.style === nextState.style //false state.style.color === nextState.style.color //false state.style.color.r === nextState.style.color.r //false state.style.color.g === nextState.style.color.g //true state.style.color.b === nextState.style.color.b //true state.style.font === nextState.style.font; //true //state.style.font.family === nextState.style.font.family; //true //state.style.font.size === nextState.style.font.size; //true state.bounds === nextState.bounds //true //state.bounds.size === nextState.bounds.size //true //state.bounds.size.width === nextState.bounds.size.width //true //state.bounds.size.height === nextState.bounds.size.height //true //state.bounds.position === nextState.bounds.position //true //state.bounds.position.x === nextState.bounds.position.x //true //state.bounds.position.y === nextState.bounds.position.y //true
style.font
操作は、不変であり、それらへの参照が変更されていないため、 bounds
およびstyle.font
オブジェクト内では必要ありません。
使いやすく、テストが簡単です。
関数に転送されたデータが誤って破損する可能性があり、そのような状況を追跡することは非常に困難です。
var arr = [2, 1, 3, 5, 4, 0]; function render(items) { return arr .sort(function(a, b) {return a < b ? -1 : a > b ? 1 : 0}) .map(function(item){ return '<div>' + item + '</div>'; }); } render(arr); console.log(arr); // [0, 1, 2, 3, 4, 5]
ここで、不変データは状況を保存します。 sort
機能は無効になります。
//Use seamless-immutable.js var arr = [2, 1, 3, 5, 4, 0]; function render(items) { return items .sort(function(a, b) {return a < b ? -1 : a > b ? 1 : 0}) .map(function(item){ return '<div>' + item + '</div>'; }); } render(arr); //Uncaught Error: The sort method cannot be invoked on an Immutable data structure. console.log(arr);
または、古い配列を変更せずに新しいソートされた配列を返します。
//Use immutable.js var arr = Immutable.fromJS([2, 1, 3, 5, 4, 0]); function render(items) { return arr .sort(function(a, b) {return a < b ? -1 : a > b ? 1 : 0}) .map(function(item){ return '<div>' + item + '</div>'; }); } render(arr); console.log(arr.toJS()); // [2, 1, 3, 5, 4, 0]
より大きなメモリ消費
不変オブジェクトが変更されるたびに、必要な変更とともにそのコピーが作成されます。 これにより、通常のオブジェクトを操作する場合よりも多くのメモリが消費されます。 ただし、不変オブジェクトは決して変更されないため、構造共有と呼ばれる戦略を使用して実装できます。これにより、予想よりもはるかに低いメモリコストが生成されます。 組み込みの配列やオブジェクトと比較すると、コストは依然として存在しますが、固定値を持ち、通常は不変性のために利用可能な他の利点によって補うことができます。
キャッシュ(メモ化)が簡単
ほとんどの場合、キャッシュするのは簡単ではありません。 この例は状況を明確にします:
var step_1 = Immutable({ data : { value : 0 } }); var step_2 = step_1.setIn(['data', 'value'], 1); var step_3 = step_2.setIn(['data', 'value'], 0); step_1.data === step_3.data; //false
最初のステップのdata.value
は最後のステップのdata.value
変わらないという事実にもかかわらず、 data
オブジェクト自体はすでに異なっており、そのリンクも変更されています。
副作用なし
これも事実ではありません。
function test(immutableData) { var value = immutableData.get('value'); window.title = value; return immutableData.set('value', 42); }
関数がクリーンになるか、 副作用がないという保証はありません。
コードアクセラレーション。 最適化の余地
ここですべてがそれほど明白ではないため、パフォーマンスは、作業する必要がある不変データ構造の特定の実装に依存します。 ただし、 Object.freeze
を使用してオブジェクトを取得し、単にフリーズした場合、そのオブジェクトとそのプロパティへのアクセスは高速にならず、一部のブラウザーではさらに遅くなります。
スレッドセーフ
JavaScriptはシングルスレッドであり、何も話す必要はありません。 多くの人が非同期性とマルチスレッドを混同しています-これは同じことではありません。
デフォルトでは、メッセージキューを非同期で処理するスレッドは1つだけです。
ブラウザにはマルチスレッド用のWebWorkersがありますが、スレッド間で可能な唯一の通信は、文字列またはシリアル化されたJSONを送信することです。 異なるワーカーからの同じ変数にはアクセスできません。
言語機能
キーワードconst
var
またはlet
代わりにconst
を使用することは、値が定数であること、または不変(不変)であることを意味しません。 const
キーワードは、他の値が変数に割り当てられないようにコンパイラーに指示するだけです。
const
を使用する場合const
最新のJavaScriptエンジンはいくつかの追加の最適化を実行できます。
例:
const obj = { text : 'test'}; obj.text = 'abc'; obj.color = 'red'; console.log(obj); //Object {text: "abc", color: "red"} obj = {}; //Uncaught TypeError: Assignment to constant variable.(…)
Object.freeze
Object.freeze
メソッドは、オブジェクトをフリーズします。 これは、オブジェクトへの新しいプロパティの追加、オブジェクトからの古いプロパティの削除、および既存のプロパティまたはそれらの列挙、カスタマイズ可能性、および書き込み可能属性の値の変更を防ぐことを意味します。 本質的に、オブジェクトは事実上不変になります。 このメソッドは、凍結されたオブジェクトを返します。
サードパーティのライブラリ
シームレスな不変
このライブラリは、通常の配列およびオブジェクトと下位互換性のある不変のデータ構造を提供します。 つまり、キーまたはインデックスによる値へのアクセスは通常と変わらず、標準サイクルが機能します。また、これはすべて、 LodashやUnderscoreなどのデータ操作用の特殊な高性能ライブラリと組み合わせて使用できます。
var array = Immutable(["totally", "immutable", {hammer: "Can't Touch This"}]); array[1] = "I'm going to mutate you!" array[1] // "immutable" array[2].hammer = "hm, surely I can mutate this nested object..." array[2].hammer // "Can't Touch This" for (var index in array) { console.log(array[index]); } // "totally" // "immutable" // { hammer: 'Can't Touch This' } JSON.stringify(array) // '["totally","immutable",{"hammer":"Can't Touch This"}]'
このライブラリはObject.freeze
使用Object.freeze
、データを変更できるメソッドの使用も禁止しています。
Immutable([3, 1, 4]).sort() // This will throw an ImmutableError, because sort() is a mutating method.
Safariなどの一部のブラウザーは、 Object.freeze
でフリーズしたオブジェクトを操作するときにパフォーマンスの問題があるため、パフォーマンスを向上させるためにproduction
アセンブリでこれを無効にします。
Immutable.js
Facebookの進歩により、不変データを操作するためのこのライブラリは、Web開発者の間で最も広く普及し、人気を博しています。 次の不変のデータ構造を提供します。
- リスト-JavaScript配列の不変の類似物
var list = Immutable.List([1, 3, 2, 4, 5]); console.log(list.size); //5 list = list.pop().pop(); //[1, 3, 2] list = list.push(6); //[1, 3, 2, 6] list = list.shift(); //[3, 2, 6] list = list.concat(9, 0, 1, 4); //[3, 2, 6, 9, 0, 1, 4] list = list.sort(); //[0, 1, 2, 3, 4, 6, 9]
- スタック-LIFOの原則に従って編成された要素の不変リスト(後入れ先出し、「後入れ先出し」)
var stack = new Immutable.Stack(); stack = stack.push( 2, 1, 0 ); stack.size; stack.get(); //2 stack.get(1); //1 stack.get(2); //0 stack = stack.pop(); // [1, 0]
- マップはJavaScriptオブジェクトの不変の類似物です
var map = new Immutable.Map(); map = map.set('value', 5); //{value : 5} map = map.set('text', 'Test'); //{value : 5, text : "Test"} map = map.delete('text'); // {value : 5}
- OrderedMapは、JavaScriptオブジェクトの不変の類似物であり、記述時と同じ順序で要素をトラバースすることを保証します
var map = new Immutable.OrderedMap(); map = map.set('m', 5); //{m : 5} map = map.set('a', 1); //{m : 5, a : 1} map = map.set('p', 8); //{m : 5, a : 1, p : 8} for(var elem of map) { console.log(elem); }
- セット-一意の値を格納するための不変のセット
var s1 = Immutable.Set( [2, 1] ); var s2 = Immutable.Set( [2, 3, 3] ); var s3 = Immutable.Set( [1, 1, 1] ); console.log( s1.count(), s2.size, s3.count() ); // 2 2 1 console.log( s1.toJS(), s2.toArray(), s3.toJSON() ); // [2, 1] [2, 3] [1] var s1S2IntersectArray = s1.intersect( s2 ).toJSON(); // [2]
- OrderedSetは、一意の値を格納するための不変のセットであり、記録時と同じ要素のトラバースの順序を保証します。
var s1 = Immutable.OrderedSet( [2, 1] ); var s2 = Immutable.OrderedSet( [2, 3, 3] ); var s3 = Immutable.OrderedSet( [1, 1, 1] ); var s1S2S3UnionArray = s1.union( s2, s3 ).toJSON();// [2, 1, 3] var s3S2S1UnionArray = s3.union( s2, s1 ).toJSON();// [1, 2, 3]
- Record-デフォルト値を持つ不変データコンストラクター
var Data = Immutable.Record({ value: 5 }); var Test = Immutable.Record({ text: '', data: new Data() }); var test = new Test(); console.log( test.get('data').get('value') ); //5 the default value
森
ClojureScript (リスト、ベクター、マップなど)からJavaScriptに永続的なデータ構造をもたらすライブラリー 。
Immutable.jsとの違い:
- パブリックメソッドのない機能API
- より速く
- より大きなライブラリサイズ
使用例:
var inc = function(n) { return n+1; }; mori.intoArray(mori.map(inc, mori.vector(1,2,3,4,5))); // => [2,3,4,5,6] //Efficient non-destructive updates! var v1 = mori.vector(1,2,3); var v2 = mori.conj(v1, 4); v1.toString(); // => '[1 2 3]' v2.toString(); // => '[1 2 3 4]' var sum = function(a, b) { return a + b; }; mori.reduce(sum, mori.vector(1, 2, 3, 4)); // => 10 //Lazy sequences! var _ = mori; _.intoArray(_.interpose("foo", _.vector(1, 2, 3, 4))); // => [1, "foo", 2, "foo", 3, "foo", 4]
直面する開発上の課題
Immutable.jsを使用することです( Moriの場合、すべてがほぼ同じです)。 Seamless-Immutableを使用する場合、ネイティブJavaScript構造との下位互換性により、このような問題は発生しません。
サーバーAPIを操作する
実際、ほとんどの場合、サーバーAPIはJSON形式のデータを受信および返します。これは、JavaScriptの標準オブジェクトおよび配列に対応しています。 これは、不変データを何らかの方法で通常のデータに、またはその逆に変換する必要があることを意味します。
通常のデータを不変に変換するImmutable.jsは、次の機能を提供します。
Immutable.fromJS(json: any, reviver?: (k: any, v: Iterable<any, any>) => any): any
reviver
関数reviver
使用して、独自の変換ルールを追加し、既存のルールを管理できます。
サーバーAPIが次のオブジェクトを返したとします。
var response = [ {_id : '573b44d91fd2f10100d5f436', value : 1}, {_id : '573dd87b212dc501001950f2', value : 2}, {_id : '5735f6ae2a380401006af05b', value : 3}, {_id : '56bdc2e1cee8b801000ff339', value : 4} ]
最も便利なのは、そのようなオブジェクトがOrderedMapとして表示されることです。 対応するreviver
を作成しreviver
。
var state = Immutable.fromJS(response, function(k, v){ if(Immutable.Iterable.isIndexed(v)) { for(var elem of v) { if(!elem.get('_id')) { return elem; } } var ordered = []; for(var elem of v) { ordered.push([elem.get('_id'), elem.get('value')]); } return Immutable.OrderedMap(ordered); } return v; }); console.log(state.toJS()); //Object {573b44d91fd2f10100d5f436: 1, 573dd87b212dc501001950f2: 2, 5735f6ae2a380401006af05b: 3, 56bdc2e1cee8b801000ff339: 4}
データを変更してサーバーに送り返す必要があるとします。
state = state.setIn(['573dd87b212dc501001950f2', 5]); console.log(state.toJS()); //Object {573b44d91fd2f10100d5f436: 1, 573dd87b212dc501001950f2: 5, 5735f6ae2a380401006af05b: 3, 56bdc2e1cee8b801000ff339: 4}
不変データを通常のデータに変換するImmutable.jsには、次の機能があります。
toJS(): any
ご覧のとおり、 reviver
ません。つまり、独自の外部immutableHelper
を作成する必要があります。 そして、どういうわけか、彼は通常のOrderMapとソースデータの構造に一致するOrderMapを区別できなければなりません。 また、OrderMapから継承することはできません。 このアプリケーションでは、構造がネストされる可能性が高く、これにより複雑さが増します。
もちろん、開発時にリストとマップのみを使用できますが、それ以外のすべてを使用するのはなぜですか? そして、特にImmutable.jsを使用する利点は何ですか?
どこでも免疫
プロジェクトがネイティブデータ構造を使用していた場合、不変のデータ構造に簡単に切り替えることができます。 データと何らかの形で相互作用するすべてのコードを書き直す必要があります。
シリアライゼーション/デシリアライゼーション
Immutable.jsは、 fromJS
、 toJS
fromJS
提供します。これらは次のようにtoJS
します。
var set = Immutable.Set([1, 2, 3, 2, 1]); set = Immutable.fromJS(set.toJS()); console.log(Immutable.Set.isSet(set)); //false console.log(Immutable.List.isList(set)); //true
これは、シリアライゼーション/デシリアライゼーションにはまったく役に立ちません。
サードパーティのtransit-immutable-jsライブラリがあります。 その使用例:
var transit = require('transit-immutable-js'); var Immutable = require('immutable'); var m = Immutable.Map({with: "Some", data: "In"}); var str = transit.toJSON(m); console.log(str) // ["~#cmap",["with","Some","data","In"]] var m2 = transit.fromJSON(str); console.log(Immutable.is(m, m2));// true
性能
パフォーマンスをテストするためにベンチマークが作成されました。 それらを自宅で実行するには、次のコマンドを実行します。
git clone https://github.com/MrCheater/immutable-benchmarks.git cd ./immutable-benchmarks npm install npm start
ベンチマーク結果はグラフで見ることができます(繰り返し/ ms)。 実行時間が長いほど、結果は悪くなります。
読むとき、ネイティブデータ構造とシームレス不変が最速であることが判明しました。
録音するとき、森は最速であることが判明しました。 シームレス不変は最悪の結果を示しました。
おわりに
この記事は、パフォーマンスを改善するためにアプリケーションで不変データを使用する必要に直面しているJavaScript開発者に役立ちます。 特に、これは、 VirtualDOM ( React 、 Mithril 、 Riot )を使用するフレームワークやFlux / Reduxソリューションを使用するフロントエンド開発者に適用されます。
要約すると、JavaScriptで免疫性が考慮されているライブラリの中で、最も速く、最も便利で使いやすいのはSeamless-immutableです。 最も安定して一般的なのはImmutable.jsです。 記録が最も速く、最も珍しいのは森です。 この調査が、プロジェクトのソリューションを選択するのに役立つことを願っています。 がんばって。