Immer.js簡析

開始

在函數式編程中,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。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實現仍是至關巧妙的,之後能夠在狀態管理上使用一下。

相關文章
相關標籤/搜索