摘要:所以整個過程只涉及三個輸入狀態,中間狀態,輸出狀態關鍵是是如何生成,如何應用修改,如何生成最終的。至此基本把上的模式解析完畢。結束實現還是相當巧妙的,以后可以在狀態管理上使用一下。
開始
在函數式編程中,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實現還是相當巧妙的,以后可以在狀態管理上使用一下。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/52574.html
摘要:所以整個過程只涉及三個輸入狀態,中間狀態,輸出狀態關鍵是是如何生成,如何應用修改,如何生成最終的。至此基本把上的模式解析完畢。結束實現還是相當巧妙的,以后可以在狀態管理上使用一下。 開始 在函數式編程中,Immutable這個特性是相當重要的,但是在Javascript中很明顯是沒辦法從語言層面提供支持,但是還有其他庫(例如:Immutable.js)可以提供給開發者用上這樣的特性,所...
摘要:無奈網絡上完善的文檔實在太少,所以自己寫了一份,本篇文章以貼近實戰的思路和流程,對進行了全面的講解。這使得成為了真正的不可變數據。的使用非常靈活,多多思考,相信你還可以發現更多其他的妙用參考文檔官方文檔 文章在 github 開源, 歡迎 Fork 、Star 前言 Immer 是 mobx 的作者寫的一個 immutable 庫,核心實現是利用 ES6 的 proxy,幾乎以最小的成...
摘要:例如維護一份在內部,來判斷是否有變化,下面這個例子就是一個構造函數,如果將它的實例傳入對象作為第一個參數,就能夠后面的處理對象中使用其中的方法上面這個構造函數相比源代碼省略了很多判斷的部分。 showImg(https://segmentfault.com/img/bV27Dy?w=1400&h=544); 博客鏈接:下一代狀態管理工具 immer 簡介及源碼解析 JS 里面的變量類...
閱讀 1167·2021-10-20 13:48
閱讀 2173·2021-09-30 09:47
閱讀 3104·2021-09-28 09:36
閱讀 2342·2019-08-30 15:56
閱讀 1195·2019-08-30 15:52
閱讀 2020·2019-08-30 10:48
閱讀 607·2019-08-29 15:04
閱讀 564·2019-08-29 12:54