這篇文章主要講解了“如何理解不可變數(shù)據(jù)結(jié)構(gòu)”,文中的講解內(nèi)容簡(jiǎn)單清晰,易于學(xué)習(xí)與理解,下面請(qǐng)大家跟著小編的思路慢慢深入,一起來(lái)研究和學(xué)習(xí)“如何理解不可變數(shù)據(jù)結(jié)構(gòu)”吧!
創(chuàng)新互聯(lián)專注于懷遠(yuǎn)網(wǎng)站建設(shè)服務(wù)及定制,我們擁有豐富的企業(yè)做網(wǎng)站經(jīng)驗(yàn)。 熱誠(chéng)為您提供懷遠(yuǎn)營(yíng)銷型網(wǎng)站建設(shè),懷遠(yuǎn)網(wǎng)站制作、懷遠(yuǎn)網(wǎng)頁(yè)設(shè)計(jì)、懷遠(yuǎn)網(wǎng)站官網(wǎng)定制、小程序設(shè)計(jì)服務(wù),打造懷遠(yuǎn)網(wǎng)絡(luò)公司原創(chuàng)品牌,更為您提供懷遠(yuǎn)網(wǎng)站排名全網(wǎng)營(yíng)銷落地服務(wù)。
Immutable 庫(kù)具有兩個(gè)最大的優(yōu)勢(shì): 不可修改以及結(jié)構(gòu)共享。
不可修改(容易回溯,易于觀察。減少錯(cuò)誤的發(fā)生)
let obj = { a: 1 }; handleChange(obj); // 由于上面有 handleChange,無(wú)法確認(rèn) obj 此時(shí)的狀態(tài) console.log(obj)
結(jié)構(gòu)共享( 復(fù)用內(nèi)存,節(jié)省空間,也就意味著數(shù)據(jù)修改可以直接記錄完整數(shù)據(jù),其內(nèi)存壓力也不大,這樣對(duì)于開(kāi)發(fā)復(fù)雜交互項(xiàng)目的重做等功能很有用)
當(dāng)然,由于當(dāng)時(shí)還在重度使用 Vue 進(jìn)行開(kāi)發(fā),而且 受益于 Vue 本身的優(yōu)化以及業(yè)務(wù)抽象和系統(tǒng)的合理架構(gòu),項(xiàng)目一直保持著良好的性能。同時(shí)該庫(kù)的侵入性和難度都很大,貿(mào)然引入項(xiàng)目也未必是一件好事。
雖然 Immutable 庫(kù)沒(méi)有帶來(lái)直接的收益,但從中學(xué)到一些思路和優(yōu)化卻陪伴著我。
當(dāng)我們不使用任何庫(kù),我們是否就無(wú)法享受不可變數(shù)據(jù)的利好?答案是否定的。
當(dāng)面臨可變性數(shù)據(jù)時(shí)候,大部分情況下我們會(huì)使用深拷貝來(lái)解決兩個(gè)數(shù)據(jù)引用的問(wèn)題。
const newData = deepCopy(myData); newData.x.y.z = 7; newData.a.b.push(9);
不幸的是,深度拷貝是昂貴的,在有些情況下更是不可接受的。深拷貝占用了大量的時(shí)間,同時(shí)兩者之間沒(méi)有任何結(jié)構(gòu)共享。但我們可以通過(guò)僅復(fù)制需要更改的對(duì)象和重用未更改的對(duì)象來(lái)減輕這種情況。如 Object.assign 或者 ... 來(lái)實(shí)現(xiàn)結(jié)構(gòu)共享。
大多數(shù)業(yè)務(wù)開(kāi)發(fā)中,我們都是先進(jìn)行深拷貝,再進(jìn)行修改。但是我們真的需要這樣做嗎?事實(shí)并非如此。從項(xiàng)目整體出發(fā)的話,我們只需要解決一個(gè)核心問(wèn)題 “深層嵌套對(duì)象”。當(dāng)然,這并不意味著我們把所有的數(shù)據(jù)都放在第一層。只需要不嵌套可變的數(shù)據(jù)項(xiàng)即可。
const staffA = { name: 'xx', gender: 'man', company: {}, authority: [] } const staffB = {...staffA} staffB.name = 'YY' // 不涉及到 復(fù)雜類型的修改即可 staffA.name // => 'xx' const staffsA = [staffA, staffB] // 需要對(duì)數(shù)組內(nèi)部每一項(xiàng)進(jìn)行淺拷貝 const staffsB = staffsA.map(x => ({...x})) staffsB[0].name = 'gg' staffsA[0].name // => 'xx'
如此,我們就把深拷貝變?yōu)榱藴\拷貝。同時(shí)實(shí)現(xiàn)了結(jié)構(gòu)共享 (所有深度嵌套對(duì)象都被復(fù)用了) 。但有些情況下,數(shù)據(jù)模型并不是容易修改的,我們還是需要修改深度嵌套對(duì)象。那么就需要這樣修改了。
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)}) });
這對(duì)于絕大部份的業(yè)務(wù)場(chǎng)景來(lái)說(shuō)是相當(dāng)高效的(因?yàn)樗皇菧\拷貝,并重用了其余的部分) ,但是編寫(xiě)起來(lái)卻非常痛苦。
immutability-helper (語(yǔ)法受到了 MongoDB 查詢語(yǔ)言的啟發(fā) ) 這個(gè)庫(kù)為 Object.assign 方案提供了簡(jiǎn)單的語(yǔ)法糖,使得編寫(xiě)淺拷貝代碼更加容易:
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 (類似于數(shù)組的 push,但是提供的是數(shù)組)
$unshift (類似于數(shù)組的 unshift,但是提供的是數(shù)組)
$splice (類似于數(shù)組的 splice, 但提供數(shù)組是一個(gè)數(shù)組, $splice: [ [1, 1, 13, 14] ] )
注意:數(shù)組中的項(xiàng)目是順序應(yīng)用的,因此順序很重要。目標(biāo)的索引可能會(huì)在操作過(guò)程中發(fā)生變化。
$toggle (字符串?dāng)?shù)組,切換目標(biāo)對(duì)象的布爾數(shù)值)
$set (完全替換目標(biāo)節(jié)點(diǎn), 不考慮之前的數(shù)據(jù),只用當(dāng)前指令設(shè)置的數(shù)據(jù))
$unset (字符串?dāng)?shù)組,移除 key 值(數(shù)組或者對(duì)象移除))
$merge (合并對(duì)象)
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] 數(shù)組)
$remove (字符串對(duì)象,為 Map 移除 key)
$apply (應(yīng)用函數(shù)到節(jié)點(diǎn))
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}
后面我們解析源碼時(shí),可以看到不同指令的實(shí)現(xiàn)。
我們可以基于當(dāng)前業(yè)務(wù)去擴(kuò)展命令。如添加稅值計(jì)算:
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
函數(shù),可以制作一個(gè)副本并使用該副本,這樣不會(huì)影響全局?jǐn)?shù)據(jù):
import { Context } from 'immutability-helper'; const myContext = new Context(); myContext.extend('$foo', function(value, original) { return 'foo!'; }); myContext.update(/* args */);
為了加強(qiáng)理解,這里我來(lái)解析一下源代碼,同時(shí)該庫(kù)代碼十分簡(jiǎn)潔強(qiáng)大:
先是工具函數(shù)(保留核心,環(huán)境判斷,錯(cuò)誤警告等邏輯去除):
// 提取函數(shù),大量使用時(shí)有一定性能優(yōu)勢(shì),且簡(jiǎn)明(更重要) 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; }); // 獲取對(duì)象 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); // 所有數(shù)據(jù)的淺拷貝 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 { // 導(dǎo)入所有指令 private commands: Record<string, any> = assign({}, defaultCommands); // 添加擴(kuò)展指令 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 { // 增強(qiáng)健壯性,如果操作命令是函數(shù),修改為 $apply const spec = (typeof $spec === 'function') ? { $apply: $spec } : $spec; // 數(shù)組(數(shù)組) 檢查,報(bào)錯(cuò) // 返回對(duì)象(數(shù)組) let nextObject = object; // 遍歷指令 getAllKeys(spec).forEach((key: string) => { // 如果指令在指令集中 if (hasOwnProperty.call(this.commands, key)) { // 性能優(yōu)化,遍歷過(guò)程中,如果 object 還是當(dāng)前之前數(shù)據(jù) const objectWasNextObject = object === nextObject; // 用指令修改對(duì)象 nextObject = this.commands[key]((spec as any)[key], nextObject, spec, object); // 修改后,兩者使用傳入函數(shù)計(jì)算,還是相等的情況下,直接使用之前數(shù)據(jù) if (objectWasNextObject && this.isEquals(nextObject, object)) { nextObject = object; } } else { // 不在指令集中,做其他操作 // 類似于 update(collection, {2: {a: {$splice: [[1, 1, 13, 14]]}}}); // 解析對(duì)象規(guī)則后繼續(xù)遞歸調(diào)用 update, 不斷遞歸,不斷返回 // ... } }); return nextObject; } }
最后是通用指令:
const defaultCommands = { $push(value: any, nextObject: any, spec: any) { // 數(shù)組添加,返回 concat 新數(shù)組 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) { // 循環(huán) splice 調(diào)用 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) { // 直接替換當(dāng)前數(shù)值 return value; }, $toggle(targets: any, nextObject: any) { const nextObjectCopy = targets.length ? copy(nextObject) : nextObject; // 當(dāng)前對(duì)象或者數(shù)組切換 targets.forEach((target: any) => { nextObjectCopy[target] = !nextObject[target]; }); return nextObjectCopy; }, $unset(value: any, nextObject: any, _spec: any, originalObject: any) { // 拷貝后循環(huán)刪除 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) { // 傳入函數(shù),直接調(diào)用函數(shù)修改 return value(original); }, };
就這樣,作者寫(xiě)了一個(gè)簡(jiǎn)潔而強(qiáng)大的淺拷貝輔助庫(kù)。
Immer 是一個(gè)非常優(yōu)秀的不可變數(shù)據(jù)庫(kù),利用 proxy 來(lái)解決問(wèn)題。不需要學(xué)習(xí)其他 api,開(kāi)箱即用 ( gzipped 3kb )
import produce from "immer" const baseState = [ { todo: "Learn typescript", done: true }, { todo: "Try immer", done: false } ] // 直接修改,沒(méi)有任何開(kāi)發(fā)負(fù)擔(dān),心情美美噠 const nextState = produce(baseState, draftState => { draftState.push({todo: "Tweet about it"}) draftState[1].done = true })
關(guān)于 immer 性能優(yōu)化請(qǐng)參考 immer performance。
該庫(kù)的核心還是在 proxy 的封裝,所以不全部介紹,僅介紹代理功能。
export const objectTraps: ProxyHandler<ProxyState> = { get(state, prop) { // PROXY_STATE是一個(gè)symbol值,有兩個(gè)作用,一是便于判斷對(duì)象是不是已經(jīng)代理過(guò),二是幫助proxy拿到對(duì)應(yīng)state的值 // 如果對(duì)象沒(méi)有代理過(guò),直接返回 if (prop === DRAFT_STATE) return state // 獲取數(shù)據(jù)的備份?如果有,否則獲取元數(shù)據(jù) const source = latest(state) // 如果當(dāng)前數(shù)據(jù)不存在,獲取原型上數(shù)據(jù) if (!has(source, prop)) { return readPropFromProto(state, source, prop) } const value = source[prop] // 當(dāng)前代理對(duì)象已經(jīng)改回了數(shù)值或者改數(shù)據(jù)是 null,直接返回 if (state.finalized_ || !isDraftable(value)) { return value } // 創(chuàng)建代理數(shù)據(jù) if (value === peek(state.base_, prop)) { prepareCopy(state) return (state.copy_![prop as any] = createProxy( state.scope_.immer_, value, state )) } return value }, // 當(dāng)前數(shù)據(jù)是否有該屬性 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) // 如果當(dāng)前有 set 屬性,意味當(dāng)前操作項(xiàng)是代理,直接設(shè)置即可 if (desc?.set) { desc.set.call(state.draft_, value) return true } // 當(dāng)前沒(méi)有修改過(guò),建立副本 copy,等待使用 get 時(shí)創(chuàng)建代理 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) } } // 數(shù)組的代理,把當(dāng)前對(duì)象的代理拷貝過(guò)去,再修改 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]) }
開(kāi)發(fā)過(guò)程中,我們往往會(huì)在 React 函數(shù)中使用 useReducer 方法,但是 useReducer 實(shí)現(xiàn)較為復(fù)雜,我們可以用 useMethods 簡(jiǎn)化代碼。useMethods 內(nèi)部就是使用 immer (代碼十分簡(jiǎn)單,我們直接拷貝 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); } } };
對(duì)比使用 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; } }; };
感謝各位的閱讀,以上就是“如何理解不可變數(shù)據(jù)結(jié)構(gòu)”的內(nèi)容了,經(jīng)過(guò)本文的學(xué)習(xí)后,相信大家對(duì)如何理解不可變數(shù)據(jù)結(jié)構(gòu)這一問(wèn)題有了更深刻的體會(huì),具體使用情況還需要大家實(shí)踐驗(yàn)證。這里是創(chuàng)新互聯(lián),小編將為大家推送更多相關(guān)知識(shí)點(diǎn)的文章,歡迎關(guān)注!
當(dāng)前題目:如何理解不可變數(shù)據(jù)結(jié)構(gòu)
文章路徑:http://aaarwkj.com/article6/pcdjog.html
成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供外貿(mào)建站、品牌網(wǎng)站制作、電子商務(wù)、網(wǎng)站改版、ChatGPT、用戶體驗(yàn)
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來(lái)源: 創(chuàng)新互聯(lián)