In addition to Weibo, there is also WeChat
Please pay attention
WeChat public account
Shulou
2025-01-16 Update From: SLTechnology News&Howtos shulou NAV: SLTechnology News&Howtos > Development >
Share
Shulou(Shulou.com)06/03 Report--
This article mainly explains "how to understand immutable data structures". The content of the explanation is simple and clear, and it is easy to learn and understand. let's study and learn "how to understand immutable data structures".
The Immutable library has two biggest advantages: immutability and structure sharing.
Can not be modified (easy to backtrack, easy to observe. Reduce the occurrence of errors)
Let obj = {a: 1}; handleChange (obj); / / unable to confirm the status of obj at this time console.log (obj) because there is handleChange on it.
Structure sharing (reuse memory and save space, which means that data modification can record complete data directly, and its memory pressure is not great, which is very useful for developing functions such as redoing of complex interactive projects)
Of course, due to the heavy use of Vue for development at that time, and benefiting from the optimization of Vue itself, the business abstraction and the reasonable architecture of the system, the project maintained good performance. At the same time, the library is intrusive and difficult, so it is not necessarily a good thing to introduce the project hastily.
Although the Immutable library did not bring direct benefits, I learned some ideas and optimizations to accompany me.
Shallow copy assign is competent for Immutable
When we don't use any libraries, can't we enjoy the benefits of immutable data? The answer is no.
When faced with mutable data, in most cases we use deep copy to solve the problem of two data references.
Const newData = deepCopy (myData); newData.x.y.z = 7ternewData.b.push (9)
Unfortunately, deep copying is expensive and, in some cases, unacceptable. Deep copy takes up a lot of time, and there is no structural sharing between the two. But we can mitigate this by copying only the objects that need to be changed and reusing the objects that have not changed. Like Object.assign or... To achieve structure sharing.
In most business development, we make a deep copy before modifying it. But do we really need to do this? This is not the case. From the perspective of the project as a whole, we only need to solve a core problem, "deeply nested objects". Of course, this does not mean that we put all the data on the first floor. You only need not to nest mutable data items.
Const staffA = {name: 'xx', gender:' man', company: {}, authority: []} const staffB = {... staffA} staffB.name = 'YY'// does not involve modification of complex types staffA.name / / = >' xx'const staffsA = [staffA StaffB] / / requires a shallow copy of each entry within the array const staffsB = staffsA.map (x = > ({... x})) staffsB [0] .name = 'gg'staffsA [0] .name / / = >' xx'
In this way, we turn a deep copy into a shallow copy. At the same time, structure sharing is implemented (all deeply nested objects are reused). In some cases, however, the data model is not easy to modify, and we still need to modify deeply nested objects. Then it needs to be modified in this way.
Const newData = Object.assign ({}, myData, {x: Object.assign ({}, myData.x, {y: Object.assign ({}, myData.x.y, {z: 7}),}), a: Object.assign ({}, myData.a, {b: myData.a.b.concat (9)}))
This is quite efficient for most business scenarios (because it is only a shallow copy and reuses the rest), but it is very painful to write.
Immutability-helper library aided development
Immutability-helper (the syntax is inspired by the MongoDB query language) this library provides simple syntax candy for the Object.assign scheme, making it easier to write shallow-copy code:
Import update from 'immutability-helper';const newData = update (myData, {x: {z: {$set: 7}}, a: {b: {$push: [9]}); const initialArray = [1,2,3]; const newArray = update (initialArray, {$push: [4]}); / / = > [1,2,3,4] initialArray / / > [1,2,3] available commands
$push (similar to the push of an array, but an array is provided)
$unshift (similar to the unshift of an array, but an array is provided)
$splice (similar to the splice of an array, but providing that the array is an array, $splice: [[1,1,13,14]])
Note: items in the array are applied sequentially, so order is important. The index of the target may change during the operation.
$toggle (array of strings, toggling the Boolean value of the target object)
$set (completely replace the target node, regardless of the previous data, only the data set by the current instruction)
$unset (array of strings, remove key values (array or object removal))
$merge (merge objects)
Const obj = {a: 5, b: 3}; const newObj = update (obj, {$merge: {b: 6, c: 7}}); / / = > {a: 5, b: 6, c: 7}
$add (add [key,value] array to Map)
$remove (string object, removing key for Map)
$apply (apply functions to nodes)
Const obj = {a: 5, b: 3}; const newObj = update (obj, {b: {$apply: function (x) {return x * 2;}); / / = > {a: 5, b: 6} const newObj2 = update (obj, {b: {$set: obj.b * 2}}); / / > {a: 5, b: 6}
Later, when we parse the source code, we can see the implementation of different instructions.
Extended command
We can extend commands based on the current business. For example, add tax value calculation:
Import update, {extend} from 'immutability-helper';extend (' $addtax', function (tax, original) {return original + (tax * original);}); const state = {price: 123}; const withTax = update (state, {price: {$addtax: 0.8},}); assert (JSON.stringify (withTax) = = JSON.stringify ({price: 221.4}))
If you don't want to mess up the global update function, you can make a copy and use it so that the global data is not affected:
Import {Context} from 'immutability-helper';const myContext = new Context (); myContext.extend (' $foo', function (value, original) {return 'fookeeper;}); myContext.update (/ * args * /); Source code parsing
To enhance understanding, let me parse the source code here, and the library code is very simple and powerful:
First, the tool functions (retention core, environment judgment, error warning, etc.):
/ / extraction function, which has some performance advantages when used in large quantities, and is concise (more important) const hasOwnProperty = Object.prototype.hasOwnProperty;const splice = Array.prototype.splice;const toString = Object.prototype.toString;// check type function type (obj: t) {return (toString.call (obj) as string) .slice (8,-1) } / / shallow copy, using Object.assign const assign = Object.assign | / * istanbul ignore next * / (target: t & any, source: s & Record) = > {getAllKeys (source) .forEach (key = > {if (hasOwnProperty.call (source, key)) {target [key] = source [key];}}); return target as T & S;}); / get object keyconst getAllKeys = typeof Object.getOwnPropertySymbols = = 'function'? (obj: Record) = > Object.keys (obj) .concat (Object.getOwnPropertySymbols (obj) as any) / * istanbul ignore next * /: (obj: Record) = > Object.keys (obj); / / shallow copy function copy of all data (object: T extends ReadonlyArray? ReadonlyArray: T extends Map? Map: T extends Set? Set: T extends object? T: any,) {return Array.isArray (object)? Assign (object.constructor (object.length), object): (type (object) = = 'Map')? New Map (object as Map): (type (object) = 'Set')? New Set (object as Set): (object & & typeof object = = 'object')? Assign (Object.create (Object.getPrototypeOf (object)) object) as T / * istanbul ignore next * /: object as T;}
Then there is the core code (also keeping the core):
Export class Context {/ / Import all instructions private commands: Record = assign ({}, defaultCommands); / / add extension instructions public extend (directive: string, fn: (param: any, old: T) = > T) {this.commands [directive] = fn } / / function core public update (object: t, $spec: Spec,): t {/ / enhance robustness. If the operation command is a function, modify it to $apply const spec = (typeof $spec = 'function')? {$apply: $spec}: $spec; / / array (array) check, error / / return object (array) let nextObject = object / / traversal instruction getAllKeys (spec) .forEach ((key: string) = > {/ / if the instruction is in the instruction set if (hasOwnProperty.call (this.commands, key)) {/ / performance optimization, during traversal, if the object is still the current previous data const objectWasNextObject = object = = nextObject / / modify the object nextObject = this.commands [key] ((spec as any) [key], nextObject, spec, object) with the instruction; / / after modification, the two are calculated using the incoming function, or if they are equal, directly use the previous data if (objectWasNextObject & & this.isEquals (nextObject, object)) {nextObject = object }} else {/ / is not in the instruction set, do other operations / / similar to update (collection, {2: {a: {$splice: [[1, 1, 13, 14]}); / / after parsing object rules, continue to call update recursively, recursively, return / /...}}); return nextObject;}}
Finally, there are general instructions:
Const defaultCommands = {$push (value: any, nextObject: any, spec: any) {/ / Array add, return concat new array return value.length? NextObject.concat (value): nextObject;}, $unshift (value: any, nextObject: any, spec: any) {return value.length? Value.concat (nextObject): nextObject;}, $splice (value: any, nextObject: any, spec: any, originalObject: any) {/ / Loop splice calls value.forEach ((args: any) = > {if (nextObject = = originalObject & & args.length) {nextObject = copy (originalObject);} splice.apply (nextObject, args);}); return nextObject }, $set (value: any, _ nextObject: any, spec: any) {/ / directly replace the current value return value;}, $toggle (targets: any, nextObject: any) {const nextObjectCopy = targets.length? Copy (nextObject): nextObject; / / current object or array switch targets.forEach ((target: any) = > {nextObjectCopy [target] =! nextObject [target];}); return nextObjectCopy }, $unset (value: any, nextObject: any, _ spec: any, originalObject: any) {/ / cycle delete value.forEach after copy ((key: any) = > {if (Object.hasOwnProperty.call (nextObject, key)) {if (nextObject = originalObject) {nextObject = copy (originalObject);} delete nextObject [key];}}); return nextObject }, $add (values: any, nextObject: any, _ spec: any, originalObject: any) {if (type (nextObject) = 'Map') {values.forEach (([key, value])) > {if (nextObject = originalObject & & nextObject.get (key)! = value) {nextObject = copy (originalObject);} nextObject.set (key, value);}) } else {values.forEach ((value: any) = > {if (nextObject = originalObject & &! nextObject.has (value)) {nextObject = copy (originalObject);} nextObject.add (value);});} return nextObject }, $remove (value: any, nextObject: any, _ spec: any, originalObject: any) {value.forEach ((key: any)) = > {if (nextObject = originalObject & & nextObject.has (key)) {nextObject = copy (originalObject);} nextObject.delete (key);}; return nextObject }, $merge (value: any, nextObject: any, _ spec: any, originalObject: any) {getAllKeys (value). ForEach ((key: any) = > {if (value [key]! = = nextObject [key]) {if (nextObject = = originalObject) {nextObject = copy (originalObject);} nextObject [key] = value [key];}}); return nextObject }, $apply (value: any, original: any) {/ / pass in the function, and directly call the function to modify return value (original);},}
In this way, the author wrote a concise and powerful shallow copy auxiliary library.
Excellent Immer library
Immer is an excellent immutable database that uses proxy to solve problems. There is no need to learn other api, use it right out of the box (gzipped 3kb)
Import produce from "immer" const baseState = [{todo: "Learn typescript", done: true}, {todo: "Try immer", done: false}] / / modify directly, without any development burden, const nextState = produce (baseState, draftState = > {draftState.push ({todo: "Tweet about it"}) draftState [1] .done = true})
Please refer to immer performance for performance optimization of immer.
Core code analysis
The core of the library is still encapsulated in proxy, so not all of it, only the proxy function.
Export const objectTraps: ProxyHandler = {get (state, prop) {/ / PROXY_STATE is a symbolic value, which has two functions: one is to determine whether the object has been proxied, and the other is to help proxy get the value of the corresponding state / / if the object has not been proxied, directly return if (prop = DRAFT_STATE) return state / / to get the backup of the data? If there is, otherwise get the metadata const source = latest (state) / / if the current data does not exist, get the data on the prototype if (! has (source, prop)) {return readPropFromProto (state, source, prop)} const value = source [prop] / / the current proxy object has been changed back to the value or the data is null Directly return if (state.finalized_ | |! isDraftable (value)) {return value} / / create proxy data if (value = peek (state.base_, prop)) {prepareCopy (state) return (state.copycat! [prop as any] = createProxy (state.scope_.immer_, value, state))} return value} / / whether the current data has this attribute has (state, prop) {return prop in latest (state)}, set (state: ProxyObjectState, prop: string / * strictly not, but helps TS * /, value) {const desc = getDescriptorFromProto (latest (state), prop) / / if there is a set attribute It means that the current operation item is a proxy, and you can set it directly to if (desc?.set) {desc.set.call (state.draft_, value) return true} / / which has not been modified at present. Create a copy of copy. Create a proxy if (! state.modified_) {const current = peek (latest (state), prop) const currentState: ProxyObjectState = current?. [draft _ STATE] if (currentState & & currentState.base_ = value) {state.copyletters! [prop] = value state.assigned_ [prop] = false return true} if (is (value)) while waiting for the use of STATE Current) & & (value! = = undefined | | has (state.base_, prop)) return true prepareCopy (state) markChanged (state)} state.copyletters! [prop] = value state.assigned_ [prop] = true return true}, defineProperty () {die (11)}, getPrototypeOf (state) {return Object.getPrototypeOf (state.base_)} Proxies for setPrototypeOf () {die (12)} / / array Copy the agent of the current object over Then modify deleteProperty and setconst arrayTraps: ProxyHandler = {} each (objectTraps, (key, fn) = > {/ / @ ts-ignore arrayTraps [key] = function () {arguments [0] = arguments [0] [0] return fn.apply (this, arguments)}}) arrayTraps.deleteProperty = function (state, prop) {if (_ _ DEV__ & isNaN (parseInt (prop as any)) die (13) return objectTraps.deleteProperty.call (this, state [0]) Prop)} arrayTraps.set = function (state, prop, value) {if (_ _ DEV__ & & prop! = = "length" & & isNaN (parseInt (prop as any) die (14) return objectTraps.setbacks. Call (this, state [0], prop, value, state [0])} other
In the process of development, we often use the useReducer method in the React function, but the implementation of useReducer is more complex, we can use useMethods to simplify the code. Immer is used internally in useMethods (the code is very simple, let's just copy index.ts).
Without using useMethods:
Const initialState = {nextId: 0, counters: []}; const reducer = (state, action) = > {let {nextId, counters} = state; const replaceCount = (id, transform) = > {const index = counters.findIndex (counter = > counter.id = id); const counter = counters [index] Return {... state, counters: [... counters.slice (0, index), {... counter, count: transform (counter.count)},... counters.slice (index + 1)]};}; switch (action.type) {case "ADD_COUNTER": {nextId = nextId + 1 Return {nextId, counters: [... counters, {id: nextId, count: 0}]};} case "INCREMENT_COUNTER": {return replaceCount (action.id, count = > count + 1);} case "RESET_COUNTER": {return replaceCount (action.id, () = > 0);}
Compare the use of useMethods:
Import useMethods from 'use-methods'; const initialState = {nextId: 0, counters: []}; const methods = state = > {const getCounter = id = > state.counters.find (counter = > counter.id = id); return {addCounter () {state.counters.push ({id: state.nextId++, count: 0});}, incrementCounter (id) {getCounter (id). Count++ }, resetCounter (id) {getCounter (id). Count = 0;};}; Thank you for reading, the above is the content of "how to understand immutable data structures". After the study of this article, I believe you have a deeper understanding of how to understand immutable data structures, and the specific use needs to be verified in practice. Here is, the editor will push for you more related knowledge points of the article, welcome to follow!
Welcome to subscribe "Shulou Technology Information " to get latest news, interesting things and hot topics in the IT industry, and controls the hottest and latest Internet news, technology news and IT industry trends.
Views: 0
*The comments in the above article only represent the author's personal views and do not represent the views and positions of this website. If you have more insights, please feel free to contribute and share.
Continue with the installation of the previous hadoop.First, install zookooper1. Decompress zookoope
"Every 5-10 years, there's a rare product, a really special, very unusual product that's the most un
© 2024 shulou.com SLNews company. All rights reserved.