开始

在函数式编程中,Immutable这个特性是相当重要的,但是在Javascript中很明显是没办法从语言层面提供支持,但是还有其他库(例如:Immutable.js)可以提供给开发者用上这样的特性,所以一直很好奇这些库是怎么实现Immutable的,这次就从Immer.js(小巧玲珑)入手看看内部是怎么做的。

Copy On Write(写时复制)

第一次了解到这样的技术还是在学Java的时候,当然这个词也是很好理解:准备修改的时候,先复制一份再去修改;这样就能避免直接修改本体数据,也能把性能影响最小化(不修改就不用复制了嘛);在Immer.js里面也是使用这种技术,而Immer.js的基本思想是这样的:

The basic idea is that you will apply all your changes to a temporarily draftState, which is a proxy of the currentState. Once all your mutations are completed, Immer will produce the nextState based on the mutations to the draft state. This means that you can interact with your data by simply modifying it, while keeping all the benefits of immutable data.

个人简单翻译一下:主要思想就是先在currentState基础上生成一个代理draftState,之后的所有修改都会在draftState上进行,避免直接修改currentState,而当修改结束后,再从draftState基础上生成nextState。所以整个过程只涉及三个State:currentState(输入状态),draftState(中间状态),nextState(输出状态);关键是draftState是如何生成,如何应用修改,如何生成最终的nextState。

分析源码

因为Immer.js确实非常小巧,所以直接从核心API出发:

const nextState = produce(baseState, draftState => {
draftState.push({todo: "Tweet about it"})
draftState[1].done = true
})

在上面produce方法就包括刚才说的currentState->draftState->nextState整个过程,然后深入produce方法:

export default function produce(baseState, producer) {
...
return getUseProxies()
? produceProxy(baseState, producer)
: produceEs5(baseState, producer)
}

Immer.js会判断是否可以使用ES6的Proxy,如果没有只能使用ES5的方式去实现代理(当然也是会麻烦一点),这里先从ES6的Proxy实现方式开始分析,后面再回头分析一下ES5的实现方式。

export function produceProxy(baseState, producer) {
const previousProxies = proxies // 1.备份当前代理对象
proxies = []
try {
const rootProxy = createProxy(undefined, baseState) // 2.创建代理
const returnValue = producer.call(rootProxy, rootProxy) // 3.应用修改
let result
if (returnValue !== undefined && returnValue !== rootProxy) {
if (rootProxy[PROXY_STATE].modified)
throw new Error(RETURNED_AND_MODIFIED_ERROR)
result = finalize(returnValue) // 4.生成对象
} else {
result = finalize(rootProxy) // 5.生成对象
}
each(proxies, (_, p) => p.revoke()) // 6.注销当前所有代理
return result
} finally {
proxies = previousProxies // 7.恢复之前的代理对象
}
}

这里把关键的步骤注释一下,第1步和第6,7步是有关联的,主要为了应对嵌套的场景:

const nextStateA = produce(baseStateA, draftStateA => {
draftStateA[1].done = true;
const nextStateB = produce(baseStateB, draftStateB => {
draftStateB[1].done = true
});
})

因为每个produce方法最后都要注销所有代理,防止produce之后仍然可以使用代理对象进行修改(因为在代理对象上修改最终还是会映射到生成的对象上),所以这里每次都需要备份一下proxies,以便之后注销。

第2步,创建代理对象(核心)

function createProxy(parentState, base) {
if (isProxy(base)) throw new Error("Immer bug. Plz report.")
const state = createState(parentState, base)
const proxy = Array.isArray(base)
? Proxy.revocable([state], arrayTraps)
: Proxy.revocable(state, objectTraps)
proxies.push(proxy)
return proxy.proxy
}

这里Immer.js会使用crateState方法封装一下我们传入的数据:

{
modified: false, //是否修改
finalized: false, //是否finalized
parent, //父state
base, //自身state
copy: undefined, //拷贝后的state
proxies: {} //存放生成的代理对象
}

然后就是根据数据是否是对象还是数组来生成对应的代理,以下是代理所拦截的操作:

const objectTraps = {
get,
has(target, prop) {
return prop in source(target)
},
ownKeys(target) {
return Reflect.ownKeys(source(target))
},
set,
deleteProperty,
getOwnPropertyDescriptor,
defineProperty,
setPrototypeOf() {
throw new Error("Immer does not support `setPrototypeOf()`.")
}
}

我们重点关注get和set方法就行了,因为这是最常用的,搞明白这两个方法基本原理也搞明白Immer.js的核心。首先看get方法:

function get(state, prop) {
if (prop === PROXY_STATE) return state
if (state.modified) {
const value = state.copy[prop]
if (value === state.base[prop] && isProxyable(value))
return (state.copy[prop] = createProxy(state, value))
return value
} else {
if (has(state.proxies, prop)) return state.proxies[prop]
const value = state.base[prop]
if (!isProxy(value) && isProxyable(value))
return (state.proxies[prop] = createProxy(state, value))
return value
}
}

一开始如果访问属性等于PROXY_STATE这个特殊值的话,直接返回封装过的state本身,如果是其他属性会返回初始对象或者是它的拷贝上对应的值。所以这里接着会出现一个分支,如果state没有被修改过,访问的是state.base(初始对象),否则访问的是state.copy(因为修改都不会在state.base上进行,一旦修改过,只有state.copy才是最新的);这里也会看到其他的代理对象只有访问对应的属性的时候才会去尝试创建,属于“懒”模式。
再看看set方法:

function set(state, prop, value) {
if (!state.modified) {
if (
(prop in state.base && is(state.base[prop], value)) ||
(has(state.proxies, prop) && state.proxies[prop] === value)
)
return true
markChanged(state)
}
state.copy[prop] = value
return true
}

如果第一次修改对象,直接会触发markChanged方法,把自身的modified标记为true,接着一直冒泡到根对象调用markChange方法:

function markChanged(state) {
if (!state.modified) {
state.modified = true
state.copy = shallowCopy(state.base)
// copy the proxies over the base-copy
Object.assign(state.copy, state.proxies) // yup that works for arrays as well
if (state.parent) markChanged(state.parent)
}
}

除了标记modified,还做另外一件就是从base上生成拷贝,当然这里做的浅复制,尽量利用已存在的数据,减小内存消耗,还有就是把proxies上之前创建的代理对象也复制过去。所以最终的state.copy上可以同时包含代理对象和普通对象,然后之后的访问修改都直接在state.copy上进行。

到这里完成了刚开始的currentState->draftState的转换了,之后就是draftState->nextState的转换,也就是之前注释的第4步:

result = finalize(returnValue)

再看看finalize方法:

export function finalize(base) {
if (isProxy(base)) {
const state = base[PROXY_STATE]
if (state.modified === true) {
if (state.finalized === true) return state.copy
state.finalized = true
return finalizeObject(
useProxies ? state.copy : (state.copy = shallowCopy(base)),
state
)
} else {
return state.base
}
}
finalizeNonProxiedObject(base)
return base
}

这个方法主要为的是从state.copy上生成一个普通的对象,因为刚才也说了state.copy上很有可能同时包含代理对象和普通对象,所以必须把代理对象都转换成普通对象,而state.finalized就是标记是否已经完成转换的。
直接深入finalizeObject方法:

function finalizeObject(copy, state) {
const base = state.base
each(copy, (prop, value) => {
if (value !== base[prop]) copy[prop] = finalize(value)
})
return freeze(copy)
}

这里也是一个深度遍历,如果state.copy上的value不等于state.base上的,肯定是被修改过的,所以直接再跳入finalize里面进行转换,最后把转换后的state.copy,freeze一下,一个新的Immutable数据就诞生了。
而另外一个finalizeNonProxiedObject方法,目标也是查找普通对象里面的代理对象进行转换,就不贴代码了。

至此基本把Immer.js上的Proxy模式解析完毕。

而在ES5上因为没有ES6的Proxy,只能仿造一下:

function createProxy(parent, base) {
const proxy = shallowCopy(base)
each(base, i => {
Object.defineProperty(proxy, "" + i, createPropertyProxy("" + i))
})
const state = createState(parent, proxy, base)
createHiddenProperty(proxy, PROXY_STATE, state)
states.push(state)
return proxy
}

创建代理的时候就是先从base上进行浅复制,然后使用defineProperty对象的getter和setter进行拦截,把映射到state.base或者state.copy上。其实现在注意到ES5只能对getter和setter进行拦截处理,如果我们在代理对象上删除一个属性或者增加一个属性,我们之后怎么去知道,所以Immer.js最后会用proxy上的属性keys和base上的keys做一个对比,判断是否有增减属性:

function hasObjectChanges(state) {
const baseKeys = Object.keys(state.base)
const keys = Object.keys(state.proxy)
return !shallowEqual(baseKeys, keys)
}

其他过程基本跟ES6的Proxy上是一样的。

结束

Immter.js实现还是相当巧妙的,以后可以在状态管理上使用一下。

Immer.js简析的更多相关文章

  1. XMR恶意挖矿案例简析

    前言 数字货币因其技术去中性化和经济价值等属性,逐渐成为大众关注的焦点,同时通过恶意挖矿获取数字货币是黑灰色产业获取收益的重要途径.本文简析通过蜜罐获取的XMR恶意挖矿事件:攻击者通过爆破SSH获取系 ...

  2. Nutch学习笔记二——抓取过程简析

    在上篇学习笔记中http://www.cnblogs.com/huligong1234/p/3464371.html 主要记录Nutch安装及简单运行的过程. 笔记中 通过配置抓取地址http://b ...

  3. React Native startReactApplication 方法简析

    在 React Native 启动流程简析 这篇文章里,我们梳理了 RN 的启动流程,最后的 startReactApplication 由于相对复杂且涉及到最终执行前端 js 的流程,我们单独将其提 ...

  4. 简析.NET Core 以及与 .NET Framework的关系

    简析.NET Core 以及与 .NET Framework的关系 一 .NET 的 Framework 们 二 .NET Core的到来 1. Runtime 2. Unified BCL 3. W ...

  5. 简析 .NET Core 构成体系

    简析 .NET Core 构成体系 Roslyn 编译器 RyuJIT 编译器 CoreCLR & CoreRT CoreFX(.NET Core Libraries) .NET Core 代 ...

  6. RecycleView + CardView 控件简析

    今天使用了V7包加入的RecycleView 和 CardView,写篇简析. 先上效果图: 原理图: 这是RecycleView的工作原理: 1.LayoutManager用来处理RecycleVi ...

  7. Java Android 注解(Annotation) 及几个常用开源项目注解原理简析

    不少开源库(ButterKnife.Retrofit.ActiveAndroid等等)都用到了注解的方式来简化代码提高开发效率. 本文简单介绍下 Annotation 示例.概念及作用.分类.自定义. ...

  8. PHP的错误报错级别设置原理简析

    原理简析 摘录php.ini文件的默认配置(php5.4): ; Common Values: ; E_ALL (Show all errors, warnings and notices inclu ...

  9. Android 启动过程简析

    首先我们先来看android构架图: android系统是构建在linux系统上面的. 所以android设备启动经历3个过程. Boot Loader,Linux Kernel & Andr ...

随机推荐

  1. 染色dp(确定一行就可行)

    题:https://codeforces.com/contest/1027/problem/E 题意:给定n*n的方格,可以染黑白,要求相邻俩行”完全“不同或完全相同,对于列也是一样.然后限制不能拥有 ...

  2. ARM7探究

    1.流水线:三级流水线 预取.译码.执行.三级并行发生 2.什么是哈佛结构? 哈佛结构是一种存储器结构,是一种并行体系结构,它的主要特点是将程序和数据存储在不同的存储空间中,即程序存储器和数据存储器是 ...

  3. 吴裕雄--天生自然python学习笔记:python文档操作自动生成菜单 Word 文件

    许多学校营养午餐的菜单是由教师来轮流制作 ,这是一个 比较烦锁的工作,如 果能自动用教师最熟悉的 Word 文件来生成一个菜单文件,使教师对生成的菜单稍作 修改即可使用,那将是一个不错的主意. 案例要 ...

  4. OpenCL介绍

    OpenCL(全称Open Computing Language,开放运算语言)是第一个面向异构系统通用目的并行编程的开放式.免费标准,也是一个统一的编程环境,便于软件开发人员为高性能计算服务器.桌面 ...

  5. spring boot学习4 多环境配置

    说明: 在企业中,一个项目一般都有测试环境(test) .开发环境(dev).生产环境(pro)等等.在每个环境中,配置信息会不一样的.比如数据库.静态资源文件位置等都会不一样的. 那么使用sprin ...

  6. HttpClient学习笔记

    HttpClient相关的实体类官方文档地址:http://hc.apache.org/httpcomponents-client-ga/httpclient/apidocs/ 使用HttpClien ...

  7. 最优化算法——常见优化算法分类及总结

    之前做特征选择,实现过基于群智能算法进行最优化的搜索,看过一些群智能优化算法的论文,在此做一下总结. 在生活或者工作中存在各种各样的最优化问题,比如每个企业和个人都要考虑的一个问题"在一定成 ...

  8. Jenkins 2 如何使用 PowerShell 以及自定 build fail (指定 exit code)

    Jenkins 除了用來做為 CI(持續性整合) 工具外,也可以與其他 plugin 配合達成其他目的(e.g.IIS restart.檔案壓縮備份-),今天就來看看可以怎麼與 PowerShell ...

  9. URL与URI与URN的区别与联系

    1.什么是URL? 统一资源定位符(或称统一资源定位器/定位地址.URL地址等[1],英语:Uniform Resource Locator,常缩写为URL),有时也被俗称为网页地址(网址).如同在网 ...

  10. Drools 7.15.0 docker容器方式部署

    关于drools的相关介绍就不再赘述了,关于drools网上的资料都很少,或者都有些老了,最近折腾了一下,记录下安装部署的过程,希望能节省下大家的时间. 一.快速部署 1.拉取基础镜像,命令如下: d ...