您好,登錄后才能下訂單哦!
這篇文章主要講解了“如何理解不可變數據結構”,文中的講解內容簡單清晰,易于學習與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學習“如何理解不可變數據結構”吧!
Immutable 庫具有兩個最大的優勢: 不可修改以及結構共享。
不可修改(容易回溯,易于觀察。減少錯誤的發生)
let obj = { a: 1 }; handleChange(obj); // 由于上面有 handleChange,無法確認 obj 此時的狀態 console.log(obj)
結構共享( 復用內存,節省空間,也就意味著數據修改可以直接記錄完整數據,其內存壓力也不大,這樣對于開發復雜交互項目的重做等功能很有用)
當然,由于當時還在重度使用 Vue 進行開發,而且 受益于 Vue 本身的優化以及業務抽象和系統的合理架構,項目一直保持著良好的性能。同時該庫的侵入性和難度都很大,貿然引入項目也未必是一件好事。
雖然 Immutable 庫沒有帶來直接的收益,但從中學到一些思路和優化卻陪伴著我。
當我們不使用任何庫,我們是否就無法享受不可變數據的利好?答案是否定的。
當面臨可變性數據時候,大部分情況下我們會使用深拷貝來解決兩個數據引用的問題。
const newData = deepCopy(myData); newData.x.y.z = 7; newData.a.b.push(9);
不幸的是,深度拷貝是昂貴的,在有些情況下更是不可接受的。深拷貝占用了大量的時間,同時兩者之間沒有任何結構共享。但我們可以通過僅復制需要更改的對象和重用未更改的對象來減輕這種情況。如 Object.assign 或者 ... 來實現結構共享。
大多數業務開發中,我們都是先進行深拷貝,再進行修改。但是我們真的需要這樣做嗎?事實并非如此。從項目整體出發的話,我們只需要解決一個核心問題 “深層嵌套對象”。當然,這并不意味著我們把所有的數據都放在第一層。只需要不嵌套可變的數據項即可。
const staffA = { name: 'xx', gender: 'man', company: {}, authority: [] } const staffB = {...staffA} staffB.name = 'YY' // 不涉及到 復雜類型的修改即可 staffA.name // => 'xx' const staffsA = [staffA, staffB] // 需要對數組內部每一項進行淺拷貝 const staffsB = staffsA.map(x => ({...x})) staffsB[0].name = 'gg' staffsA[0].name // => 'xx'
如此,我們就把深拷貝變為了淺拷貝。同時實現了結構共享 (所有深度嵌套對象都被復用了) 。但有些情況下,數據模型并不是容易修改的,我們還是需要修改深度嵌套對象。那么就需要這樣修改了。
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)}) });
這對于絕大部份的業務場景來說是相當高效的(因為它只是淺拷貝,并重用了其余的部分) ,但是編寫起來卻非常痛苦。
immutability-helper (語法受到了 MongoDB 查詢語言的啟發 ) 這個庫為 Object.assign 方案提供了簡單的語法糖,使得編寫淺拷貝代碼更加容易:
import update from 'immutability-helper'; const newData = update(myData, { x: {y: {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]
$push (類似于數組的 push,但是提供的是數組)
$unshift (類似于數組的 unshift,但是提供的是數組)
$splice (類似于數組的 splice, 但提供數組是一個數組, $splice: [ [1, 1, 13, 14] ] )
注意:數組中的項目是順序應用的,因此順序很重要。目標的索引可能會在操作過程中發生變化。
$toggle (字符串數組,切換目標對象的布爾數值)
$set (完全替換目標節點, 不考慮之前的數據,只用當前指令設置的數據)
$unset (字符串數組,移除 key 值(數組或者對象移除))
$merge (合并對象)
const obj = {a: 5, b: 3}; const newObj = update(obj, {$merge: {b: 6, c: 7}}); // => {a: 5, b: 6, c: 7}
$add(為 Map 添加 [key,value] 數組)
$remove (字符串對象,為 Map 移除 key)
$apply (應用函數到節點)
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}
后面我們解析源碼時,可以看到不同指令的實現。
我們可以基于當前業務去擴展命令。如添加稅值計算:
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 }));
如果您不想弄臟全局的 update
函數,可以制作一個副本并使用該副本,這樣不會影響全局數據:
import { Context } from 'immutability-helper'; const myContext = new Context(); myContext.extend('$foo', function(value, original) { return 'foo!'; }); myContext.update(/* args */);
為了加強理解,這里我來解析一下源代碼,同時該庫代碼十分簡潔強大:
先是工具函數(保留核心,環境判斷,錯誤警告等邏輯去除):
// 提取函數,大量使用時有一定性能優勢,且簡明(更重要) const hasOwnProperty = Object.prototype.hasOwnProperty; const splice = Array.prototype.splice; const toString = Object.prototype.toString; // 檢查類型 function type<T>(obj: T) { return (toString.call(obj) as string).slice(8, -1); } // 淺拷貝,使用 Object.assign const assign = Object.assign || /* istanbul ignore next */ (<T, S>(target: T & any, source: S & Record<string, any>) => { getAllKeys(source).forEach(key => { if (hasOwnProperty.call(source, key)) { target[key] = source[key] ; } }); return target as T & S; }); // 獲取對象 key const getAllKeys = typeof Object.getOwnPropertySymbols === 'function' ? (obj: Record<string, any>) => Object.keys(obj).concat(Object.getOwnPropertySymbols(obj) as any) /* istanbul ignore next */ : (obj: Record<string, any>) => Object.keys(obj); // 所有數據的淺拷貝 function copy<T, U, K, V, X>( object: T extends ReadonlyArray<U> ? ReadonlyArray<U> : T extends Map<K, V> ? Map<K, V> : T extends Set<X> ? Set<X> : T extends object ? T : any, ) { return Array.isArray(object) ? assign(object.constructor(object.length), object) : (type(object) === 'Map') ? new Map(object as Map<K, V>) : (type(object) === 'Set') ? new Set(object as Set<X>) : (object && typeof object === 'object') ? assign(Object.create(Object.getPrototypeOf(object)), object) as T /* istanbul ignore next */ : object as T; }
然后是核心代碼(同樣保留核心) :
export class Context { // 導入所有指令 private commands: Record<string, any> = assign({}, defaultCommands); // 添加擴展指令 public extend<T>(directive: string, fn: (param: any, old: T) => T) { this.commands[directive] = fn; } // 功能核心 public update<T, C extends CustomCommands<object> = never>( object: T, $spec: Spec<T, C>, ): T { // 增強健壯性,如果操作命令是函數,修改為 $apply const spec = (typeof $spec === 'function') ? { $apply: $spec } : $spec; // 數組(數組) 檢查,報錯 // 返回對象(數組) let nextObject = object; // 遍歷指令 getAllKeys(spec).forEach((key: string) => { // 如果指令在指令集中 if (hasOwnProperty.call(this.commands, key)) { // 性能優化,遍歷過程中,如果 object 還是當前之前數據 const objectWasNextObject = object === nextObject; // 用指令修改對象 nextObject = this.commands[key]((spec as any)[key], nextObject, spec, object); // 修改后,兩者使用傳入函數計算,還是相等的情況下,直接使用之前數據 if (objectWasNextObject && this.isEquals(nextObject, object)) { nextObject = object; } } else { // 不在指令集中,做其他操作 // 類似于 update(collection, {2: {a: {$splice: [[1, 1, 13, 14]]}}}); // 解析對象規則后繼續遞歸調用 update, 不斷遞歸,不斷返回 // ... } }); return nextObject; } }
最后是通用指令:
const defaultCommands = { $push(value: any, nextObject: any, spec: any) { // 數組添加,返回 concat 新數組 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) { // 循環 splice 調用 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) { // 直接替換當前數值 return value; }, $toggle(targets: any, nextObject: any) { const nextObjectCopy = targets.length ? copy(nextObject) : nextObject; // 當前對象或者數組切換 targets.forEach((target: any) => { nextObjectCopy[target] = !nextObject[target]; }); return nextObjectCopy; }, $unset(value: any, nextObject: any, _spec: any, originalObject: any) { // 拷貝后循環刪除 value.forEach((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) { // 傳入函數,直接調用函數修改 return value(original); }, };
就這樣,作者寫了一個簡潔而強大的淺拷貝輔助庫。
Immer 是一個非常優秀的不可變數據庫,利用 proxy 來解決問題。不需要學習其他 api,開箱即用 ( gzipped 3kb )
import produce from "immer" const baseState = [ { todo: "Learn typescript", done: true }, { todo: "Try immer", done: false } ] // 直接修改,沒有任何開發負擔,心情美美噠 const nextState = produce(baseState, draftState => { draftState.push({todo: "Tweet about it"}) draftState[1].done = true })
關于 immer 性能優化請參考 immer performance。
該庫的核心還是在 proxy 的封裝,所以不全部介紹,僅介紹代理功能。
export const objectTraps: ProxyHandler<ProxyState> = { get(state, prop) { // PROXY_STATE是一個symbol值,有兩個作用,一是便于判斷對象是不是已經代理過,二是幫助proxy拿到對應state的值 // 如果對象沒有代理過,直接返回 if (prop === DRAFT_STATE) return state // 獲取數據的備份?如果有,否則獲取元數據 const source = latest(state) // 如果當前數據不存在,獲取原型上數據 if (!has(source, prop)) { return readPropFromProto(state, source, prop) } const value = source[prop] // 當前代理對象已經改回了數值或者改數據是 null,直接返回 if (state.finalized_ || !isDraftable(value)) { return value } // 創建代理數據 if (value === peek(state.base_, prop)) { prepareCopy(state) return (state.copy_![prop as any] = createProxy( state.scope_.immer_, value, state )) } return value }, // 當前數據是否有該屬性 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) // 如果當前有 set 屬性,意味當前操作項是代理,直接設置即可 if (desc?.set) { desc.set.call(state.draft_, value) return true } // 當前沒有修改過,建立副本 copy,等待使用 get 時創建代理 if (!state.modified_) { const current = peek(latest(state), prop) const currentState: ProxyObjectState = current?.[DRAFT_STATE] if (currentState && currentState.base_ === value) { state.copy_![prop] = value state.assigned_[prop] = false return true } if (is(value, current) && (value !== undefined || has(state.base_, prop))) return true prepareCopy(state) markChanged(state) } state.copy_![prop] = value state.assigned_[prop] = true return true }, defineProperty() { die(11) }, getPrototypeOf(state) { return Object.getPrototypeOf(state.base_) }, setPrototypeOf() { die(12) } } // 數組的代理,把當前對象的代理拷貝過去,再修改 deleteProperty 和 set const arrayTraps: ProxyHandler<[ProxyArrayState]> = {} 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.set!.call(this, state[0], prop, value, state[0]) }
開發過程中,我們往往會在 React 函數中使用 useReducer 方法,但是 useReducer 實現較為復雜,我們可以用 useMethods 簡化代碼。useMethods 內部就是使用 immer (代碼十分簡單,我們直接拷貝 index.ts 即可)。
不使用 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); } } };
對比使用 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; } }; };
感謝各位的閱讀,以上就是“如何理解不可變數據結構”的內容了,經過本文的學習后,相信大家對如何理解不可變數據結構這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是億速云,小編將為大家推送更多相關知識點的文章,歡迎關注!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。