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的核心逻辑,和全部比较难以理解的地方。