Immer.js原理探索学习

2023-07-22  本文已影响0人  Yubble

        一年没更新过了,各位客爷过的都怎么样,老菜鸟这一年经历了太多的事,感觉人一下子成熟了好多,连我的母上大人都说我现在的表达比以前好太多了。

        毕竟我们在任何处境下都要保持学习,由此进入正题吧,来讨论学习下关于Immer.js的前世今生。

认识Immer

        提到Immer.js就不得不提Immutable.js,在老菜鸟学习React的时候经常听到一个词叫“副作用”,后来才知道是React在操作复杂数组的时候,没有给数组做好充分的“拷贝”,并且结合React自身的diff比较缺陷,亦导致了在修改数组深层元素时,无法触发页面的刷新。(由此看来Vue各版本的diff做的是多好)

      (注:React的差异比较使用的是shallowEqual,各位有精力的话自行学习哈~)

        所以为了修复这个bug,Facebook的大佬们设计并实现了一套’可持久化数据结构‘。

        简单点就是在数据被修改的时候,我先保存一套下来,你改的只是我Copy出来的一份,所以页面做Diff刷新时,比较的就是两个不同地址的数组。

                                                                                                        ——前端装逼架构师 Yubble

        起初这个想法的具体实现是由Immutable.js来完成的,用过的客爷最熟悉不过了,其中提供了常用的数据类型,Map,Set,List等等,使用的时候用它提供的方法包裹下,经过各类数据结构及算法操作后,即可拷贝出一套新的数据出来。(没有JSON.stringfy,或者for循环递归这么简单哈,有兴趣的客爷还是要针对研究下)。

这是在下见过的说服力最强的不可变数据结构图,改变数据栈地址的同时兼容复用

        那么为什么又推出了Immer呢,两方面,性能和使用成本。

        1、虽说Immutable.js通过算法,降低了递归带来的时间复杂度,但是树形结构终归是对时间及空间复杂的一大考验,且Immutable.js针对不同的数据类型又实现了不同的拷贝方法,所以整体库比较大。

        2、其次最重要的方面,Immer用起来要比Immutable简单太多了

多嵌套数据更新对比图

        诸位,各位,在齐位,看下Immer的修改简单太多了吧,直接操作数据的分身draft即可,相比之下Immutable的fromJS,setIn,getIn都是什么鬼,不光是修改,连读取都是有心智成本的。学不完根本学不完。

探究Immer

        开玩笑的,该学还是得学,毕竟房贷还没还完呢......

        那么聪明又好学的你一定会问“Immer这么先进,它具体是怎么实现的呢。”

        我看到Immer的源码之后第一反应就是,哦~做替身。

秦桧经历过一次危险后,吓怕了,分裂出一个替身,之后不论是下达指令,还是行刺被杀,都是替身的活儿

        同样的,在Immer中,每个数据只要被更新过,就会分裂出一个分身,之后的千变万化,再怎么折腾也是在分身上,本体丝毫不受伤害,由此在React diff比对时能灵活的更新Dom。

        那么针对这个思路,我们可以模拟出几个角色了。

        首先是一个状态机,它应该包含数据本身,是否被修改,以及修改后的替身数据这三个成员变量。

用类方式实现,为什么不用返回成员字面量来实现,因为需要在类上绑上原型方法

        紧接着我们有了状态机,那么状态的修改查看在哪里实现呢,所以给这个状态机加上两个方法,分别是get和set。

我们通过set和get给状态机增加了两个对外Api,并实现了一个最简单shallowCopy,由此状态机就完成了

        其次我们开始考虑劫持器,这里源码中用到的是Proxy元编程,不熟悉的客官翻下我之前的博文哈。

        劫持的目标是谁?应该是我们之前包装好的状态机(createState)的实例吧,只有对他动手脚的话,他才能根据自己有没有被更新过而判断返回主体还是返回替身呢~

        而判断返回主体还是替身的这段逻辑,已经被状态机写好了(get, set, _markChanged),所以我们把对它的操作劫持下来,直接调用方法就好。

        有个细节老菜之前不明白,就好比[].push()这个方法,劫持算get还是set呀,事实证明,还是算get的,应该是说访问了数组的push方法,于是问题就来了,我arr[1]下表这种也算get访问,arr.push()也算get访问,那么怎么返回真正数据内容呢,所以人家在考虑的时候,也帮我们想到了。即用一个标识代表我访问了数据内容。

这是我的劫持器,target代表状态机的实例,最后也是调用状态机提供的get和set方法,注意这个target.get和target.set是我们在上面手动实现的哈,不是js自带的拦截。

        于是现在我们两大功能角色(状态机、劫持器)都准备好了,我们写个生产方法把它整合起来吧。

        生产函数做的事情更存粹,既为我们各个角色的整合,它的出入参分别是:

        入参:state,原始数据。producer,操作数据的回调

        出参:返回主体或替身数据

        看代码大致就是做了三件事:

            1、声明状态机,用劫持器代理他

            2、将被代理后的状态机抛出去,供开发者操作

            3、抛出一个数据供Dom渲染

        至此一个简约的Immer就搞出来了,我们可以用一下试试

写个todo list,给数组中新增一条todo:收拾屋子,并且将第二条:学习法律的进程置为:已完成,看下返回的两条数据

        可以看到新生成的nextState确实已更新出第三条todo:收拾屋子,由此可以触发React的Dom更新了。

        如果客官够细心的话会发现draftState[1].done = true这一条同样会影响到本体数据,这是因为Proxy的劫持也只能劫持到数据的第一层操作,如果是复杂数据层级较多的情况下,仍然需要使用递归嵌套的方式做逐层劫持,有心的客官可以研究下Proxy代理多层结构

        如果只谈劫持以及返回替身数据的话,我们的简易Immer.js够用了,但是要想达到性能优化的Persistent data structure,我们还缺少算法这一步,在下精力实在有限,这期先不研究了


吐槽时间

        再来说下React,老菜鸟之前一直是Vue系的,所以对Vue一直情有独钟,不过Vue2.0的缺陷确实也无可厚非,它的MVVM让开发者省了不少事,但OOP这种设计模式也确实让Vue2.0功能的单一性极其受限,节省了心智成本的同时也降低了抽象方法的可拓展性,所以Vue3.0也在向函数式看齐~

        但是React咱就说说呀,能不能把该你做的事情做完,不要老让开发者花费心智理解什么不可变数据,感觉用你开发附加的学习成本还是不小的,看看人家Vue3.0为什么从来没让开发者为这些事发过愁,再次站队一番Vue3~

        老菜鸟经过一番心理斗争,从当前公司选择离职了,毕竟身体和家庭在我心里是最重要的,没了健康挣再多的钱有什么用,奉劝各位量力而行吧,我们在资本面前都是耗材,能陪你走到最后的就是家人和这副伤痕累累的自己。

        最后祝各位能在这并不安稳的年代找到自己奋斗的目标与价值,有钱的捧个钱场,没钱的帮在下找个班儿上。

看下我家刘lucky的傻样
上一篇下一篇

猜你喜欢

热点阅读