溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

如何理解不可變數(shù)據(jù)結(jié)構(gòu)

發(fā)布時(shí)間:2021-10-20 12:42:49 來(lái)源:億速云 閱讀:131 作者:iii 欄目:編程語(yǔ)言

這篇文章主要講解了“如何理解不可變數(shù)據(jù)結(jié)構(gòu)”,文中的講解內(nèi)容簡(jiǎn)單清晰,易于學(xué)習(xí)與理解,下面請(qǐng)大家跟著小編的思路慢慢深入,一起來(lái)研究和學(xué)習(xí)“如何理解不可變數(shù)據(jù)結(jié)構(gòu)”吧!

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ì)于開發(fā)復(fù)雜交互項(xiàng)目的重做等功能很有用)

如何理解不可變數(shù)據(jù)結(jié)構(gòu)

當(dāng)然,由于當(dāng)時(shí)還在重度使用 Vue 進(jìn)行開發(fā),而且 受益于 Vue 本身的優(yōu)化以及業(yè)務(wù)抽象和系統(tǒng)的合理架構(gòu),項(xiàng)目一直保持著良好的性能。同時(shí)該庫(kù)的侵入性和難度都很大,貿(mào)然引入項(xiàng)目也未必是一件好事。

雖然 Immutable 庫(kù)沒有帶來(lái)直接的收益,但從中學(xué)到一些思路和優(yōu)化卻陪伴著我。

淺拷貝 assign 勝任 Immutable

當(dāng)我們不使用任何庫(kù),我們是否就無(wú)法享受不可變數(shù)據(jù)的利好?答案是否定的。

當(dāng)面臨可變性數(shù)據(jù)時(shí)候,大部分情況下我們會(huì)使用深拷貝來(lái)解決兩個(gè)數(shù)據(jù)引用的問題。

const newData = deepCopy(myData);
newData.x.y.z = 7;
newData.a.b.push(9);

不幸的是,深度拷貝是昂貴的,在有些情況下更是不可接受的。深拷貝占用了大量的時(shí)間,同時(shí)兩者之間沒有任何結(jié)構(gòu)共享。但我們可以通過僅復(fù)制需要更改的對(duì)象和重用未更改的對(duì)象來(lái)減輕這種情況。如 Object.assign 或者 ... 來(lái)實(shí)現(xiàn)結(jié)構(gòu)共享。

大多數(shù)業(yè)務(wù)開發(fā)中,我們都是先進(jìn)行深拷貝,再進(jìn)行修改。但是我們真的需要這樣做嗎?事實(shí)并非如此。從項(xiàng)目整體出發(fā)的話,我們只需要解決一個(gè)核心問題 “深層嵌套對(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)說是相當(dāng)高效的(因?yàn)樗皇菧\拷貝,并重用了其余的部分) ,但是編寫起來(lái)卻非常痛苦。

immutability-helper 庫(kù)輔助開發(fā)

immutability-helper (語(yǔ)法受到了 MongoDB 查詢語(yǔ)言的啟發(fā) ) 這個(gè)庫(kù)為 Object.assign 方案提供了簡(jiǎn)單的語(yǔ)法糖,使得編寫淺拷貝代碼更加容易:

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ì)在操作過程中發(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)。

擴(kuò)展命令

我們可以基于當(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)化,遍歷過程中,如果 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);
  },
};

就這樣,作者寫了一個(gè)簡(jiǎn)潔而強(qiáng)大的淺拷貝輔助庫(kù)。

優(yōu)秀的 Immer 庫(kù)

Immer 是一個(gè)非常優(yōu)秀的不可變數(shù)據(jù)庫(kù),利用 proxy 來(lái)解決問題。不需要學(xué)習(xí)其他 api,開箱即用 ( gzipped 3kb )

import produce from "immer"

const baseState = [
  {
    todo: "Learn typescript",
    done: true
  },
  {
    todo: "Try immer",
    done: false
  }
]

// 直接修改,沒有任何開發(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)代理過,二是幫助proxy拿到對(duì)應(yīng)state的值
    // 如果對(duì)象沒有代理過,直接返回
    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)前沒有修改過,建立副本 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ì)象的代理拷貝過去,再修改 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])
}

其他

開發(fā)過程中,我們往往會(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)過本文的學(xué)習(xí)后,相信大家對(duì)如何理解不可變數(shù)據(jù)結(jié)構(gòu)這一問題有了更深刻的體會(huì),具體使用情況還需要大家實(shí)踐驗(yàn)證。這里是億速云,小編將為大家推送更多相關(guān)知識(shí)點(diǎn)的文章,歡迎關(guān)注!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI