在函数式编程中,Immutable这个特性是至关重要的,可是在Javascript中很明显是没办法从语言层面提供支持,可是还有其余库(例如:Immutable.js)能够提供给开发者用上这样的特性,因此一直很好奇这些库是怎么实现Immutable的,此次就从Immer.js(小巧玲珑)入手看看内部是怎么作的。编程
第一次了解到这样的技术仍是在学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。app
由于Immer.js确实很是小巧,因此直接从核心API出发:ide
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,以便以后注销。idea
第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实现仍是至关巧妙的,之后能够在状态管理上使用一下。