2zH / articles-collection

Chuunibyou is good!
1 stars 0 forks source link

[source code] Immer.js && Proxy #13

Open 2zH opened 6 years ago

2zH commented 6 years ago

Official repo: https://github.com/mweststrate/immer

介绍

轻量的 immutable 库,由 mobx 作者 mweststrate 开发。与 mobx 有相似的地方 (并非状态管理库),但更轻量,功能粒度更细。它用来帮助不可变 (immutable) 状态树进行数据迭代,源码的实现上使用 Proxy API 生成 Draft state tree, 通过getter/setter来捕获对状态树的修改行为,以更合理的性能损耗 (cost) 迭代出一个全新的状态树。

默认支持 Curry ,可用于函数合并。

官方介绍: Create the next immutable state tree by simply modifying the current tree 通过简单的修改当前状态树来创建下一个不可变状态树。

Example

import produce from "immer"

const baseState = [
    {
        todo: "Learn typescript",
        done: true
    },
    {
        todo: "Try immer",
        done: false
    }
]
const nextState = baseState.slice()
nextState.push({ todo: "Tweet about it" })
nextState[1] = {
    ...nextState[1],
    done: true
}
const nextStateUseImmer = produce(baseState, draftState => {
    draftState.push({ todo: "Tweet about it" })
    draftState[1].done = true
})

Reducer Example:

// Redux reducer
// Shortened, based on: https://github.com/reactjs/redux/blob/master/examples/shopping-cart/src/reducers/products.js
const byId = (state, action) => {
  switch (action.type) {
    case RECEIVE_PRODUCTS:
      return {
        ...state,
        ...action.products.reduce((obj, product) => {
          obj[product.id] = product
          return obj
        }, {})
      }
    default:
      return state
  }
}
import produce from 'immer'

const byId = (state, action) =>
  produce(state, draft => {
    switch (action.type) {
      case RECEIVE_PRODUCTS:
        action.products.forEach(product => {
          draft[product.id] = product
        })
    }
  })

React.setState example:

/**
 * Classic React.setState with a deep merge
 */
onBirthDayClick1 = () => {
    this.setState((prevState)=>({
        user: {
            ...prevState.user,
            age: prevState.user.age + 1
        }
    }))
}

/**
 * ...But, since setState accepts functions,
 * we can just create a curried producer and further simplify!
*/
onBirthDayClick2 = () => {
    this.setState(produce(draft => {
        draft.user.age += 1
    }))
}
2zH commented 6 years ago

主体部分在函数执行开始的位置先把当前 scope 下的 proxies 存为 previousProxies,函数执行的最后还原回去。函数内主要调用了,createProxy 与 finalize 这两个函数,通过 createProxy 生成 DraftStateTree,然后放入 producer 当中执行,若返回结果不为原 draft state tree 并且不是 undefined,通过 finalize 函数把 DraftStateTree 处理为 NextStateTree 并返回,同时 revoke 所有 proxies 中储存的 proxy 对象。

主体思路为: PrevStateTree -> createProxy -> DraftStateTree -> finalize -> NextStateTree.

function produce(baseState, producer) {
    // ...
    return getUseProxies()
        ? produceProxy(baseState, producer)
        : produceEs5(baseState, producer)
}

// https://github.com/mweststrate/immer/blob/master/src/common.js#L16
// https://github.com/mweststrate/immer/blob/master/src/common.js#L34
function getUserProxies () {
    return typeof Proxy !== "undefined"
}

// produceEs5是作为不支持 Proxy API 的兼容方案,这里暂且不讨论
// https://github.com/mweststrate/immer/blob/master/src/proxy.js#L131
// 主体部分
function produceProxy(baseState, producer) {
    const previousProxies = proxies
    proxies = []
    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) {
            // something was returned, and it wasn't the proxy itself
            if (rootProxy[PROXY_STATE].modified)
                throw new Error(RETURNED_AND_MODIFIED_ERROR)

            // See #117
            // Should we just throw when returning a proxy which is not the root, but a subset of the original state?
            // Looks like a wrongly modeled reducer
            result = finalize(returnValue)
        } else {
            result = finalize(rootProxy)
        }
        // revoke all proxies
        each(proxies, (_, p) => p.revoke())
        return result
    } finally {
        proxies = previousProxies
    }
}
2zH commented 6 years ago

createProxy:

/// https://github.com/mweststrate/immer/blob/master/src/proxy.js#L122
function createProxy(parentState, base) {
    const state = createState(parentState, base)
    const proxy = Array.isArray(base)
        ? Proxy.revocable([state], arrayTraps)
        : Proxy.revocable(state, objectTraps)
    proxies.push(proxy)
    return proxy.proxy
}

createState:

function createState(parent, base) {
    return {
        modified: false,   // 是否被修改过
        finalized: false,    // 是否修改已经全部完成
        parent,                  // 是否存在父元素
        base,                     // 本体
        copy: undefined, // 本体的浅拷贝
        proxies: {}            //  存储每个 propertyKey 的代理对象
    }
}

objectTraps, arrayTraps:

对应不同类型的 Proxy handler.


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("Don't even try this...")
}
}

const arrayTraps = {} each(objectTraps, (key, fn) => { arrayTraps[key] = function() { arguments[0] = arguments[0][0] return fn.apply(this, arguments) } }) // 这里主要列出的是 get/set ,帮助理解,其他被复写的方法/函数可去源码处研究。 // https://github.com/mweststrate/immer/blob/master/src/proxy.js#L58 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)) // only create proxy if it is not yet a proxy, and not a new object // (new objects don't need proxying, they will be processed in finalize anyway) 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 } }

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 }


finalize:
``` JavaScript
// https://github.com/mweststrate/immer/blob/master/src/common.js#L77
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
}
function finalizeObject(copy, state) {
    const base = state.base
    each(copy, (prop, value) => {
        if (value !== base[prop]) copy[prop] = finalize(value)
    })
    return freeze(copy)
}

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)
}
2zH commented 6 years ago

To be continue...

参考文章:

2zH commented 6 years ago

To be continue...

参考文章: