初识Immutablejs菜鸟总结包含JS的基本数据类型和引用数据类型JS的深拷贝和浅拷贝

这几天公司的React项目中有用到Immutable,自己在对页面中的数据处理上也用到了Immutable,网上查阅相关资料后,自己做了一些实用的总结吧,参考过的一些不错的文章有:Immutable 详解及 React 中实践,Immutable 常用API简介,Immutable官方文档 1.Immutable是什么?mmutable 数据就是一旦创建,就不能再被更改的数据。对 Immutable 对象的任何修改或添加删除操作都会返回一个新的 Immutable 对象。Immutable 实现的原理是 Persistent Data Structure [psstnt det strkt(r)] (持久化数据结构),也就是使用旧数据创建新数据时,要保证旧数据同时可用且不变。同时为了避免 deepCopy 把所有节点都复制一遍带来的性能损耗,Immutable 使用了 Structural Sharing(结构共享),即如果对象树中一个节点发生变化,只修改这个节点和受它影响的父节点,其它节点则进行共享。2.为什么要使用Immutable JavaScript 中的对象(object)、数组(Array)、函数(Function)一般是可变的(Mutable),因为使用了引用赋值,新的对象简单的引用了原始对象,改变新的对象将影响到原始对象。。举个例子red={a:1}; yellow = red;如果进行yellow.a=2的操作,你会发现red中的a也变成了2。可以参考:详解JS中的基本数据类型和引用数据类型 ,JavaScript的基本数据类型和引用数据类型虽然这样做可以节约内存,但当应用复杂后,这就造成了非常大的隐患,Mutable 带来的优点变得得不偿失。为了解决这个问题,一般的做法是使用 shallowCopy(浅拷贝)或 deepCopy(深拷贝)来避免被修改,但这样做造成了 CPU 和内存的浪费。可以参考:JavaScript 深拷贝(deep copy)和浅拷贝(shallow copy)3.Immutable的数据类型 常用的有两种数据类型分别是:List: 有序索引集,类似JavaScript中的Array。Map: 无序索引集,类似JavaScript中的Object。下面的用的比较少,很少接触:OrderedMap: 有序的Map,根据数据的set()进行排序。Set: 没有重复值的集合。OrderedSet: 有序的Set,根据数据的add进行排序。Stack: 有序集合,支持使用unshift()和shift()添加和删除。Range(): 返回一个Seq.Indexed类型的集合,这个方法有三个参数,start表示开始值,默认值为0,end表示结束值,默认为无穷大,step代表每次增大的数值,默认为1.如果start = end,则返回空集合。Repeat(): 返回一个vSeq.Indexe类型的集合,这个方法有两个参数,value代表需要重复的值,times代表要重复的次数,默认为无穷大。Record: 一个用于生成Record实例的类。类似于JavaScript的Object,但是只接收特定字符串为key,具有默认值。Seq: 序列,但是可能不能由具体的数据结构支持。Collection: 是构建所有数据结构的基类,不可以直接构建。4.immutable常用Api:Immutable 常用API简介,Immutable文档 参考:

June 15, 2019 · 1 min · jiezi

新的-Vue-Functionbased-API-当中的看到的-Clojure-的影子

这次 Vue 大会看到了 Vue 新的 API 设计, 中间有一些觉得眼熟的写法,后面也看到了工业聚的一些解读, 大致知道是什么样的用法吧..当然现场演讲过 Vue 具体实现的优化是更复杂的, 比这个 API 要多.. Vue.js作者在VueConf的演讲视频出炉Vue Function-based API RFC 中文版本揭秘Vue-3.0最具潜力的API其中比较让我觉得眼熟的是 value(0) 还有特别是 state({count: 0}) 的用法, function useMouse() { const x = value(0) const y = value(0) const update = e => { x.value = e.pageX y.value = e.pageY } onMounted(() => { window.addEventListener('mousemove', update) }) onUnmounted(() => { window.removeEventListener('mousemove', update) }) return { x, y }}value() 返回的是一个 value wrapper (包装对象)。一个包装对象只有一个属性:.value ,该属性指向内部被包装的值。这是因为当包装对象被暴露给模版渲染上下文,或是被嵌套在另一个响应式对象中的时候,它会被自动展开 (unwrap) 为内部的值。 ...

June 12, 2019 · 4 min · jiezi

React项目集成Immutablejs

1、前言本文章项目的依赖包及其版本如下: Package NameVersionantd^3.16.6connected-react-router^6.4.0customize-cra^0.2.12immutable^4.0.0-rc.12react^16.8.6react-app-rewired^2.1.1react-redux^7.0.3react-router-config^5.0.0react-router-dom^5.0.0react-scripts3.0.1redux^4.0.1redux-logger^3.0.6redux-persist^5.10.0redux-persist-expire^1.0.2redux-persist-transform-immutable^5.0.0redux-saga^1.0.22、准备工作,搭建项目下面是我的项目结构,每个人或者每个公司都有自己的目录架构,这里我的只供大家参考,另外搭建项目过程和介绍如何使用immutable.js不是本文章的重点,如何使用immutable.js以及本文章相关代码后面我会给出,如果有疑问欢迎大家在下面留言 |-- App.js|-- index.js|-- serviceWorker.js|-- assets| |-- audio| |-- css| | |-- App.scss| | |-- base.scss| | |-- index.css| | |-- override-antd.scss| |-- image| | |-- Welcome.png| | |-- awbeci.png| | |-- bgLogo.png| | |-- hiy_logo.png| | |-- indexPop1.png| | |-- indexPop2.png| | |-- logo.png| | |-- logoX.png| | |-- right.png| |-- video|-- components| |-- HOC| | |-- loading.js| |-- common| |-- layout| |-- AppRoute.js| |-- LayoutPage.js| |-- Loading.js| |-- MasterPage.js| |-- RouterView.js| |-- SideMenu.js| |-- layoutPage.scss| |-- masterPage.scss|-- config| |-- base.conf.js|-- context| |-- themeContext.js|-- pages| |-- DepartmentManage.js| |-- Index.js| |-- NoFound.js| |-- NoPermission.js| |-- UserManage.js| |-- login| |-- Login.js| |-- login.scss|-- redux| |-- actions| | |-- authAction.js| | |-- layoutPageAction.js| |-- middleware| | |-- authTokenMiddleware.js| |-- reducers| | |-- authReducer.js| | |-- index.js| | |-- layoutPageReducer.js| |-- sagas| | |-- authSaga.js| | |-- index.js| |-- store| | |-- index.js| |-- thunks|-- router| |-- index.js|-- service| |-- apis| | |-- 1.0| | |-- index.js| | |-- urls.js| |-- mocks| | |-- 1.0| | |-- index.js| | |-- testMock.js| |-- request| |-- ApiRequest.js| |-- MockRequest.js|-- test| |-- App.test.js|-- utils3、集成immutable.js此项目除了依赖包要配置之外,只有redux下的reducer相关文件会设置成immutable.js普通的react组件我没有设置成immutable.js ...

