不可变数据结构和函数式编程的关系?
函数式编程
- 相对于命令式编程 更看重函数的执行目的而不是执行过程。
- 函数是一等公民 意味着函数优先和提倡函数组合。
- 函数可以和其他数据一样作为参数传递或者作为返回值返回
- 数据不可变性是函数式编程中推崇的重要概念
- 数据不可变在react开发中有着广泛应用:好处在于开发简单,数据可回溯,减少副作用减少bug出现。
数据可变可能会带来的问题?
- javascript中的对象一般是可变的,因为使用了引用赋值,新的对象简单引用了原始对象
- 所以当我们改变了新的对象会影响到原始对象
- 可变数据会有效的利用内存,但是如果随着应用变得越来越复杂 也会造成非常大的隐患
- 为了解决这个问题,我们一般会复制一个新对象,再在新对象上做修改,但是这会造成更多的性能问题和内存浪费。
深拷贝
function deepCopy(obj) {
// 基础数据类型直接返回
if (!obj && typeof obj !== "object") {
return obj;
}
// 如果是dom节点 直接clone一个
if (obj.nodeTpe && "cloneNode" in obj) {
return obj.cloneNode(true);
}
// 如果是Date类型
if (getType(obj) === "date") {
return new Date(obj.getTime());
}
// 如果是RegExp
if (getType(obj) === "regexp") {
return new RegExp(obj.source);
}
const result = isArray(obj)
? []
: obj.constructor
? new obj.constructor()
: {};
for (let key in obj) {
if (Object.prototype.hasOwnProperty(obj, key)) {
result[key] = deepCopy(obj[key]);
}
}
return result;
}
如何解决这个问题?
- 我们一般使用浅拷贝或者深拷贝来处理来防止对象被随意篡改
- 方法有:object.assign,扩展运算符…,Object.freeze等浅拷贝方法
- 但是这些方法只适用于浅层级的处理,对象层级如果比较深,还是需要特殊处理的
- 我们可以使用javascript的一些函数式api如slice,filter,map,reduce配合一些浅拷贝操作
- 来实现数据的不可变,但这无疑会增加开发的难度
- 不可变数据结构immutable.js和immer.js是两种解决方案
- 使用了结构共享 如果对象树中一个节点发生变化,只修改这个节点和受他影响的父节点,其他节点进行共享。
immutable 不可变数据
immutable
三大特性
- 持久化数据结构
- 结构共享
- 惰性操作(懒初始化)
immutable
内部实现了一套完整的 Persistent Data Structure,还有很多易用的数据类型。像 Collection、List、Map、Set、Record、Seq。
immutable
优势
- 降低复杂度
- 节省内存
- 方便回溯
简单使用Map和List:
Map
const immutable = require("immutable");
const assert = require("assert");
let obj1 = immutable.Map({ name: 'benpaoyinfu', age: 8 });
let obj2 = obj1.set('name', 'benpaoyinfu2');
let obj3 = obj2.update('age', x => x + 1);
let obj4 = obj3.merge({ home: '上海' });
console.log(obj1, obj2, obj3, obj4);
let obj6 = immutable.fromJS({ user: { name: 'benpaoyinfu', age: 18 }, 'k': 'v' });
let obj7 = obj6.setIn(['user', 'name'], 'benpaoyinfu');
let obj8 = obj7.updateIn(['user', 'age'], x => x + 1);
let obj9 = obj8.mergeIn(["user"], { home: '上海' });
console.log(obj6, obj7, obj8, obj9);
console.log(obj6.get('user'));
console.log(obj6.getIn(['user', 'name']));
console.log(...obj6.keys());
console.log(...obj6.values());
console.log(...obj6.entries());
var map1 = immutable.Map({ name: 'benpaoyinfu', age: 9 });
var map2 = immutable.Map({ name: 'benpaoyinfu', age: 9 });
assert(map1 !== map2);
assert(Object.is(map1, map2) === false);
assert(immutable.is(map1, map2) === true);
List
let immutable = require('immutable');
let arr1 = immutable.fromJS([1, 2, 3]);
console.log(arr1.size);
let arr2 = arr1.push(4);
console.log(arr2);
let arr3 = arr2.pop();
console.log(arr3);
let arr4 = arr3.update(2, x => x + 1);
console.log(arr4);
let arr5 = arr4.concat([5, 6]);
console.log(arr5);
let arr6 = arr5.map(item => item * 2);
console.log(arr6);
let arr7 = arr6.filter(item => item >= 10);
console.log(arr7);
console.log(arr7.get(0));
console.log(arr7.includes(10));
console.log(arr7.last());
let val = arr7.reduce((val, item) => val + item, 0);
console.log(val);
console.log(arr7.count());
immer不可变数据结构
immer 是 mobx 的作者写的一个 immutable 库
核心实现是利用 ES6 的 proxy,几乎以最小的成本实现了 js 的不可变数据结构
对 draftState 的修改都会反应到 nextState 上
而 Immer 使用的结构是共享的,nextState 在结构上又与 currentState 共享未修改的部分
import { produce } from 'immer';
let baseState = {}
let nextState = produce(baseState, (draft) => {})
console.log(baseState===nextState);
import { produce } from 'immer';
let baseState = {
ids: [1],
pos: {
x: 1,
y: 1
}
}
let nextState = produce(baseState, (draft) => {
draft.ids.push(2);
})
console.log(baseState.ids === nextState.ids);//false
console.log(baseState.pos === nextState.pos);//true
immer.js核心实现
- 兼容对象,函数和数组
// 创建草稿对象
function createDraftState(baseState){
if(Array.isArray(baseState)){
return [...baseState]
}else if(Object.prototype.toString.call(baseState)){
return Object.assign({}, baseState)
}else{
return baseState;
}
}
// 生产方法
function produce(baseState, producer){
const proxy = toProxy(baseState);
producer(proxy);
const internal = proxy[INTERNAL];
internal.mutated?internal.draftStat:baseState
}
// 创建代理
const INTERNAL = Symbol('internal');
function toProxy(baseState, parentChange){
let keyProxy = {};
let internal = {
draftState: createDraftState(),
keyProxy,
mutated: false
}
return new Proxy(baseState, {
get(target, key){
if(key === INTERNAL){
return internal;
}
const value = target[key];
if(Object.prototype.toString.call(value) === '[object Object]'
|| Array.isArray(value)){
if(key in keyProxy){
return keyProxy[key]
}else{
keyProxy[key] = toProxy(value, () => {
internal.mutated = true;
const childProxy = keyProxy[key];
const { draftState } = childProxy[INTERNAL];
internal.draftState[key] = draftState;
parentChange&&parentChange()
})
return keyProxy[key]
}
}else if(typeof value === 'function'){
internal.mutated = true;
parentChange&&parentChange();
return value.bind(internal.draftState)
}
return internal.mutated ? internal.draftState[key]: baseState[key]
},
set(target, key, value){
internal.mutated = true;
const { draftState } = internal;
for(const key in target){
draftState[key] = key in draftState ? draftState[key] : target[key];
}
draftState[key] = value;
parentChange&&parentChange();
return true;
}
})
}
hook版实现
- queueMicrotask用来帮我们执行微任务
- 当我们期望某段代码不阻塞当前执行的同步代码,同时又期望它尽可能快地执行时,我们可能会使用到queueMicrotask
import { useState, useRef } from 'react';
import { toProxy, INTERNAL } from './core';
import * as is from './is';
function useImmerState(baseState) {
const [state, setState] = useState(baseState);
const draftRef = useRef(toProxy(baseState, () => {
queueMicrotask(()=>{
const internalState = draftRef.current[INTERNAL];
const newState = internalState.draftState;
setState(() => {
return (is.isArray(newState) ? [...newState] : Object.assign({}, newState));
});
})
}));
const updateDraft = (producer) => producer(draftRef.current);
return [state, updateDraft];
}
export default useImmerState;