immer是前端在immutable領域的另一個實踐,相比較immutable而言,它擁有更低的學習成本,在使用上能夠直接使用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的實現主要有兩種方案,在支持Proxy的環境下會使用Proxy,在不支持Proxy的環境下會使用defineProperty。在這裏主要介紹Proxy的實現方案,由於基本的實現思路都是類似的,經過學習Proxy的實現方案,咱們也能熟悉一下在平時業務開發時不多用的Proxy api。typescript
從上面的例子能夠看出使用immer,主要經過調用produce這個api,從源碼中能夠看到produce這個函數其實調用了produceProxy函數:api
function produce(baseState, producer) { ... return getUseProxies() ? produceProxy(baseState, producer) : produceEs5(baseState, producer) }
在繼續閱讀immer的源碼以前,咱們不妨想一下,如何經過Proxy實現immer的功能?數組
咱們必須在沒修改對象的狀況下獲取原對象的屬性,在修改的狀況下又不要修改原對象的屬性。咱們能夠很容易想到get handler的操做:app
new Proxy(data, { get(target, prop){ return target[prop] }, set(target, prop, value){ } })
可是set如何處理?因此咱們代理的對象不能只是數據自己,在immer中每一個代理的對象都是如下結構:函數
function createState(parent, base) { return { base, // 要代理的原數據 parent, // 要代理數據的父對象 copy: undefined, // 在set時,修改這個數據對應的值 proxies: {}, // 講解get時,再談這個 modified: false, // 有沒有要修改這份數據 finalized: false // 本文最後會講解 } }
接下來咱們回到produceProxy函數:學習
export function produceProxy(baseState, producer) { ... const previousProxies = proxies proxies = [] // 經過createProxy建立的proxy都會在這裏面 try { // create proxy for root const rootProxy = createProxy(undefined, baseState) // 建立根代理 // execute the thunk const returnValue = producer.call(rootProxy, rootProxy) // 執行函數,拿到返回值 // 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函數,最後finalize(rootProxy)。this
接下來看一下createProxy的相關邏輯:代理
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的邏輯看起來很簡單,可是你可能會有兩個疑問:code
[state]
先來回答第一個問題,使用Proxy.revocable主要是爲了防止如下狀況的出現:
let proxy const nextState = produce(baseState, s => { proxy = s s.aProp = "hello" }) proxy.aProp = "Hallo"
如代碼所示,若是produce執行完成後,proxy不作revoke,會致使外部變量拿到的proxy,還有做用,就會形成不指望的狀況出現。因此在produceProxy最後,會把函數執行週期全部建立的proxy都revoke掉。
第二個問題,經過produceProxy的代碼,咱們能夠看到在調用外部傳入的producer函數的時候,傳給producer函數的是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上包裹了一層,傳入的參數將[state]
改成了state
。
接下來看一下get,先看一下數據沒有被修改過的狀況(即還沒調用過set)
function get(state, prop) { if (prop === PROXY_STATE) return state // PROXY_STATE是一個symbol值,有兩個做用,一是便於判斷對象是否是已經代理過,二是幫助proxy拿到對應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函數主要有兩個做用:
經過get的時候建立代理就保證了無論在produce中操做的數據嵌套有多深,咱們操做的都是代理對象,如:
a.b.c = 1 // a.b是一個代理對象 a.b.c.push(1) // a.b.c是一個代理對象
接下來看set函數
function set(state, prop, value) { // set的關鍵是不改老的值,因此改的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函數中,把base的屬性和proxies的上的屬性都淺拷貝給了copy,今後,對目標對象的取值仍是設值都是操做state.copy。
咱們看一下整個get函數,能夠看到state.modified爲true的狀況下,邏輯很簡單,對應屬性沒變化的時候建立代理,返回值,對應屬性變化了,直接返回對應值。
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 } }
除了get和set,還有6個其餘的handler,但總體思路和get、set一致,就不一一介紹了。咱們看一下produceProxy的最後一塊,也是我認爲最很差理解的一部分finalize。
export function produceProxy(baseState, producer) { ... const previousProxies = proxies proxies = [] // 經過createProxy建立的proxy都會在這裏面 try { // create proxy for root const rootProxy = createProxy(undefined, baseState) // 建立根代理 // execute the thunk const returnValue = producer.call(rootProxy, rootProxy) // 執行函數,拿到返回值 // 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函數對rootProxy進行了一系列的操做,如今咱們要返回下一次的state,咱們要遞歸地state上的屬性,把屬性對應的代理對象,改成對應的值。
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函數的是rootProxy,是一個Proxy,咱們先看isProxy(base)爲true的狀況,簡化一下對應的邏輯,能夠看到邏輯很簡單:
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)函數的返回值。
至於爲何要設置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) }
finalizeObject函數遍歷copy上的屬性,對於value和base[prop]不相等的狀況,調用finalize(value),最後freeze copy對象,而後返回。
value和base[prop]不相等說明可能存在兩種狀況:
因此咱們就好理解finalize函數中爲何既要處理value是proxy的狀況,又要處理value不是proxy的狀況了。
當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,就調用finalize,以去除proxy,不然就遞歸的去找下面屬性是否是proxy。
最後咱們說一下finalize函數中爲何要state.finalized = true
,按照正常的邏輯屬性在finalize函數中只會訪問一次,根本這行代碼。
這行代碼是爲了防止一種狀況,某個屬性值被賦值給了另一個屬性,這兩個屬性訪問的是一個數據,此時若是state已經finalized,就直接返回他的copy。
至此,咱們已經閱讀完immer的核心邏輯,和全部比較難以理解的地方。