UNPKG

immutability-util

Version:

Mutate a copy of data without changing the original source. Inspired by immutability-helper.

331 lines (295 loc) 7.7 kB
/** * Copyright (c) 2017 hustcc * License: MIT * GitHub: https://github.com/hustcc/immutability-util * * Inspired by: * * https://github.com/kolodny/immutability-helper * https://github.com/ProtoTeam/immutability-helper-x * * [Notice] * Version 0.0.x -> 0.1.x * Copy the data when using the methods, and the `value()` API only return the current state which is immutable. * When call multiple methods, will has repeated data copy. * * Version 0.2.x (TODO) * Only save the path array and value, and copy the data when calling `value()` API. * Only do data copy once on the node, will get much more performance. * **/ const invariant = require('./invariant.js'); class ImmutabilityUtil { /** * @param state */ constructor(state = {}) { this.state = state; // state data this.commands = this._commands(); } _commands = () => ({ $apply: (value, origin) => { this._invariantApply(value); return value(origin); }, $merge: (value, origin) => { this._invariantMerge(value, origin); let next = origin; this._allKeys(value).forEach((key) => { if (value[key] !== next[key]) { if (next === origin) next = this._copy(origin); next[key] = value[key]; } }); return next; }, $push: (value, origin) => { this._invariantPushAndUnshift(value, origin, '$push'); return value.length ? origin.concat(value) : origin; }, $set: value => value, $splice: (value, origin) => { this._invariantSplices(value, origin); let next = origin; value.forEach((splice) => { this._invariantSplice(splice); if (next === origin && splice.length) next = this._copy(origin); Array.prototype.splice.apply(next, splice); }); return next; }, $unset: (value, origin) => { this._invariantUnset(value); let next = origin; value.forEach((key) => { if (Object.hasOwnProperty.call(next, key)) { if (next === origin) next = this._copy(origin); delete next[key]; } }); return next; }, $unshift: (value, origin) => { this._invariantPushAndUnshift(value, origin, '$unshift'); return value.length ? value.concat(origin) : origin; }, }); /** * Clone object. * @param object * @returns {*} * @private */ _copy = (object) => { if (object instanceof Array) return object.slice(); else if (object && typeof object === 'object') return Object.assign(new object.constructor(), object); return object; }; /** * Get all keys of object. Symbols supported. * @param obj * @returns {*} * @private */ _allKeys = (obj) => { if (typeof Object.getOwnPropertySymbols === 'function') { return Object.keys(obj).concat(Object.getOwnPropertySymbols(obj)); } return Object.keys(obj); }; /** * Get path array from path string. * path string is the string like `app.ui.data`, split by . * @param path * @returns {Array|*} * @private */ _pathArray = (path) => { if (Array.isArray(path)) return path.filter(e => !!e); // [n] => .n path = path.replace(/(\[\d+\])/g, s => `.${s.slice(1, -1)}`); return path.split('.').filter(e => !!e); }; /** * 核心处理函数 * @param path * @param update * @param processor 处理的函数 * @private */ _process = (path, update, processor) => { path = this._pathArray(path); const length = path.length; // start to loop the path string. const _loop = (state, i) => { let nextState = state; if (i < length - 1) { i += 1; const key = path[i]; const nextStateForKey = _loop(nextState[key], i); if (nextStateForKey !== nextState[key]) { if (nextState === state) { // console.log('copy', key); nextState = this._copy(state); } nextState[key] = nextStateForKey; } } else { nextState = processor(update, state); } return nextState; }; this.state = _loop(this.state, -1); }; /** * $apply * @param path * @param value * @returns {ImmutabilityUtil} */ $apply = (path, value) => { this._process(path, value, this.commands.$apply); return this; }; /** * $merge * @param path * @param value * @returns {ImmutabilityUtil} */ $merge = (path, value) => { this._process(path, value, this.commands.$merge); return this; }; /** * $push * @param path * @param value * @returns {ImmutabilityUtil} */ $push = (path, value) => { this._process(path, value, this.commands.$push); return this; }; /** * $set * @param path * @param value * @returns {ImmutabilityUtil} */ $set =(path, value) => { this._process(path, value, this.commands.$set); return this; }; /** * $splice * @param path * @param value * @returns {ImmutabilityUtil} */ $splice = (path, value) => { this._process(path, value, this.commands.$splice); return this; }; /** * $unset * @param path * @param value * @returns {ImmutabilityUtil} */ $unset = (path, value) => { this._process(path, value, this.commands.$unset); return this; }; /** * $unshift * @param path * @param value * @returns {ImmutabilityUtil} */ $unshift = (path, value) => { this._process(path, value, this.commands.$unshift); return this; }; /** * get the state value. * @returns {{}|*} */ value = () => this.state; setState = (state) => { this.state = state; return this; }; // invariants _invariantPushAndUnshift = (value, origin, command) => { invariant( Array.isArray(origin), 'iu(): expected target of %s to be an array; got %s.', command, origin ); invariant( Array.isArray(value), 'iu(): expected value of %s to be an array; got %s. Did you forget to wrap your parameters in an array?', command, value ); }; _invariantSplices = (value, origin) => { invariant( Array.isArray(origin), 'iu(): expected target of $splice to be an array; got %s.', origin ); this._invariantSplice(value); }; _invariantSplice = (value) => { invariant( Array.isArray(value), 'iu(): expected value of $splice to be an array of arrays; got %s. ' + 'Did you forget to wrap your parameters in an array?', value ); }; _invariantApply = (fn) => { invariant( typeof fn === 'function', 'iu(): expected target of $apply to be a function; got %s.', fn ); }; _invariantMerge = (value, origin) => { invariant( value && typeof value === 'object', 'iu(): expected value of $merge to be an \'object\'; got %s.', value ); invariant( origin && typeof origin === 'object', 'iu(): expected target of $merge to be an \'object\'; got %s.', origin ); }; _invariantUnset = (value) => { invariant( Array.isArray(value), 'iu(): expected value of $unset to be an array; got %s. Did you forget to wrap your parameters in an array?', value ); }; } // 单例模式,提高性能 const instance = new ImmutabilityUtil(); /** * 库函数入口 * @param state 需要被修改的 state 状态 * @param newInstance 默认使用单例,提高性能 * @returns {ImmutabilityUtil} */ const entry = (state, newInstance = false) => { if (newInstance) { return new ImmutabilityUtil(state); } return instance.setState(state); }; module.exports = entry;