摘要:我們看一下整個函數(shù)可以看到為的情況下,邏輯很簡單,對應(yīng)屬性沒變化的時候創(chuàng)建代理,返回值,對應(yīng)屬性變化了,直接返回對應(yīng)值。
immer是前端在immutable領(lǐng)域的另外一個實踐,相比較immutable而言,它擁有更低的學(xué)習(xí)成本,在使用上可以直接使用js 原生api去修改引用對象,得到一個新的不可變的引用對象。
import produce from "immer" const baseState = [ { todo: "Learn typescript", done: true }, { todo: "Try immer", done: false } ] const nextState = produce(baseState, draftState => { draftState.push({todo: "Tweet about it"}) draftState[1].done = true })
immer的實現(xiàn)主要有兩種方案,在支持Proxy的環(huán)境下會使用Proxy,在不支持Proxy的環(huán)境下會使用defineProperty。在這里主要介紹Proxy的實現(xiàn)方案,因為基本的實現(xiàn)思路都是相似的,通過學(xué)習(xí)Proxy的實現(xiàn)方案,我們也能熟悉一下在平時業(yè)務(wù)開發(fā)時很少用的Proxy api。
從上面的例子可以看出使用immer,主要通過調(diào)用produce這個api,從源碼中可以看到produce這個函數(shù)其實調(diào)用了produceProxy函數(shù):
function produce(baseState, producer) { ... return getUseProxies() ? produceProxy(baseState, producer) : produceEs5(baseState, producer) }produceProxy
在繼續(xù)閱讀immer的源碼之前,我們不妨想一下,如何通過Proxy實現(xiàn)immer的功能?
我們必須在沒修改對象的情況下獲取原對象的屬性,在修改的情況下又不要修改原對象的屬性。我們可以很容易想到get handler的操作:
new Proxy(data, { get(target, prop){ return target[prop] }, set(target, prop, value){ } })
但是set如何處理?所以我們代理的對象不能只是數(shù)據(jù)本身,在immer中每個代理的對象都是以下結(jié)構(gòu):
function createState(parent, base) { return { base, // 要代理的原數(shù)據(jù) parent, // 要代理數(shù)據(jù)的父對象 copy: undefined, // 在set時,修改這個數(shù)據(jù)對應(yīng)的值 proxies: {}, // 講解get時,再談這個 modified: false, // 有沒有要修改這份數(shù)據(jù) finalized: false // 本文最后會講解 } }
接下來我們回到produceProxy函數(shù):
export function produceProxy(baseState, producer) { ... const previousProxies = proxies proxies = [] // 通過createProxy創(chuàng)建的proxy都會在這里面 try { // create proxy for root const rootProxy = createProxy(undefined, baseState) // 創(chuàng)建根代理 // execute the thunk const returnValue = producer.call(rootProxy, rootProxy) // 執(zhí)行函數(shù),拿到返回值 // and finalize the modified proxy let result // check whether the draft was modified and/or a value was returned if (returnValue !== undefined && returnValue !== rootProxy) { ... } else { result = finalize(rootProxy) } // revoke all proxies each(proxies, (_, p) => p.revoke()) // 銷毀代理,主要是為了防止外層的變量拿到這個代理做一些操作 return result } finally { proxies = previousProxies } }
可以看到主要邏輯還是很清晰的,為數(shù)據(jù)創(chuàng)建代理,然后調(diào)用producer函數(shù),最后finalize(rootProxy)。
接下來看一下createProxy的相關(guān)邏輯:
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 }
createProxy的邏輯看起來很簡單,但是你可能會有兩個疑問:
為什么使用Proxy.revocable做代理,而不是new Proxy?
為什么要把數(shù)組的state包裹到一個數(shù)組里面[state]
先來回答第一個問題,使用Proxy.revocable主要是為了防止以下情況的出現(xiàn):
let proxy const nextState = produce(baseState, s => { proxy = s s.aProp = "hello" }) proxy.aProp = "Hallo"
如代碼所示,如果produce執(zhí)行完成后,proxy不做revoke,會導(dǎo)致外部變量拿到的proxy,還有作用,就會造成不期望的情況出現(xiàn)。所以在produceProxy最后,會把函數(shù)執(zhí)行周期所有創(chuàng)建的proxy都revoke掉。
第二個問題,通過produceProxy的代碼,我們可以看到在調(diào)用外部傳入的producer函數(shù)的時候,傳給producer函數(shù)的是proxy,如果不使用[state],proxy代理的state就是一個對象。此時如果對其類型進行判斷Array.isArray(proxy)就會返回false。
我們可以看一下objectTraps和arrayTraps分別是什么:
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()`.") } } const arrayTraps = {} each(objectTraps, (key, fn) => { arrayTraps[key] = function() { arguments[0] = arguments[0][0] return fn.apply(this, arguments) // state push proxy } })
可以看到objectTraps是一個很普通的handlers,而arrayTraps則是在objectTraps上包裹了一層,傳入的參數(shù)將[state]改為了state。
Handler接下來看一下get,先看一下數(shù)據(jù)沒有被修改過的情況(即還沒調(diào)用過set)
function get(state, prop) { if (prop === PROXY_STATE) return state // PROXY_STATE是一個symbol值,有兩個作用,一是便于判斷對象是不是已經(jīng)代理過,二是幫助proxy拿到對應(yīng)state的值 if (state.modified) { ... } 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 } }
get函數(shù)主要有兩個作用:
返回對應(yīng)的數(shù)據(jù)
為對應(yīng)的數(shù)據(jù)創(chuàng)建代理
通過get的時候創(chuàng)建代理就保證了不管在produce中操作的數(shù)據(jù)嵌套有多深,我們操作的都是代理對象,如:
a.b.c = 1 // a.b是一個代理對象 a.b.c.push(1) // a.b.c是一個代理對象
接下來看set函數(shù)
function set(state, prop, value) { // set的關(guān)鍵是不改老的值,所以改的copy上的值 if (!state.modified) { if ( (prop in state.base && is(state.base[prop], value)) || (has(state.proxies, prop) && state.proxies[prop] === value) //值不變的情況下直接return true ) return true markChanged(state) } state.copy[prop] = value return true }
set的邏輯相對簡單,set值就是改state.copy上的值,同時如果state是第一次修改,就markChanged(state)
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) } }
在markChanged函數(shù)中,把base的屬性和proxies的上的屬性都淺拷貝給了copy,從此,對目標(biāo)對象的取值還是設(shè)值都是操作state.copy。
我們看一下整個get函數(shù),可以看到state.modified為true的情況下,邏輯很簡單,對應(yīng)屬性沒變化的時候創(chuàng)建代理,返回值,對應(yīng)屬性變化了,直接返回對應(yīng)值。
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 } }finalize
除了get和set,還有6個其他的handler,但整體思路和get、set一致,就不一一介紹了。我們看一下produceProxy的最后一塊,也是我認(rèn)為最不好理解的一部分finalize。
export function produceProxy(baseState, producer) { ... const previousProxies = proxies proxies = [] // 通過createProxy創(chuàng)建的proxy都會在這里面 try { // create proxy for root const rootProxy = createProxy(undefined, baseState) // 創(chuàng)建根代理 // execute the thunk const returnValue = producer.call(rootProxy, rootProxy) // 執(zhí)行函數(shù),拿到返回值 // and finalize the modified proxy let result // check whether the draft was modified and/or a value was returned if (returnValue !== undefined && returnValue !== rootProxy) { ... } else { result = finalize(rootProxy) } // revoke all proxies each(proxies, (_, p) => p.revoke()) // 銷毀代理,主要是為了防止外層的變量拿到這個代理做一些操作 return result } finally { proxies = previousProxies } }
前面通過producer函數(shù)對rootProxy進行了一系列的操作,現(xiàn)在我們要返回下一次的state,我們要遞歸地state上的屬性,把屬性對應(yīng)的代理對象,改為對應(yīng)的值。
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) // base不是代理則說明base下面的屬性會有代理 return base }
因為我們第一次傳入finalize函數(shù)的是rootProxy,是一個Proxy,我們先看isProxy(base)為true的情況,簡化一下對應(yīng)的邏輯,可以看到邏輯很簡單:
const state = base[PROXY_STATE] if (state.modified === true) { return finalizeObject( state.copy, state ) } else { return state.base }
如果state沒有被修改過,就直接返回state.base,如果state修改過,就返回finalizeObject(state.copy, state)函數(shù)的返回值。
至于為什么要設(shè)置state.finalized的值,我們稍后再講,我們先看一下finalizeObject函數(shù)的邏輯。
function finalizeObject(copy, state) { const base = state.base each(copy, (prop, value) => { if (value !== base[prop]) copy[prop] = finalize(value) }) return freeze(copy) }
finalizeObject函數(shù)遍歷copy上的屬性,對于value和base[prop]不相等的情況,調(diào)用finalize(value),最后freeze copy對象,然后返回。
value和base[prop]不相等說明可能存在兩種情況:
由于value被get過,此時value是一個代理對象。
value被set過,此時value可能是一個普通的值也可能是一個代理對象(比如把rootProxy的某個子孫代理屬性賦值給了copy[prop],即value)。
所以我們就好理解finalize函數(shù)中為什么既要處理value是proxy的情況,又要處理value不是proxy的情況了。
當(dāng)value不是一個proxy的時候,value的子屬性可能是一個proxy(因為賦值的時候,可能值的子屬性是proxy),immer用finalizeNonProxiedObject處理這種情況。
function finalizeNonProxiedObject(parent) { // If finalize is called on an object that was not a proxy, it means that it is an object that was not there in the original // tree and it could contain proxies at arbitrarily places. Let"s find and finalize them as well if (!isProxyable(parent)) return if (Object.isFrozen(parent)) return each(parent, (i, child) => { if (isProxy(child)) { parent[i] = finalize(child) } else finalizeNonProxiedObject(child) }) // always freeze completely new data freeze(parent) }
如果屬性值是一個proxy,就調(diào)用finalize,以去除proxy,否則就遞歸的去找下面屬性是不是proxy。
最后我們說一下finalize函數(shù)中為什么要state.finalized = true,按照正常的邏輯屬性在finalize函數(shù)中只會訪問一次,根本這行代碼。
這行代碼是為了防止一種情況,某個屬性值被賦值給了另外一個屬性,這兩個屬性訪問的是一個數(shù)據(jù),此時如果state已經(jīng)finalized,就直接返回他的copy。
至此,我們已經(jīng)閱讀完immer的核心邏輯,和所有比較難以理解的地方。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/96341.html
摘要:無奈網(wǎng)絡(luò)上完善的文檔實在太少,所以自己寫了一份,本篇文章以貼近實戰(zhàn)的思路和流程,對進行了全面的講解。這使得成為了真正的不可變數(shù)據(jù)。的使用非常靈活,多多思考,相信你還可以發(fā)現(xiàn)更多其他的妙用參考文檔官方文檔 文章在 github 開源, 歡迎 Fork 、Star 前言 Immer 是 mobx 的作者寫的一個 immutable 庫,核心實現(xiàn)是利用 ES6 的 proxy,幾乎以最小的成...
摘要:精讀原文介紹了學(xué)習(xí)源碼的兩個技巧,并利用實例說明了源碼學(xué)習(xí)過程中可以學(xué)到許多周邊知識,都讓我們受益匪淺。討論地址是精讀源碼學(xué)習(xí)如果你想?yún)⑴c討論,請點擊這里,每周都有新的主題,周末或周一發(fā)布。 1. 引言 javascript-knowledge-reading-source-code 這篇文章介紹了閱讀源碼的重要性,精讀系列也已有八期源碼系列文章,分別是: 精讀《Immer.js》源...
摘要:例如維護一份在內(nèi)部,來判斷是否有變化,下面這個例子就是一個構(gòu)造函數(shù),如果將它的實例傳入對象作為第一個參數(shù),就能夠后面的處理對象中使用其中的方法上面這個構(gòu)造函數(shù)相比源代碼省略了很多判斷的部分。 showImg(https://segmentfault.com/img/bV27Dy?w=1400&h=544); 博客鏈接:下一代狀態(tài)管理工具 immer 簡介及源碼解析 JS 里面的變量類...
摘要:所以整個過程只涉及三個輸入狀態(tài),中間狀態(tài),輸出狀態(tài)關(guān)鍵是是如何生成,如何應(yīng)用修改,如何生成最終的。至此基本把上的模式解析完畢。結(jié)束實現(xiàn)還是相當(dāng)巧妙的,以后可以在狀態(tài)管理上使用一下。 開始 在函數(shù)式編程中,Immutable這個特性是相當(dāng)重要的,但是在Javascript中很明顯是沒辦法從語言層面提供支持,但是還有其他庫(例如:Immutable.js)可以提供給開發(fā)者用上這樣的特性,所...
閱讀 1223·2021-11-25 09:43
閱讀 1337·2021-09-26 09:55
閱讀 2330·2021-09-10 11:20
閱讀 3365·2019-08-30 15:55
閱讀 1441·2019-08-29 13:58
閱讀 1164·2019-08-29 12:36
閱讀 2337·2019-08-29 11:18
閱讀 3407·2019-08-26 11:47