GitHub - mweststrate/immer: Create the next immutable state by mutating the current one
(MobXの作者による)イミュータブルに状態を操作するユーティリティライブラリ。
ちなみに、この世で最初にスターしたのは俺ですw
immerとは
読んだのは`v0.8.2`。
ImmutableJSみたく独特なAPIを覚えなくてもいい、今まで通り配列やオブジェクトを変更すればいい。
それなのにイミュータブルにデータを扱える!素敵!というやつ。
// いわゆるreducerが const byId = (state, action) => { switch (action.type) { case RECEIVE_PRODUCTS: return { ...state, ...action.products.reduce((obj, product) => { obj[product.id] = product return obj }, {}) } default: return state } } // immerならこう書ける const byId = (state, action) => produce(state, draft => { switch (action.type) { case RECEIVE_PRODUCTS: action.products.forEach(product => { draft[product.id] = product }) } })
ちなみに作者による概説記事もあるので、TL;DR的な意味でリンクを。
一言でいうならば、Proxyを通してコピーオンライトでデータ構造をイミュータブルにするライブラリ。
コードの構造
├── common.js いわゆるUtil ├── es5.js Proxyに対応してない環境向けの実装 ├── index.js es5かproxyかの振り分け └── proxy.js Proxyに対応してる環境向けの実装
というわけで、今回主に見るのは`proxy.js`に書かれてる内容。
そもそもProxy
7割くらいの人は「なんやっけそれ?」ってなる気がするw
const obj = { a: 1 }; const handler = { get(target, name) { return name in target ? target[name] : 0; }, set(target, name, val) { if (typeof val !== 'number') { target[name] = 0; } else { target[name] = val; } return target[name]; }, }; const p = new Proxy(obj, handler); // Get 0 for undefined props console.log(p.a); // 1 console.log(p.b); // 0 // Set 0 if value is not a number p.c = 10; console.log(p.c); // 10 p.d = 'Not a number'; console.log(p.d); // 0
という感じで、`get`とか`set`とか内部的に呼ばれる操作にトラップを仕掛けて、その名の通り挙動をプロキシできるやつ。
トラップを仕掛けられる内部的な操作は以下の通り。
Internal Method | Handler Method |
---|---|
GetPrototypeOf | `getPrototypeOf` |
SetPrototypeOf | `setPrototypeOf` |
IsExtensible | `isExtensible` |
PreventExtensions | `preventExtensions` |
GetOwnProperty | `getOwnPropertyDescriptor` |
DefineOwnProperty | `defineProperty` |
HasProperty | `has` |
Get | `get` |
Set | `set` |
Delete | `deleteProperty` |
OwnPropertyKeys | `ownKeys` |
Call | `apply` |
Construct | `construct` |
いわゆるES6で追加されてたけど、メタプロ用というニッチさからまったく話題になってないAPIさん。
で、このProxyは生成の仕方が2パターンあって、上述のコンストラクタでやるパターンともう1つ、後から`revoke()`できるやつがある。
const { proxy, revoke } = Proxy.revocable(target, handler); revoke();
`immer`で使ってるのはこっち。
というか、使い捨てないコードならこっちしか使わんような気もする。
produceProxy(baseState, producer)が全て
正確には2つの関数が`export`されてるけど、まあコレが本丸。
export function produceProxy(baseState, producer) { const previousProxies = revocableProxies revocableProxies = [] try { // create proxy for root const rootClone = createProxy(undefined, baseState) // execute the thunk const maybeVoidReturn = producer(rootClone) //values either than undefined will trigger warning; !Object.is(maybeVoidReturn, undefined) && console.warn( `Immer callback expects no return value. However ${typeof maybeVoidReturn} was returned` ) // and finalize the modified proxy const res = finalize(rootClone) // revoke all proxies revocableProxies.forEach(p => p.revoke()) return res } finally { revocableProxies = previousProxies } }
`try`の中でやってる以下がキモ。
// Proxyを仕込む const rootClone = createProxy(undefined, baseState) // そこに対してWriteな変更があれば、それが記録される producer(rootClone) // 記録された変更と、元の状態をあわせて新たな状態を返す const res = finalize(rootClone) // Proxyは使い終わったのでrevoke revocableProxies.forEach(p => p.revoke()) return res
同じ人やし当たり前なんやけど、MobXでやってたのと雰囲気は近い。
次期MobXもProxy使うとかどうとか。
createProxy()
配列かオブジェクトかを判別して、それぞれのトラップを仕込むだけ。
この時にあわせて、内部で扱う用に独自のプロパティを持つオブジェクトにコンバートしてる。
function createState(parent, base) { return { modified: false, finalized: false, parent, base, copy: undefined, proxies: {} } }
このProxyのSetにトラップしてる関数が、そのツリーに対して変更(Write)があったかを見てる。
function set(state, prop, value) { if (!state.modified) { if ( (prop in state.base && Object.is(state.base[prop], value)) || (prop in state.proxies && state.proxies[prop] === value) ) return true markChanged(state) } state.copy[prop] = value return true }
これで後でどれが変更されたのかが追える。
finalize()
オリジナルとコピーを検証して返す。
変更されてれば(`modified`なフラグが立ってれば)コピーを返すし、何も変わってないならオリジナルな参照を返す。
ちなみに、`Object.freeze()`して返してくれるのもココでやってる。