Open EthanLin-TWer opened 7 years ago
React.js Conf 2015 - Immutable Data and React with Lee Byron: https://www.youtube.com/watch?v=I7IdS-PbEgI
how - persistent(remain previous input immutable) immutable(never changed once created) data structures - performance - interfaces & implmentations - dag(Directed Acyclic Graph) algorithm - Trie for arrays and objects that're not primitives
j.c.r licklider - trie, alan kay - smalltalk, phil bagwell - basic hash array, rich hickey - simple made easy simplicity is hard work
why -
实践了一段时间以后,目前觉得,seamless-immutable 是最佳方案。为什么呢?
先看一下我们要什么,我们要的是 redux reducer 的 immutability(不可变能力)。这种能力,无非就是满足以下两点:
reducer 是个纯函数,相当于我们只是在 state = reducer(previousState, action)
这个出口,加一层 immutable 的保证,变成:state = ensureImmutable(reducer(previousState, action))
。理想来说,除了 redux 的 reducer,应用的其他部分是不应该感知到这个变化的。
而 ImmutableJS 这样的库完全违反了这一点,一旦引入,应用处处都要知道和学习它的存在。就像 redux 一样,一旦引入,很难替换,是个很有风险的技术选型。而 seamless-immutable 做到了,只在 reducer 层加一层 immutable 的能力,而应用的其他部分无感知。
侵入性这个东西基本是不能忍的,不可能为了获得一个小的(虽然极为重要的) immutable 能力,耦合上整个应用的设计。综上,就凭这一点,可以断言重量级的 ImmutableJS 等库不是合理选择。它应该被以简单的方式实现。
咦? 为啥只搜到了 React 栈(六)和(二),其他部分还没建issue吗🤣
学习了,感觉用了Immutable就想用了一种另外的语言……它给人带来的不适感真是值得斟酌。 一旦大规模使用就难以根除,其实如果有在immutable使用上不畏艰险写满测试的话,还是能有信心完全根除的。如果不是的话,恐怕只会越改越心虚。
Immutability
Immutability 并不局限于 JavaScript,只不过 Redux 的一个基石就是 immutability。Redux 的程序必须同时具备 Immutability,否则就是逻辑错误的。 文档已经回答了很多问题,基本上只有下面「为什么没有 immutable 的 redux 是逻辑错误的」一节是自己的思考过程,其余可悉数参考官方文档。非常完整的文档,非常专业。
Immutable 是什么?
原始定义可能比较难找了。大概的意思是两点:
遵循 immutable 的设计,数据之间会呈现一个关系:引用的对比结果等同于值的对比结果。我们下面会看到,为什么这个推论对于 redux 来说是必须要有的。
为什么没有 immutable 的 redux 是逻辑错误的?
先废几句话再进正题:redux 设计过程使用了浅对比是正确的,这个决定恰如其分。如果用了深对比,大数据量下一定出性能问题,这样库也不可能被广泛采用了。
再来问,为什么说 Redux 一定需要数据的 immutability,没有就一定是逻辑错误呢?因为 Redux 出于性能考虑使用了浅对比算法(shallow equality check)。浅对比进行的是引用对比,不比对值变化,因此需要 Redux 的用户自行建立「引用对比」和「值对比」之间的对应关系。这种关系事实上就是 immutability 的表述:
也就是说,immutability 是一种关系,它保证了
dataEqualityCheck()
(deepEqualityCheck()
) 和referenceEqualityCheck()
(shallowEqualityCheck()
) 的结果总是相同的。Redux 需要这种关系,而这是需要开发者去保证的。为什么说浅对比没有性能问题
与浅对比(引用对比)相对应的是深对比(值对比)。深对比能检测到对象是否深度值「相等」,但算法复杂度更高。这两种算法并没有必然说要使用哪一种,Redux 选择了使用浅对比,这就是框架本身的取舍与选择。这个取舍是美的,我认为恰如其分。如果使用了深对比,处理起真实项目上的大型对象一定会有性能问题。
为什么 Redux 一定要进行这样的「对比」
因为 Redux 需要在「数据发生变化」的时候,通知其监听者进行相应的动作。数据发生变化,既包含数据结构的变化,也包含数据中任何一层对象(或基本类型)值的变化。也即是说,要了解「数据是否发生变化」,Redux 必须进行精确的值对比。
然而如上所说,直接进行值对比的代价太高,因此 Redux 做了这样的妥协:只进行引用对比,通过强制 引用变化 和 值变化 建立一一联系的方式,间接地进行值对比。
引用比对结果 与 值比对结果 必须等价,这个关系就是 immutability 的表述。这也是我为什么讲,Immutability 是 Redux 的必然推论。否则整个 Redux 就是逻辑错误的一个系统。
这个推论,其实也是被官方论证了的,见 这里。
有兴趣的同学,可以看下面这段代码。「Redux 需要预设 Immutability」这个事情,更具体地说,是发生在
redux
的combineReducers()
这个方法中的:节选自
redux/src/combineReducers.js
。2017-08-07 执行npm install -g redux
取得的 redux 版本。如何使用 Immutable?不同方案的对比?
综上所述,Immutable 其实是一种「关系」,一个「引用比对结果与值比对结果必须一致」的关系,即这个等式必须恒成立:
dataEqualityCheck() === referenceEqualityCheck()
。只要你的 Redux reducer 中返回的新 state 都满足这个关系,那么就等于说你实现了 Immutability。至于如果做到这点,大概有两种方式:http://redux.js.org/docs/faq/ImmutableData.html#what-approaches-are-there-for-handling-data-immutably-do-i-have-to-use-immutablejs
原生 JS 优点是轻量、无需任何依赖,但缺点也突出:容易遗漏、容易产生冗余代码、大数量级数据下会有性能问题。
使用库则解决了原生 JS 的缺憾:引入后自动获得 Immutable 能力(由库保证)、丰富强大的 API、大数据量下性能优秀、使得数据版本历史等变得简单。但毛病同样突出,以 ImmutableJS 这个库为例(我也不知道为什么官方要举这个库为例子)。以下部分缺憾可能是针对这个库而特有的。
ImmutableJS 的缺点
toJS()
方法每次都会创建新对象,即便传入的值对象是相同的,两次调用生成的对象引用还是会不一样。这违反了 Immutable 定律的第一条:值不改变时引用也不能变。因此,在任何涉及 redux 的地方不恰当使用都可能造成逻辑错误,最典型例子就是在react-redux
库的mapStateToProps()
中使用了toJS()
但又没改变对象值,此时会导致组件的非必要渲染toJS()
方法toJS()
方法能转成普通 JS 对象,但性能稍慢,而且反过来也无法跟 Immutable 对象一起玩了teacher.students.henry
与teacher.getIn(['students', 'henry'])
综上,ImmutableJS 用不用呢?官方认为,值得,因为「在 redux reducer 中修改了原对象可能导致的 bug,发现、调试成本极高」,而 Immutable 解决了这个问题(然后带出了其他3、4个问题)。这个成本我是同意的,组件不该渲染时可能重复渲染,可能该渲染时不渲染,而且你还不能确定是不是 mutated 数据带来的问题。
其他实现 immutable 的方案?
https://github.com/facebook/immutable-js: 所有章节都在讨论这个选择的可行性,应该是目前最主流的选择了。优点是性能好,API 强大;缺点是 API 不够友好,侵入性强,需要团队约束与背景知识,潜在移除成本高。
https://github.com/gajus/redux-immutable: 让 ImmutableJS 与 redux 一起工作的库。
https://github.com/rtfeldman/seamless-immutable: 解决了 ImmutableJS API 不够友好和侵入性强的问题。同时引入了两个小问题:在原生对象上加了扩展,以及仅支持较主流浏览器的高版本,因为依赖于浏览器实现。感觉上,这个库我会更倾向,因为既有丰富而友好的 API,又能享受性能上的优势,还对应用没有侵入性;那两个毛病稍小,在原生对象上加扩展这个事比较不能忍,但也没其他方法,能忍。
Redux + ImmutableJS 最佳实践
综上,作者认为,redux 的 immutability 必须有,虽然 ImmutableJS 诸多缺点,但还是要用滴。只不过用起来不易,于是给凡人们总结了一个最佳实践,主要就是解决上面提到能解决的大部分痛点,应慎读之:
get()
方法,这样就不 pure 了,维护、测试成本都变大toJS()
违反 Immutable 定律第一条mapStateToProps()
中用toJS()
update
、merge
和set
方法时,确保要更新的对象已经使用fromJS()
包装过综上,诸多缺点中,多数都可以用(复杂的)最佳实践和项目规范来避免,尽管有一定的学习成本。基本无法解决的问题只有一个:一旦引入,去除掉可能就需要巨大的代价。
我的结论
总而言之,我得出的几点结论:
侵入性很强,主要是指很多地方需要「知道」ImmutableJS 的存在以及「如何使用它」,比如 reducer 的
initState
、container component 的mapStateToProps
、HOC 的组件中要做个中间层把 Smart Component 中的 ImmutableJStoJS()
到 Dumb Component 中,等;推荐的解决方案是,把「如何使用」,操作具体数据结构的这部分从业务逻辑中解耦出去,让业务逻辑对数据的获取方式无感知。虽然解决了这个问题,但需要从设计上迁就这个库的引入,说是更强的侵入性也未可知。团队约束与沟通成本,主要是指即便用了库,要保证最佳实践,团队依然需要掌握一些知识,比如不能在
mapStateToProps()
中使用toJS()
、Container Component -> HOC -> Pure function Component 的层次和数据流动方向(state ->toJS()
-> JS)需要了解等。Immutable 在其他语言中
以下内容尚未整理。
意义:
缺点:
然而,这个来自函数式编程世界的概念,对于编程和解决业务问题又有什么意义呢?怎么映射过来呢?
所以,数据有两种属性:值和属性。数据又分为两种:基本类型和可嵌套类型(如数组、对象)。数据的「对比」,就有了两种对比方法:值对比和引用对比。
引用可以认为是一个复杂数据的「identity」,你内部的「数值」可以改变,但你的「标识」——就是这个引用——是不变的。而对于基本类型来说,只存在值对比,比如数字,3 和 4 不相等就是不相等,不存在数值不等,但「标识」相等的情况。这个「标识」是不存在的,基本类型只有值,只存在值对比。又比如布尔类型,真就是真,假就是假;比如字符串,这在不同语言中多也设计成为不可变的基本类型,因此只有值对比。
可嵌套类型的对比,就可以有不同的对比方式了。可以对比「标识」也即引用,也可以比对每一项属性「值」,看你的上下文在乎的是哪一个。对比「标识」的话,则没有办法反映到具体值的变化;对比「值」的话,比较彻底,性能一定会成问题。
于是,在「复杂类型对象对比」这个上下文中,如果你既想检测结果能反映具体值的变化,又想具有常数复杂度的优秀性能,你就需要其他规则的介入了。Immutability 就是这样的一条规则:
这样一来,值变化 与 引用变化就建立了等价关系,你只需要比较引用,就可以以常数复杂度等价检测到「值是否变化」。
这个事情不知道怎么讲比较清楚呢。另外,所以是有什么意义呢?