摘要:所以整個(gè)過程只涉及三個(gè)輸入狀態(tài),中間狀態(tài),輸出狀態(tài)關(guān)鍵是是如何生成,如何應(yīng)用修改,如何生成最終的。至此基本把上的模式解析完畢。結(jié)束實(shí)現(xiàn)還是相當(dāng)巧妙的,以后可以在狀態(tài)管理上使用一下。
開始
在函數(shù)式編程中,Immutable這個(gè)特性是相當(dāng)重要的,但是在Javascript中很明顯是沒辦法從語(yǔ)言層面提供支持,但是還有其他庫(kù)(例如:Immutable.js)可以提供給開發(fā)者用上這樣的特性,所以一直很好奇這些庫(kù)是怎么實(shí)現(xiàn)Immutable的,這次就從Immer.js(小巧玲瓏)入手看看內(nèi)部是怎么做的。
Copy On Write(寫時(shí)復(fù)制)第一次了解到這樣的技術(shù)還是在學(xué)Java的時(shí)候,當(dāng)然這個(gè)詞也是很好理解:準(zhǔn)備修改的時(shí)候,先復(fù)制一份再去修改;這樣就能避免直接修改本體數(shù)據(jù),也能把性能影響最小化(不修改就不用復(fù)制了嘛);在Immer.js里面也是使用這種技術(shù),而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.
個(gè)人簡(jiǎn)單翻譯一下:主要思想就是先在currentState基礎(chǔ)上生成一個(gè)代理draftState,之后的所有修改都會(huì)在draftState上進(jìn)行,避免直接修改currentState,而當(dāng)修改結(jié)束后,再?gòu)膁raftState基礎(chǔ)上生成nextState。所以整個(gè)過程只涉及三個(gè)State:currentState(輸入狀態(tài)),draftState(中間狀態(tài)),nextState(輸出狀態(tài));關(guān)鍵是draftState是如何生成,如何應(yīng)用修改,如何生成最終的nextState。
分析源碼因?yàn)镮mmer.js確實(shí)非常小巧,所以直接從核心API出發(fā):
const nextState = produce(baseState, draftState => { draftState.push({todo: "Tweet about it"}) draftState[1].done = true })
在上面produce方法就包括剛才說的currentState->draftState->nextState整個(gè)過程,然后深入produce方法:
export default function produce(baseState, producer) { ... return getUseProxies() ? produceProxy(baseState, producer) : produceEs5(baseState, producer) }
Immer.js會(huì)判斷是否可以使用ES6的Proxy,如果沒有只能使用ES5的方式去實(shí)現(xiàn)代理(當(dāng)然也是會(huì)麻煩一點(diǎn)),這里先從ES6的Proxy實(shí)現(xiàn)方式開始分析,后面再回頭分析一下ES5的實(shí)現(xiàn)方式。
export function produceProxy(baseState, producer) { const previousProxies = proxies // 1.備份當(dāng)前代理對(duì)象 proxies = [] try { const rootProxy = createProxy(undefined, baseState) // 2.創(chuàng)建代理 const returnValue = producer.call(rootProxy, rootProxy) // 3.應(yīng)用修改 let result if (returnValue !== undefined && returnValue !== rootProxy) { if (rootProxy[PROXY_STATE].modified) throw new Error(RETURNED_AND_MODIFIED_ERROR) result = finalize(returnValue) // 4.生成對(duì)象 } else { result = finalize(rootProxy) // 5.生成對(duì)象 } each(proxies, (_, p) => p.revoke()) // 6.注銷當(dāng)前所有代理 return result } finally { proxies = previousProxies // 7.恢復(fù)之前的代理對(duì)象 } }
這里把關(guān)鍵的步驟注釋一下,第1步和第6,7步是有關(guān)聯(lián)的,主要為了應(yīng)對(duì)嵌套的場(chǎng)景:
const nextStateA = produce(baseStateA, draftStateA => { draftStateA[1].done = true; const nextStateB = produce(baseStateB, draftStateB => { draftStateB[1].done = true }); })
因?yàn)槊總€(gè)produce方法最后都要注銷所有代理,防止produce之后仍然可以使用代理對(duì)象進(jìn)行修改(因?yàn)樵诖韺?duì)象上修改最終還是會(huì)映射到生成的對(duì)象上),所以這里每次都需要備份一下proxies,以便之后注銷。
第2步,創(chuàng)建代理對(duì)象(核心)
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會(huì)使用crateState方法封裝一下我們傳入的數(shù)據(jù):
{ modified: false, //是否修改 finalized: false, //是否finalized parent, //父state base, //自身state copy: undefined, //拷貝后的state proxies: {} //存放生成的代理對(duì)象 }
然后就是根據(jù)數(shù)據(jù)是否是對(duì)象還是數(shù)組來生成對(duì)應(yīng)的代理,以下是代理所攔截的操作:
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()`.") } }
我們重點(diǎn)關(guān)注get和set方法就行了,因?yàn)檫@是最常用的,搞明白這兩個(gè)方法基本原理也搞明白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這個(gè)特殊值的話,直接返回封裝過的state本身,如果是其他屬性會(huì)返回初始對(duì)象或者是它的拷貝上對(duì)應(yīng)的值。所以這里接著會(huì)出現(xiàn)一個(gè)分支,如果state沒有被修改過,訪問的是state.base(初始對(duì)象),否則訪問的是state.copy(因?yàn)樾薷亩疾粫?huì)在state.base上進(jìn)行,一旦修改過,只有state.copy才是最新的);這里也會(huì)看到其他的代理對(duì)象只有訪問對(duì)應(yīng)的屬性的時(shí)候才會(huì)去嘗試創(chuàng)建,屬于“懶”模式。
再看看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 }
如果第一次修改對(duì)象,直接會(huì)觸發(fā)markChanged方法,把自身的modified標(biāo)記為true,接著一直冒泡到根對(duì)象調(diào)用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) } }
除了標(biāo)記modified,還做另外一件就是從base上生成拷貝,當(dāng)然這里做的淺復(fù)制,盡量利用已存在的數(shù)據(jù),減小內(nèi)存消耗,還有就是把proxies上之前創(chuàng)建的代理對(duì)象也復(fù)制過去。所以最終的state.copy上可以同時(shí)包含代理對(duì)象和普通對(duì)象,然后之后的訪問修改都直接在state.copy上進(jìn)行。
到這里完成了剛開始的currentState->draftState的轉(zhuǎn)換了,之后就是draftState->nextState的轉(zhuǎn)換,也就是之前注釋的第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 }
這個(gè)方法主要為的是從state.copy上生成一個(gè)普通的對(duì)象,因?yàn)閯偛乓舱f了state.copy上很有可能同時(shí)包含代理對(duì)象和普通對(duì)象,所以必須把代理對(duì)象都轉(zhuǎn)換成普通對(duì)象,而state.finalized就是標(biāo)記是否已經(jīng)完成轉(zhuǎn)換的。
直接深入finalizeObject方法:
function finalizeObject(copy, state) { const base = state.base each(copy, (prop, value) => { if (value !== base[prop]) copy[prop] = finalize(value) }) return freeze(copy) }
這里也是一個(gè)深度遍歷,如果state.copy上的value不等于state.base上的,肯定是被修改過的,所以直接再跳入finalize里面進(jìn)行轉(zhuǎn)換,最后把轉(zhuǎn)換后的state.copy,freeze一下,一個(gè)新的Immutable數(shù)據(jù)就誕生了。
而另外一個(gè)finalizeNonProxiedObject方法,目標(biāo)也是查找普通對(duì)象里面的代理對(duì)象進(jìn)行轉(zhuǎn)換,就不貼代碼了。
至此基本把Immer.js上的Proxy模式解析完畢。
而在ES5上因?yàn)闆]有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 }
創(chuàng)建代理的時(shí)候就是先從base上進(jìn)行淺復(fù)制,然后使用defineProperty對(duì)象的getter和setter進(jìn)行攔截,把映射到state.base或者state.copy上。其實(shí)現(xiàn)在注意到ES5只能對(duì)getter和setter進(jìn)行攔截處理,如果我們?cè)诖韺?duì)象上刪除一個(gè)屬性或者增加一個(gè)屬性,我們之后怎么去知道,所以Immer.js最后會(huì)用proxy上的屬性keys和base上的keys做一個(gè)對(duì)比,判斷是否有增減屬性:
function hasObjectChanges(state) { const baseKeys = Object.keys(state.base) const keys = Object.keys(state.proxy) return !shallowEqual(baseKeys, keys) }
其他過程基本跟ES6的Proxy上是一樣的。
結(jié)束Immter.js實(shí)現(xiàn)還是相當(dāng)巧妙的,以后可以在狀態(tài)管理上使用一下。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/52574.html
摘要:所以整個(gè)過程只涉及三個(gè)輸入狀態(tài),中間狀態(tài),輸出狀態(tài)關(guān)鍵是是如何生成,如何應(yīng)用修改,如何生成最終的。至此基本把上的模式解析完畢。結(jié)束實(shí)現(xiàn)還是相當(dāng)巧妙的,以后可以在狀態(tài)管理上使用一下。 開始 在函數(shù)式編程中,Immutable這個(gè)特性是相當(dāng)重要的,但是在Javascript中很明顯是沒辦法從語(yǔ)言層面提供支持,但是還有其他庫(kù)(例如:Immutable.js)可以提供給開發(fā)者用上這樣的特性,所...
摘要:無奈網(wǎng)絡(luò)上完善的文檔實(shí)在太少,所以自己寫了一份,本篇文章以貼近實(shí)戰(zhàn)的思路和流程,對(duì)進(jìn)行了全面的講解。這使得成為了真正的不可變數(shù)據(jù)。的使用非常靈活,多多思考,相信你還可以發(fā)現(xiàn)更多其他的妙用參考文檔官方文檔 文章在 github 開源, 歡迎 Fork 、Star 前言 Immer 是 mobx 的作者寫的一個(gè) immutable 庫(kù),核心實(shí)現(xiàn)是利用 ES6 的 proxy,幾乎以最小的成...
摘要:例如維護(hù)一份在內(nèi)部,來判斷是否有變化,下面這個(gè)例子就是一個(gè)構(gòu)造函數(shù),如果將它的實(shí)例傳入對(duì)象作為第一個(gè)參數(shù),就能夠后面的處理對(duì)象中使用其中的方法上面這個(gè)構(gòu)造函數(shù)相比源代碼省略了很多判斷的部分。 showImg(https://segmentfault.com/img/bV27Dy?w=1400&h=544); 博客鏈接:下一代狀態(tài)管理工具 immer 簡(jiǎn)介及源碼解析 JS 里面的變量類...
摘要:精讀原文介紹了學(xué)習(xí)源碼的兩個(gè)技巧,并利用實(shí)例說明了源碼學(xué)習(xí)過程中可以學(xué)到許多周邊知識(shí),都讓我們受益匪淺。討論地址是精讀源碼學(xué)習(xí)如果你想?yún)⑴c討論,請(qǐng)點(diǎn)擊這里,每周都有新的主題,周末或周一發(fā)布。 1. 引言 javascript-knowledge-reading-source-code 這篇文章介紹了閱讀源碼的重要性,精讀系列也已有八期源碼系列文章,分別是: 精讀《Immer.js》源...
閱讀 1178·2021-10-20 13:48
閱讀 2208·2021-09-30 09:47
閱讀 3113·2021-09-28 09:36
閱讀 2353·2019-08-30 15:56
閱讀 1208·2019-08-30 15:52
閱讀 2028·2019-08-30 10:48
閱讀 617·2019-08-29 15:04
閱讀 579·2019-08-29 12:54