June 1, 2019 · 4 min · jiezi

Immutablejs-Object-Formatter使用

1、什么是Immutable.js Object Formatter?Immutable.js Object Formatter 是一款格式化immutable.jsChrome插件,极大方便开发人员操作immutable.js。 2、到chrome 网上应用店安装 3、启用之后,点击设置 4、启用Enable custom formatters 5、查看代码已经被格式化 参考:1.Custom Formatters for Immutable.js2.Custom Object Formatters in Chrome DevTools3.immutable-devtools4.Immutable.js Object Formatter 1.9.2 CRX for Chrome

May 31, 2019 · 1 min · jiezi

immutable.js在react项目中的简单使用

immutable.jsimmutable可以将一个对象转为一个不可更改的对象。对 Immutable 对象的任何修改或添加删除操作都会返回一个新的 Immutable 对象。fromJSheader/store/reducer.js import { fromJS } from ‘immutable’; const defaultState = fromJS({ data: {}, list: [] }) export default (state = defaultState, action) => { switch (action.type) { case ‘change_data’: return state.set(“data”, action.data); case ‘change_all’: return state.merge({ list: action.data, data: action.data }); default: return state; } };index.js—–组件 const mapStateToProps = (state) => { return { // focused: state.get(“header”).get(“focused”) focused: state.getIn([‘header’, ‘data’]) } };store/reducer.js import { combineReducers } from “redux-immutable”; import { reducer as headerReducer } from ‘../common/header/store’; const reducer = combineReducers({ header: headerReducer }); export default reducer; ...

April 16, 2019 · 1 min · jiezi

Immutable.js 源码解析 --Map 类型

上一片文章介绍的是 List 结构。那对于 Map 结构又要如何处理,没有 List 结构的索引,那怎么办呢? 我们把键名变为哈希值就可以啦~HAMT:Hash Arrey Mapped Trie 。这个结构就是Map中所用到的。immutable中的hash计算核心代码如下:function hashString(string) { // This is the hash from JVM // The hash code for a string is computed as // s[0] * 31 ^ (n - 1) + s[1] * 31 ^ (n - 2) + … + s[n - 1], // where s[i] is the ith character of the string and n is the length of // the string. We “mod” the result to make it between 0 (inclusive) and 2^31 // (exclusive) by dropping high bits. let hashed = 0; for (let ii = 0; ii < string.length; ii++) { hashed = (31 * hashed + string.charCodeAt(ii)) | 0; } return smi(hashed);}// v8 has an optimization for storing 31-bit signed numbers.// Values which have either 00 or 11 as the high order bits qualify.// This function drops the highest order bit in a signed number, maintaining// the sign bit.export function smi(i32) { return ((i32 >>> 1) & 0x40000000) | (i32 & 0xbfffffff);}上面只是一个计算hash值的函数,讨论的重点再下面呢。一、Map 的结构先看看Map的结构:function makeMap(size, root, ownerID, hash) { const map = Object.create(MapPrototype); map.size = size; map._root = root; map.__ownerID = ownerID; map.__hash = hash; map.__altered = false; return map;}和list不同,Map中没有_tail,所有的数据都在_root里面。再Map里,我们要分几种情况:1、当键值对不超过8个2、当键值对超过8个小于16个3、当键值对超过16个4、hash冲突怎么办用下面这段代码调试看看每种情况的结构: let jsObj = {}; for (let i = 0; i < 8; i++) { jsObj[Math.random()] = Math.random(); } let map1 = Immutable.Map(jsObj); console.log(map1);二、ArrayMapNode当键值对不超过8个的时候采用的是这个结构。这种方式是最简单的,所有键值对保存在 entries 里面。同时 get/set 方法都较为简单,直接遍历一下获取就好了。从图中我们可以看到,immutable把key和value转换为一个数组的arr[0]和arr[1]来存储。ArrayMapNode.prototype.update: update(ownerID, shift, keyHash, key, value, didChangeSize, didAlter) { ….. const entries = this.entries; let idx = 0; const len = entries.length; for (; idx < len; idx++) { // 寻找需要更新的值 if (is(key, entries[idx][0])) { break; } } const exists = idx < len;//是否存在这个key …… // 判断个数是否大于8 MAX_ARRAY_MAP_SIZE的值为8 if (!exists && !removed && entries.length >= MAX_ARRAY_MAP_SIZE) { return createNodes(ownerID, entries, key, value); } …… const newEntries = isEditable ? entries : arrCopy(entries); if (exists) { if (removed) { idx === len - 1 ? newEntries.pop() : (newEntries[idx] = newEntries.pop()); } else { newEntries[idx] = [key, value];//存在就直接把值赋值给idx的位置 } } else { newEntries.push([key, value]);//不存在 就是新增 push一个值进去 } …… return new ArrayMapNode(ownerID, newEntries); }}上面代码中MAX_ARRAY_MAP_SIZE的定义:const MAX_ARRAY_MAP_SIZE = SIZE / 4;const MAX_BITMAP_INDEXED_SIZE = SIZE / 2;const MIN_HASH_ARRAY_MAP_SIZE = SIZE / 4;export const SIZE = 1 << SHIFT;三、BitmapIndexedNode当键值对超过8个小于16个的时候采用的是这个结构。BitmapIndexedNode 的子节点是 ValueNode或者BitmapIndexedNode。在 BitmapIndexedNode 里面查/增/改元素,都需要用到 bit-map(位图)算法,BitmapIndexedNode.bitmap 存储的是键名和存储顺序的位图信息。例如 get 方法,通过 BitmapIndexedNode.bitmap,以及 key 名就可以获取该键值对的排列序号,从而获取到相应的 ValueNode。BitmapIndexedNode.prototype.update:update(ownerID, shift, keyHash, key, value, didChangeSize, didAlter) { if (keyHash === undefined) { keyHash = hash(key);//如果没有hash值计算hash值 } // 根据BitmapIndexedNode中存储的bitmap判断当前传入的key是否在某个位置已经存在。bitmap用32 位(二进制)来记录元素是否存在。1表示存在,0表示不存在。 // 例如:keyHash 转换为二进制后为11101110000110000001101001101 ,每5位为一组,shift假定为5 // (keyHash >>> shift)& MASK 取出需要的5位。第一次取到从低位开始的第一个5位:01101。keyHashFrag的十进制的值为13 // 1 << keyHashFrag 除第13位(从0开始)外,其他位都为0 即:10000000000000 // bit & bitmap 得出bitmap的第13位是否为1 // eg:bitmap=010101110111010000101011100010100 即 010101110111010000111011100010100 & 10000000000000 发现第13位为1 则存在这个元素 const keyHashFrag = (shift === 0 ? keyHash : keyHash >>> shift) & MASK; const bit = 1 << keyHashFrag; const bitmap = this.bitmap; const exists = (bitmap & bit) !== 0; …… // 计算1的数量,即算出key在BitmapIndexedNode的存储位置 // eg:bitmap=010101110111010000111011100010100 // bitmap & (bit - 1) 即 010101110111010000111011100010100 & 1111111111111 = 1011100010100 // 使用 popCount 函数计算有多少个1 计算出 有 6 个 1 // 即 idx = 6 // 所以我们需要查找的元素在BitmapIndexedNode的存储位置为6 const idx = popCount(bitmap & (bit - 1)); // 如果这个位置有数据,取出当前BitmapIndexedNode中对应的数据,如果不存在,置为undefined const nodes = this.nodes; const node = exists ? nodes[idx] : undefined; const newNode = updateNode( node, ownerID, shift + SHIFT, keyHash, key, value, didChangeSize, didAlter ); if (newNode === node) { return this; } // 判断是否超过16 if (!exists && newNode && nodes.length >= MAX_BITMAP_INDEXED_SIZE) { return expandNodes(ownerID, nodes, bitmap, keyHashFrag, newNode); } …… // 生成新的Bitmap const newBitmap = exists ? (newNode ? bitmap : bitmap ^ bit) : bitmap | bit; // 生成新的nodes // eg:exists=false, idx=1情况: // oldArray: [vA, undefind, vC, vD] // newArray: [vA, newVNode, vC, vD] // exits=true情况,idx=8 // 原来位置8指向新生成的BitmapIndexedNode const newNodes = exists ? newNode ? setAt(nodes, idx, newNode, isEditable) : spliceOut(nodes, idx, isEditable) : spliceIn(nodes, idx, newNode, isEditable); if (isEditable) { this.bitmap = newBitmap; this.nodes = newNodes; return this; } return new BitmapIndexedNode(ownerID, newBitmap, newNodes); }}这里我把popCount的源码也贴出来:function popCount(x) { x -= (x >> 1) & 0x55555555; x = (x & 0x33333333) + ((x >> 2) & 0x33333333); x = (x + (x >> 4)) & 0x0f0f0f0f; x += x >> 8; x += x >> 16; return x & 0x7f;}四、HashArrayMapNode当键值对超过16个采用的是这个结构。HashArrayMapNode 的亲子元素可以是 HashArrayMapNode/BitmapIndexedNode/ValueNode。由此看来巨量的键值对,将有 HashArrayMapNode/BitmapIndexedNode/ValueNode 组合而成,而每个 HashArrayMapNode 最多有32个亲子元素,BitmapIndexedNode 最多有16个亲子元素。 HashArrayMapNode 类对应带的 count,代表其子元素的数量。当需要读取的时候,直接键名的哈希值,就能够实现了来一个庞大点的数据:HashArrayMapNode.prototype.update: update(ownerID, shift, keyHash, key, value, didChangeSize, didAlter) { if (keyHash === undefined) { keyHash = hash(key); } // 计算当前这个层级的idx const idx = (shift === 0 ? keyHash : keyHash >>> shift) & MASK; const removed = value === NOT_SET; const nodes = this.nodes; const node = nodes[idx]; …… const newNode = updateNode( node, ownerID, shift + SHIFT, keyHash, key, value, didChangeSize, didAlter ); …… if (isEditable) { this.count = newCount; this.nodes = newNodes; return this; } return new HashArrayMapNode(ownerID, newCount, newNodes); }}function updateNode( node, ownerID, shift, keyHash, key, value, didChangeSize, didAlter) { //没有子节点了,即找到了这个值 if (!node) { if (value === NOT_SET) { return node; } SetRef(didAlter); SetRef(didChangeSize); return new ValueNode(ownerID, keyHash, [key, value]); } // 当还有子节点,则继续递归查找 return node.update( ownerID, shift, keyHash, key, value, didChangeSize, didAlter );}五、HashCollisionNode虽然说hash冲突的情况是很少的,但是也有这种情况出现的。比如 key = ‘Aa’ key = ‘BB’,其哈希值是完全一样的,这个时候就会启动 HashCollisionNode 对象,将相同的哈希值的对象都放在同一个 HashCollisionNode 里面,而这里面就是简单的线性读写数组了,没有之前的 Bitmapped 操作,毕竟一次性不可能有太多相同哈希值的键名出现。 ...

November 24, 2018 · 4 min · jiezi

Immutable.js 源码解析 --List 类型

一、存储图解我以下面这段代码为例子,画出这个List的存储结构:let myList = [];for(let i=0;i<1100;i++) { myList[i] = i;}debugger;//可以在这里打个断点调试let immutableList = Immutable.List(myList)debugger;console.log(immutableList.set(1000, ‘Remm’));debugger;console.log(immutableList.get(1000));二、vector trie 的构建过程我们用上面的代码为例子一步一步的解析。首先是把原生的list转换为inmutable的list 类型:export class List extends IndexedCollection { // @pragma Construction constructor(value) { // 此时的value就是上面的myList数组 const empty = emptyList(); if (value === null || value === undefined) {//判断是否为空 return empty; } if (isList(value)) {//判断是否已经是imutable的list类型 return value; } const iter = IndexedCollection(value);//序列化数组 const size = iter.size; if (size === 0) { return empty; } assertNotInfinite(size); if (size > 0 && size < SIZE) { // 判断size是否超过32 return makeList(0, size, SHIFT, null, new VNode(iter.toArray())); } return empty.withMutations(list => { list.setSize(size); iter.forEach((v, i) => list.set(i, v)); }); } 。。。。。。}首先会创建一个空的listlet EMPTY_LIST;export function emptyList() { return EMPTY_LIST || (EMPTY_LIST = makeList(0, 0, SHIFT));}SHIFT的值为5,export const SHIFT = 5; // Resulted in best performance after ______?再继续看makeList,可以清晰看到 List 的主要部分:function makeList(origin, capacity, level, root, tail, ownerID, hash) { const list = Object.create(ListPrototype); list.size = capacity - origin;// 数组的长度 list._origin = origin;// 数组的起始位置 一般是0 list._capacity = capacity;// 数组容量 等于 size list._level = level;//树的深度,为0时是叶子结点。默认值是5,存储指数部分,用于方便位运算,增加一个深度,level值+5 list._root = root;// trie树实现 list._tail = tail;// 32个为一组,存放最后剩余的数据 其实就是 %32 list.__ownerID = ownerID; list.__hash = hash; list.__altered = false; return list;}将传入的数据序列化// ArraySeqiter = {size: 数组的length,_array: 传入数组的引用}判断size是否超过32,size > 0 && size < SIZE 这里 SIZE : export const SIZE = 1 << SHIFT;即 32。若没有超过32,所有数据都放在_tail中。_root 和 _tail 里面的数据又有以下结构:// @VNode classconstructor(array, ownerID) { this.array = array; this.ownerID = ownerID;}可以这样调试查看:let myList = [];for(let i=0;i<30;i++) { myList[i] = i;}debugger;//可以在这里打个断点调试console.log(Immutable.List(myList));size如果超过32return empty.withMutations(list => { list.setSize(size);//构建树的结构 主要是计算出树的深度 iter.forEach((v, i) => list.set(i, v));//填充好数据});export function withMutations(fn) { const mutable = this.asMutable(); fn(mutable); return mutable.wasAltered() ? mutable.__ensureOwner(this.__ownerID) : this;}list.setSize(size)中有一个重要的方法setListBounds,下面我们主要看这个方法如何构建这颗树这个方法最主要的作用是 确定 list的levelfunction setListBounds(list, begin, end) { …… const newTailOffset = getTailOffset(newCapacity); // New size might need creating a higher root. // 是否需要增加数的深度 把 1 左移 newLevel + SHIFT 位 相当于 1 * 2 ^ (newLevel + SHIFT) // 以 size为 1100 为例子 newTailOffset的值为1088 第一次 1088 > 2 ^ 10 树增加一层深度 // 第二次 1088 < 2 ^ 15 跳出循环 newLevel = 10 while (newTailOffset >= 1 << (newLevel + SHIFT)) { newRoot = new VNode( newRoot && newRoot.array.length ? [newRoot] : [], owner ); newLevel += SHIFT; } ……}function getTailOffset(size) { // (1100 - 1) / 2^5 % 2^5 = 1088 return size < SIZE ? 0 : (((size - 1) >>> SHIFT) << SHIFT);}经过 list.setSize(size);构建好的结构三、set 方法iter.forEach((v, i) => list.set(i, v));这里是将iter中的_array填充到list这里主要还是看看set方法如何设置数据set(index, value) { return updateList(this, index, value);}function updateList(list, index, value) { …… if (index >= getTailOffset(list._capacity)) { newTail = updateVNode(newTail, list.__ownerID, 0, index, value, didAlter); } else { newRoot = updateVNode( newRoot, list.__ownerID, list._level, index, value, didAlter ); } ……}function updateVNode(node, ownerID, level, index, value, didAlter) { // 根据 index 和 level 计算 数据set的位置在哪 const idx = (index >>> level) & MASK; // 利用递归 一步一步的寻找位置 直到找到最终的位置 if (level > 0) { const lowerNode = node && node.array[idx]; const newLowerNode = updateVNode( lowerNode, ownerID, level - SHIFT, index, value, didAlter ); …… // 把node节点的array复制一份生成一个新的节点newNode editableVNode函数见下面源码 newNode = editableVNode(node, ownerID); // 回溯阶段将 子节点的引用赋值给自己 newNode.array[idx] = newLowerNode; return newNode; } …… newNode = editableVNode(node, ownerID); // 当递归到叶子节点 也就是level <= 0 将值放到这个位置 newNode.array[idx] = value; …… return newNode;}function editableVNode(node, ownerID) { if (ownerID && node && ownerID === node.ownerID) { return node; } return new VNode(node ? node.array.slice() : [], ownerID);}下面我们看看运行了一次set(0,0)的结果整个结构构建完之后下面我们接着看刚刚我们构建的list set(1000, ‘Remm’),其实所有的set的源码上面已经解析过了,我们再来温习一下。调用上面的set方法,index=1000,value=‘Remm’。调用updateList,继而调用updateVNode。通过const idx = (index >>> level) & MASK;计算要寻找的节点的位置(在这个例子中,idx的值依次是0->31->8)。 不断的递归查找,当 level <= 0 到达递归的终止条件,其实就是达到树的叶子节点,此时通过newNode = editableVNode(node, ownerID);创建一个新的节点,然后 newNode.array[8] = ‘Remm’。接着就是开始回溯,在回溯阶段,自己把自己克隆一个,newNode = editableVNode(node, ownerID);,注意这里克隆的只是引用,所以不是深拷贝。然后再将idx位置的更新了的子节点重新赋值,newNode.array[idx] = newLowerNode;,这样沿着路径一直返回,更新路径上的每个节点,最后得到一个新的根节点。更新后的list:四、get 方法了解完上面的list构建和set,我们再来看 immutableList.get(1000) 源码就是小菜一碟了。 get(index, notSetValue) { index = wrapIndex(this, index); if (index >= 0 && index < this.size) { index += this._origin; const node = listNodeFor(this, index); return node && node.array[index & MASK]; } return notSetValue; }function listNodeFor(list, rawIndex) { if (rawIndex >= getTailOffset(list._capacity)) { return list._tail; } if (rawIndex < 1 << (list._level + SHIFT)) { let node = list._root; let level = list._level; while (node && level > 0) { // 循环查找节点所在位置 node = node.array[(rawIndex >>> level) & MASK]; level -= SHIFT; } return node; }}五、tire 树 的优点来一张从网上盗来的图:这种树的数据结构(tire 树),保证其拷贝引用的次数降到了最低,就是通过极端的方式,大大降低拷贝数量,一个拥有100万条属性的对象,浅拷贝需要赋值 99.9999万次,而在 tire 树中,根据其访问的深度,只有一个层级只需要拷贝 31 次,这个数字不随着对象属性的增加而增大。而随着层级的深入,会线性增加拷贝数量,但由于对象访问深度不会特别高,10 层已经几乎见不到了,因此最多拷贝300次,速度还是非常快的。我上面所解析的情况有 构建、修改、查询。其实还有 添加 和 删除。 其实Immutable.js 部分参考了 Clojure 中的PersistentVector的实现方式。所以可以看看下面这篇文章:https://hypirion.com/musings/… ...

November 24, 2018 · 4 min · jiezi

immutability因React官方出镜之使用总结分享!

引言 之前项目中遇到数据拷贝、引用之间数据层级嵌套过深,拷贝的值相互之间影响的问题,后来引入了immutability-helper,使用过程中的一些总结,跟大家分享下,至于为什么不是immutable,请看下文分解,这里是@IT·平头哥联盟,我是首席填坑官——苏南。 相信大家在面试/工作中都遇到过js对象/数组的拷贝问题,面试官问你,你一般怎么做??在现在ES6盛行的当下,不会一点ES6都不好意思说自己是前端(其实我一般都说自己是攻城狮、切图崽????),我们想的大多第一想法,如下:Object.assign - 最方便;[…] - 最有逼格;JSON.parse、JSON.stringify - 完美组合;$.extend() - jQuery时代的引领潮流时尚前沿的API;最后想到的才是自己递归实现一个; 但是通常我们使用的Object.assign属于浅拷贝,当数据嵌套层级较深时,就……呵呵了;而JSON.parse、stringify它应该是创建一个临时可能很大的字符串,然后又访问解析器,性能是比较慢的。于是后来发现了 immutable「不可变数据」,曾经我也一度特别喜欢它,但时间久了,慢慢发现,它过于有个性了些、凡事都都没有任何商量的余地,所有的数据,从创建、变更、插入、删除等操作,都要按它的套路来,对于我这种一生放荡不羁爱自由的人来说,长时间的约束,是不能忍的;都说两人如果三观不合,是无法长久下去的,可能也是缘份吧,在后来的某一天偶然的闲逛中邂逅了新欢 ————Immutability Helpers。 嗯,今天的主题就是给大家分享一下,Immutability Helpers的一些用法,会介绍API的使用操作和小技巧,如有不理解不对,请纠正: 太兴奋了,差点忘了,补充一下,一个简单的拷贝: //实现一个简单的递归数据拷贝 let customClone = (rawObj)=>{ let copyObj = {}; for (var key in rawObj) { if( typeof rawObj[key] === ‘object’ && Object.prototype.toString.call(rawObj[key]) !== ‘[object Array]’){ copyObj[key] = customClone(rawObj[key]); }else{ copyObj[key] = rawObj[key]; }; }; return copyObj; }; let objA = {“name”:“苏南”,“sex”:“男”,“height”:“176”}; let objB = customClone(objA); objB.signature = “宝剑锋从磨砺出,梅花香自苦寒来,做有温度的攻城狮”; console.log(objA); console.log(objB);补充一个 Object.assign 的坑 : let data = { a:1, b:2, children:{ name:“苏南”, organization:"@IT·平头哥联盟", job:“首席填坑官”, address:“ShenZhen”, age:18 } }; let data2 = Object.assign({},data); data2.children.age = 28; data2.children.job = “首席甩锅官”; data2.b = 666; console.log(“我是原始数据 data:",data); console.log(“我是复制后的数据 data2:",data2);immutable 最后的一次回顾 都说有了新欢,忘了旧爱,但我不是那种无情无义的人,最后正式介绍一下 immutable,为我俩的……画上一个圆满的句号: 再次强调,并不是觉得immutable不好,不够强大,只是自己个人观点,有些不喜欢而已,各位immutable粉勿喷,想了解更多的同学可以点击这里Immutable data encourages pure functions (data-in, data-out) and lends itself to much simpler application development and enabling techniques from functional programming such as lazy evaluation.使用示例: const list1 = List([ 1, 2, 3 ]); const list2 = List([ 4, 5, 6 ]); const array = [ 7, 8, 9 ]; const list3 = list1.concat(list2, array); console.log(list3) // List {size: 9, _origin: 0, _capacity: 9, _level: 5, _root: null, …} 是不能直接获取到数据的,须使用get,– list3.get(0) let data = fromJS({ obj:{} }); let data1 = { a:1, b:2, children:{ name:“苏南”, } }; let data2 = data.mergeIn([‘obj’],data1,{c:666}); console.log(“获取的数据:",data2.getIn([‘obj’,‘c’])); console.log(“这里是由formJS创建的数据:",data2.getIn([‘obj’,‘children’,’name’]));//使用immutable后,所有数据都要类似选择器,一个一个往下选择,并不是说它不好、功能不够强大,只是自己有些不喜欢它类似JQuery选择器一样的语法,get、getIn、set、List等的使用方式,当然它也是可以使用 toJS方法转回来的。Immutability Helpers出场gitHub上它对自己的介绍很简单:Mutate a copy of data without changing the original source —— 在不更改原始源的情况下改变数据副本。 与它结缘,是因为它在react官方文档中出镜,而被我所宠幸,真的 ,只是因为在人群中多看了它一眼再也没能忘掉, 它跟immutable不一样,不会有那么多条条框框约束你,给你自由、给你独立的空间、给你独立的思想,让你想用即用、用之即走~~(泥马,怎么有点像张小龙说它的小程序一样????),但您放心,它的坑真的比小程序少,API也很简洁,接下来来看一下,它的基本用法:$push —— 数组;$unshift —— 数组;$splice —— 数组;$set —— 替换/覆盖/合并原数据;$toggle —— array of strings ,toggles a list of boolean fields from the target object;$unset —— remove the list of keys in array from the target object;$merge —— 合并对象;$apply —— passes in the current value to the function and updates it with the new returned value;$add —— 新增;$remove —— 删除。以上基本就是它全部的API了,下面一起来看看,具体用法吧:$push 的使用 :看名字就知道它的作用了啦,跟原生的push一样,不过写法有一点点不一样; let arr = [1,2,3,4,5,66]; let arr2 = update(arr,{ $push : [“a”,“b”,“c”], //一定要 []号的形式哦,不可以 “a”; [4]:{ // !!index ,可以指定修改下标的值 $set:“我是替换过的” } }); console.log(arr2);$unshift 的使用 :一样,跟原生的unshift,在原数组开头处插入,同样写法是以一个数组的形式; let arr = [1,2,3,4,5,66]; let arr2 = update(arr,{ $unshift : [“a”,“b”,“c”], [4]:{ $set:“我是首席填坑官∙苏南” //这里需要注意,它的操作是在 unshift之前执行的,也就是在原 arr 上查找 第4个下标 } }); console.log(“原始数组”,arr);// [1, 2, 3, 4, 5, 66] 相互之间并不会影响 console.log(arr2); //[“a”, “b”, “c”, 1, 2, 3, 4, “我是首席填坑官∙苏南”, 66]$splice 的使用 :注意 :数组套数组,start,end, 插入的数据……,; let arr = [1,2,3,4,5,66]; let arr2 = update(arr,{ $splice : [[1,2,[66788,99],{a:123,b:“苏南”}]], // or [0,1,“从我开始是插入的内容”,88,89,90,“后面可以很多,是数组、对象、字符串都行”] }); console.log(arr2); //复杂一些的用法: let obj={ name:“immutable”, list :[1,2,[90,55,44,3,22,55],3,4,6,7,8] }; let obj2 = update(obj,{ list:{ [2]:value=>update(value,{ $splice:[[0,2]] // [90,55,44,3,22,55] => [44, 3, 22, 55] }) } });$set 的使用 :上面已经演示过了,其实有点替换的意思,当有重复的值时,就会覆盖,没有就新增,来展示复杂一点的场景,层级深的数据,也不会相互影响; let obj={ name:“immutable”, children:{ address:“ShenZhen”, hobby:"@IT·平头哥联盟-前端开发” } }; let obj2 = update(obj,{ $set : {name:“immutability-helper”,other:“其他字段,如微信公众号:honeyBadger8,每周为你带来最新分享”} }); let obj3 = update(obj,{ name:{ $set : “苏南” }, children:{ hobby:{ $set:“首席填坑官 - javascript” } } }); console.log(“原始数据:",obj); console.log(“obj2:",obj2); console.log(“obj3”,obj3); $toggle 的使用:听名字,应该就能猜出来,开关切换的意思;Boolean 布尔值的切换,如果你是强制要 Number 类型 的 0、1,那么使用引方法的时候就要注意了; let obj={ name:“immutable”, a:false, b:true, c:1, d:0 }; let obj2 = update(obj,{ $toggle:[‘b’,‘a’,“c”,“d”], }); console.log(“原始数据:",obj); console.log(“obj2:",obj2);$unset 的使用:它跟$set相反,有点remove的味道,但又貌似有不同的之处,当操作的对象为object时key是删除了;而数组array中它的值没有了,却保留了下标,不改变数组的长度,删除数组建议还是用$splice;请看下图: let arr = [1,2,3,4,5,6]; let obj={ name:“immutable”, children:{ address:“ShenZhen”, hobby:“写博客” } }; let obj2 = update(obj,{ $unset : [“name”], children:{ $unset:[“address”] } }); console.log(“原始数据:",obj); console.log(“obj2:",obj2); let arr2 = update(arr,{ $unset : [1] }); console.log(“arr2:",arr2,arr2.length);$merge 的使用:$merge 跟我们最爱的Object.assign一样,做合并操作的,但它比assign优秀很多,深层次拷贝,不会相互影响 : let arr = [1,2,3,4,5,6]; let obj={ name:“immutable”, children:{ address:“ShenZhen”, hobby:“写博客”, array:[“我不是程序员”,“切图崽了解一下”], } }; let obj2 = update(obj,{ $merge:{ arr }, children:{ array:{ $merge:{items:[“从前有坐山”,“山里有个庙”]}, $splice:[[3,0,“住着一个小和尚”]] } } }); console.log(“原始数据:",obj); console.log(“obj2:",obj2);$apply 的使用:$apply 基于当前值进行一个函数运算,从而得到新的值 :注意 :它必须是一个 function 哦! let obj={ name:“immutable”, children:{ items:[“从前有一坐山”], array: [1,2,3,4,5,6], } }; let obj2 = update(obj,{ name:{ $apply:(val)=>(“首席填坑官”) }, children:{ items:{ $apply:(val)=>{ console.log(“旧值”,val); return [3,0,“住着一个小和尚”] } }, array:{ $apply:(val)=>(val.reverse()) //必须是一个函数 } } }); console.log(“原始数据:",obj); console.log(“obj2:",obj2);$remove 的使用:$remove 一定一定 要是使用Set、Map 创建的数组:要删除的值,必须是数组成存在的,如值不存在则忽略,$remove:[2,666],2会删除,6则会被忽略;这个api有点奇怪,正常普通的数组 [],这样的删除不了!!;常见错误如下图: let obj={ name:“immutable”, children:{ array:new Set([1, 2, 3, 4, 4]), } }; let obj2 = update(obj,{ children:{ array:{ $remove:[2], }, } }); console.log(“原始数据:",obj); console.log(“obj2:",obj2);$add 的使用:$add 跟刚才的 $remove 一样要使用Map/Set,$add方法也跟 es6 Map/Set的 add方法一致:只是写的时候也要注意一些, [ [] ] ,嵌套! let obj={ name:“immutable”, array:new Map([[“a”,1],[“b”,2]]), }; let obj2 = update(obj,{ array:{ $add:[[“66”,56]], }, }); console.log(“原始数据:",obj); console.log(“obj2:",obj2); console.log(“获取key a:",obj2.array.get(‘a’));Immutability Helpers的高阶用法:还可以自定义方法,如 定义一个 $trinocular 方法,来判断数组中的值;只是一个简单的示例,更多复杂的用法,可以自己去探索哦 去官方 github ???? update.extend(’$trinocular’, function(proportion, original) { return original > 88 ? (original/proportion ): (proportion+original); }); let array =[56,33,55,777,322,444,61,12,34,52,245]; let array2 = array.map((k,v)=>update(k,{ $trinocular:2 })) console.log(“原始数据:",array); console.log(“array2:",array2);总结/结尾: 以上就是基础 API 的用法 ,添加了一些官方示例,没有讲到的组合使用,以及使用过程中,可能出现的一些错误,需要留意的地方,更多定制高级用法,有兴趣的同学可以自行了解一下。 以上就是今天为大家带来的分享,它可能没有 immutable 那么多功能,但贵在简洁,不会有太多的约束,如理解有误之处,欢迎各位大佬纠正,毕竟我还只是个宝宝——新手上路中!????。 下方是我弄的一个公众号,欢迎关注,以后文章会第一时间,在公众号上更新,原因是之前分享的有两篇文章,竟然被其他公众号抄袭了????,前些天去更新发表的时候,微信提示我文章已经不是原创了检测到相同的文章,宝宝心里那个凉啊~,果断申诉告了对方(是一个培训学校公众号,好气哦),补了掘金发布的链接和截图日期,万幸最后胜诉了????!????????更多文章:做完小程序项目、老板给我加了6k薪资~面试踩过的坑,都在这里了~你应该做的前端性能优化之总结大全!如何给localStorage设置一个过期时间?手把手教你如何绘制一辆会跑车如何用CSS3画出懂你的3D魔方?SVG Sprites Icon的使用技巧作者:苏南 - 首席填坑官链接:https://honeybadger8.github.i…交流:912594095、公众号:honeyBadger8本文原创,著作权归作者所有。商业转载请联系@IT·平头哥联盟获得授权,非商业转载请注明原链接及出处。 ...

November 4, 2018 · 4 min · jiezi

前端数据扁平化与持久化

(PS: 时间就像海绵里的水,挤到没法挤,只能挤挤睡眠时间了~ 知识点还是需要整理的,付出总会有收获,tired but fulfilled~)前言最近业务开发,从零搭建网页生成器,支持网页的可视化配置。为了满足这种需求,需要将各种页面抽象成类似地模块,再将每个模块抽象成各个可配置的组件,有些组件还包含一些小部件。这样一来,页面配置的JSON数据就会深层级地嵌套,那么修改一个小组件的配置,要怎样来更新页面树的数据?用id一层一层遍历?这样做法当然是不推荐的,不仅性能差,代码写起来也麻烦。因此,就考虑能否像数据库一样,把数据范式化,将嵌套的数据展开,每条数据对应一个id,通过id直接操作。Normalizr 就帮你做了这样一件事情。另外考虑到页面编辑,就需要支持 撤销 与 重做的功能,那么要怎样来保存每一步的数据?页面编辑的数据互相关联,对象的可变性会带来很大的隐患。虽然JS中的const(es6)、Object.freeze(es5) 可以防止数据被修改,但它们都是shallow处理,遇到嵌套多和深的结构就需要递归处理,而递归又存在性能上的问题。这时,用过React的童鞋就知道了,React借助 Immutable 来减少DOM diff的比对,它就能够很好地解决上面这两个问题。Immutable 实现的原理是 Persistent Data Structure(持久化数据结构),也就是使用旧数据创建新数据时,要保证旧数据同时可用且不变。那么为什么在JS中,诸如对象这样的数据类型是可变的呢?我们先来了解一下JS的数据类型。JS数据类型JS的数据类型包括基本类型和引用类型。基本类型包括String、Number、 Boolean、Null、Undefined,引用类型主要是对象(包括Object、Function、Array、Data等)。基础类型的值本身无法被改变,而引用类型,如Object,是可以被改变的。本文讨论的数据不可变,就是指保持对象的状态不变。来看看下面的例子:// 基本类型var a = 1;var b = a;b = 3;console.log(a); // 1console.log(b); // 3// 引用类型var obj1 = {};obj1.arr = [2,3,4];var obj2 = obj1;obj2.arr.push(5);console.log(obj1.arr); // [2, 3, 4, 5]console.log(obj2.arr); // [2, 3, 4, 5]上面例子中,b的值改变后,a的值不会随着改变;而obj2.arr被修改后,obj1.arr的值却跟着变化了。这是因为JS对象中的赋值是“引用赋值”,即在赋值的过程中,传递的是在内存中的引用。这也是JS中对象为什么有深拷贝和浅拷贝的用法,只有深拷贝后,对新对象的修改才不会改变原来的对象。浅拷贝只会将对象的各个属性进行依次复制,并不会进行递归复制,而 JavaScript 存储对象都是存地址的。上面代码中,只是执行了浅拷贝,结果导致 obj1 和 obj2指向同一块内存地址。所以修改obj2.arr,obj1.arr的值也变了。如果是深拷贝(如Lodash的cloneDeep)则不同,它不仅将原对象的各个属性逐个复制出去,而且将原对象各个属性所包含的对象也依次采用深拷贝的方法递归复制到新对象上,也就不会存在上面 obj1 和 obj2 中的 arr 属性指向同一个内存对象的问题。为了更清晰地理解这个问题,还是得来了解下javascript变量的存储方式。数据类型的存储程序的运行都需要内存,JS语言把数据分配到内存的栈(stack)和堆(heap)进行各种调用(注:内存中除了栈和堆,还有常量池)。JS这样分配内存,与它的垃圾回收机制有关,可以使程序运行时占用的内存最小。在JS中,每个方法被执行时,都会建立自己的内存栈,这个方法内定义的变量就会一一被放入这个栈中。等到方法执行结束,它的内存栈也自然地销毁了。因此,所有在方法中定义的变量都是放在栈内存中的。当我们在程序中创建一个对象时,这个对象将被保存到运行时数据区中,以便反复利用(因为对象的创建成本通常较大),这个运行时数据区就是堆内存。堆内存中的对象不会随方法的结束而销毁,即使方法结束后,这个对象还可能被另一个引用变量所引用。只有当一个对象没有任何引用变量引用它时,系统的垃圾回收机制才会在核实的时候回收它。总的来说,栈中存储的是基础变量以及一些对象的引用变量,基础变量的值是存储在栈中,而引用变量存储在栈中的是指向堆中的对象的地址,这就是修改引用类型总会影响到其他指向这个地址的引用变量的原因。堆是运行时动态分配内存的,存取速度较慢,栈的优势是存取速度比堆要快,并且栈内的数据可以共享,但是栈中数据的大小与生存期必须是确定的,缺乏灵活性。Normalizr与范式化范式化(Normalization)是数据库设计中的一系列原理和技术,以减少数据库中数据冗余,增进数据的一致性。直观地描述就是寻找对象之间的关系,通过某种方式将关系之间进行映射,减少数据之间的冗余,优化增删改查操作。Normalizr库本身的解释就是Normalizes nested JSON according to a schema),一种类似于关系型数据库的处理方法,通过建表建立数据关系,把深层嵌套的数据展开,更方便灵活的处理和操作数据。来看个官网的例子,理解一下:{ “id”: “123”, “author”: { “id”: “1”, “name”: “Paul” }, “title”: “My awesome blog post”, “comments”: [ { “id”: “324”, “commenter”: { “id”: “2”, “name”: “Nicole” } } ]}这是一份博客的数据,一篇文章article有一个作者author, 一个标题title, 多条评论,每条评论有一个评论者commenter,每个commenter又有自己的id和name。这样如果我们要获取深层级的数据,如commenter时,就需要层层遍历。这时候,如果使用Normalizr,就可以这样定义Schema:import { schema } from ’normalizr’; const user = new schema.Entity(‘users’);const comment = new schema.Entity(‘comments’, { commenter: user});const article = new schema.Entity(‘articles’, { author: user, comments: [comment]});然后调用一下 Normalize,就可以得到扁平化后的数据,如下:{ “entities”: { “users”: { “1”: { “id”: “1”, “name”: “Paul” }, “2”: { “id”: “2”, “name”: “Nicole” } }, “comments”: { “324”: { “id”: “324”, “commenter”: “2” } }, “articles”: { “123”: { “id”: “123”, “author”: “1”, “title”: “My awesome blog post”, “comments”: [“324”] } } }, “result”: “123”}这样每个作者、每条评论、每篇文章都有对应的id, 我们就不需要遍历,可以直接拿对应的id进行修改。再来看下我们在项目中的示例代码:分别定义element、section 和 page三张表,并指定它们之间的关系。这样范式化后,想对某个页面某个模块或者某个元素进行增删查改,就直接拿对应的id,不需要再耗性能去遍历了。Immutable与持久化Facebook工程师Lee Byron花了3年时间打造Immutable,与 React 同期出现。Immutable Data,维基百科上是这样定义的:In computing, a persistent data structure is a data structure that always preserves the previous version of itself when it is modified. Such data structures are effectively immutable, as their operations do not (visibly) update the structure in-place, but instead always yield a new updated structure.简单来说,Immutable Data 就是一旦创建,就不能再被更改的数据。对 Immutable 对象的任何修改或添加删除操作都会返回一个新的 Immutable 对象。Immutable 实现的原理是 Persistent Data Structure(持久化数据结构),也就是使用旧数据创建新数据时,要保证旧数据同时可用且不变。Immutable 使用了 Structural Sharing(结构共享),即如果对象树中一个节点发生变化,只修改这个节点和受它影响的父节点,其它节点则进行共享,这样就避免了深拷贝带来的性能损耗。我们通过图片来理解一下:Immutable 内部实现了一套完整的持久化数据结构,有很多易用的数据类型,如Collection、List、Map、Set、Record、Seq(Seq是借鉴了Clojure、Scala、Haskell这些函数式编程语言,引入的一个特殊结构)。它有非常全面的map、filter、groupBy、reduce、find等函数式操作方法。它的Api很强大,大家有兴趣可以去看下。这里简单列举 updateIn/getIn 来展示它带来的一些便捷操作:var obj = { a: { b: { list: [1, 2, 3] } }};var map = Immutable.fromJS(obj); // 注意 fromJS这里实现了深转换var map2 = Immutable.updateIn([‘a’, ‘b’, ’list’], (list) => { return list.push(4);});console.log(map2.getIn([‘a’, ‘b’, ’list’]))// List [ 1, 2, 3, 4 ]代码中我们要改变数组List的值,不必一层一层获取数据,而是直接传入对应的路径修改就行。这种操作在数据嵌套越深时,优势更加明显。来看下我们业务代码的示例吧。这里在多个页面的模块配置中,要更新某个页面的某个模块的数据,我们只需要在updateIn传入对应的path和value,就可以达到预想的效果。篇幅有限,更多的示例请自行查看api。熟悉React的同学也基于它结构的不可变性和共享性,用它来能够快速进行数据的比较。原本React中使用PureRenderMixin来做DOM diff比较,但只是浅比较,当数据结构比较深的时候,依然会存在多余的diff过程。这里只提个点,不深入展开了,感兴趣的同学可以自行google。与 Immutable.js 类似的,还有个seamless-immutable,它的代码库非常小,压缩后下载只有 2K。而 Immutable.js 压缩后下载有16K。大家各取所需,根据实际情况,自己斟酌下使用哪个比较适合。优缺点什么事物都有利弊,代码库也不例外。这里列举下它们的优缺点,大家权衡利弊,一起来看下:Normalizr 可以将数据扁平化处理,方便对深层嵌套的数据进行增删查改,但是文档不是很清晰,大家多查多理解,引入库文件也会增大。Immutable 有持久化数据解构,如List/Map等,并发安全。第二,它支持结构共享,比cloneDeep 性能更优,节省内存。第三,它借鉴了Clojure、Scala、Haskell这些函数式编程语言,引入了特殊结构Seq,支持Lazy operation。Undo/Redo,Copy/Paste,甚至时间旅行这些功能对它来说都是小菜一碟。缺点方面,Immutable源文件过大,压缩后有15kb。其次,侵入性强,与原生api容易混淆。第三,类型转换比较繁琐,尤其是与服务器交互频繁时,这种缺点就更加明显。总结篇幅有限,时间也比较晚了,关于前端数据的扁平化与持久化处理先讲这么多了,有兴趣的同学可以关注下,后面有时间会多整理分享。参考资料前端数据范式化Immutable详解及React中实践为什么需要Immutable.jsfacebook immutable.js 意义何在,使用场景?一些链接, 关于不可变数据 ...

September 3, 2018 · 2 min · jiezi