immer源碼閱讀

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)
}

produceProxy

在繼續閱讀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

  • 爲何使用Proxy.revocable作代理,而不是new Proxy?
  • 爲何要把數組的state包裹到一個數組裏面[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

Handler

接下來看一下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
    }
}

finalize

除了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]不相等說明可能存在兩種狀況:

  • 因爲value被get過,此時value是一個代理對象。
  • value被set過,此時value多是一個普通的值也多是一個代理對象(好比把rootProxy的某個子孫代理屬性賦值給了copy[prop],即value)。

因此咱們就好理解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的核心邏輯,和全部比較難以理解的地方。

相關文章
相關標籤/搜索