前言
React和Redux都遵守組件狀態(tài)為不可變(immutable)的理念甜攀,通常在修改state的時(shí)候都會(huì)使用到es6的解構(gòu)語法或者Object.assign()绎橘。
使用immer.js
比起Facebook親自打造的immutable.js定枷,immer.js比immutable.js體積更小交煞,不需要去記immutable.js里像Collection喊巍、List疆瑰、Map办陷、Set琅轧、Record這樣的數(shù)據(jù)結(jié)構(gòu)析孽,使用的時(shí)候還需要toJS()轉(zhuǎn)換成數(shù)組和對(duì)象才能正常使用搭伤。
往list里面添加新的一項(xiàng)使用解構(gòu)的方式要這樣寫
interface StateType {
name: string,
todoList: {
list: { name: string, done: boolean }[]
}
}
const App: React.FC = () => {
const [state, setState] = useState<StateType>({
name: 'lin',
todoList: {
list: [{ name: '學(xué)習(xí)react', done: true }]
}
})
const addTodoList = () => {
setState(state => ({
name: state.name,
todoList: {
list: [...state.todoList.list, { name: '學(xué)習(xí)immer源碼', done: true }]
}
}))
}
// ......
}
使用immer
import { produce } from 'immer'
interface StateType {
name: string,
todoList: {
list: { name: string, done: boolean }[]
}
}
const App: React.FC = () => {
const [state, setState] = useState<StateType>({
name: 'lin',
todoList: {
list: [{ name: '學(xué)習(xí)react', done: true }]
}
})
const addTodoList = () => {
setState(produce(state => {
state.todoList.list.push({ name: '學(xué)習(xí)immer源碼', done: true })
}))
}
// ......
}
immer原理
immer工作原理就是把produce方法傳入的第一個(gè)參數(shù)作為初始值initialState,通過es6的proxy進(jìn)行代理返回一個(gè)draft代理對(duì)象袜瞬,如果當(dāng)前環(huán)境不支持proxy則會(huì)使用Object.defineProperty()實(shí)現(xiàn)監(jiān)聽怜俐,進(jìn)行g(shù)et、set的攔截把只有改變了的部分會(huì)從draft映射到初始對(duì)象當(dāng)中邓尤。
draft代理對(duì)象的一些描述字段
{
modified, // 是否被修改過
finalized, // 是否已經(jīng)完成(所有 setter 執(zhí)行完拍鲤,并且已經(jīng)生成了 copy)
parent, // 父級(jí)對(duì)象
base, // 原始對(duì)象(也就是 obj)
copy, // base(也就是 obj)的淺拷貝,使用 Object.assign(Object.create(null), obj) 實(shí)現(xiàn)
proxies, // 存儲(chǔ)每個(gè) propertyKey 的代理對(duì)象汞扎,采用懶初始化策略
}
Produce
Immer 的核心源碼在 immerClass.ts 這個(gè)類上的produce方法季稳,produce方法里面會(huì)通過createProxy這個(gè)方法創(chuàng)建一個(gè)代理對(duì)象。
produce: IProduce = (base: any, recipe?: any, patchListener?: any) => {
/* 如果第一個(gè)參數(shù)傳入的是函數(shù)而不是一個(gè)對(duì)象且第二個(gè)參數(shù)不是函數(shù)澈魄,則采用curried函數(shù)的方式景鼠。*/
if (typeof base === "function" && typeof recipe !== "function") {
/* 第二個(gè)參數(shù)就變成了初始值 */
const defaultBase = recipe
/* 第一個(gè)參數(shù)變成了recipe函數(shù) */
recipe = base
const self = this
return function curriedProduce(
this: any,
base = defaultBase,
...args: any[]
) {
/* 將修改后的參數(shù)從新傳入正常的produce函數(shù) 代理對(duì)象draft實(shí)際是recipe函數(shù)的第一個(gè)參數(shù),初始值取的是produce的第二個(gè)參數(shù) */
return self.produce(base, (draft: Drafted) => recipe.call(this, draft, ...args))
}
}
// ... 略
let result
/* 只有對(duì)象 數(shù)組 和 "immerable classes" 可以進(jìn)行代理 */
if (isDraftable(base)) {
/* scope 是 immer 的一個(gè)內(nèi)部概念痹扇,當(dāng)目標(biāo)對(duì)象有復(fù)雜嵌套時(shí)铛漓,利用 scope 區(qū)分和跟蹤嵌套處理的過程 */
const scope = enterScope(this)
/* 核銷方法 createProxy 創(chuàng)建代理一個(gè)對(duì)象 */
const proxy = createProxy(this, base, undefined)
let hasError = true
try {
result = recipe(proxy)
hasError = false
} finally {
if (hasError) revokeScope(scope)
else leaveScope(scope)
}
if (typeof Promise !== "undefined" && result instanceof Promise) {
return result.then(
result => {
usePatchesInScope(scope, patchListener)
return processResult(result, scope)
},
error => {
revokeScope(scope)
throw error
}
)
}
usePatchesInScope(scope, patchListener)
return processResult(result, scope)
} else if (!base || typeof base !== "object") {
result = recipe(base)
if (result === NOTHING) return undefined
if (result === undefined) result = base
if (this.autoFreeze_) freeze(result, true)
return result
} else die(21, base)
}
createProxy
createProxy會(huì)判斷傳入的對(duì)象類型來采取不同的代理模式溯香,一般情況下都是會(huì)使用createProxyProxy也就是Proxy進(jìn)行代理
export function createProxy<T extends Objectish>(
immer: Immer,
value: T,
parent?: ImmerState
): Drafted<T, ImmerState> {
/* 根據(jù)傳入的對(duì)象類型來采取不同的代理模式 */
const draft: Drafted = isMap(value)
? getPlugin("MapSet").proxyMap_(value, parent) /* 傳入的base對(duì)象是Map類型 */
: isSet(value)
? getPlugin("MapSet").proxySet_(value, parent) /* 傳入的base對(duì)象是Set類型 */
: immer.useProxies_
? createProxyProxy(value, parent) /* 當(dāng)前環(huán)境支持Proxy則使用Proxy進(jìn)行代理 */
: getPlugin("ES5").createES5Proxy_(value, parent) /* 當(dāng)前環(huán)境不支持Proxy則使用Object.defineProperty() */
const scope = parent ? parent.scope_ : getCurrentScope()
scope.drafts_.push(draft)
return draft
}
createProxyProxy
createProxyProxy會(huì)把初始對(duì)象base當(dāng)作Proxy.revocable(target, handler)第一個(gè)參數(shù)target創(chuàng)建一個(gè)可撤銷的代理對(duì)象。
export function createProxyProxy<T extends Objectish>(
base: T,
parent?: ImmerState
): Drafted<T, ProxyState> {
const isArray = Array.isArray(base)
const state: ProxyState = {
type_: isArray ? ProxyType.ProxyArray : (ProxyType.ProxyObject as any),
// Track which produce call this is associated with.
scope_: parent ? parent.scope_ : getCurrentScope()!,
// True for both shallow and deep changes.
modified_: false,
// Used during finalization.
finalized_: false,
// Track which properties have been assigned (true) or deleted (false).
assigned_: {},
// The parent draft state.
parent_: parent,
// The base state.
base_: base,
// The base proxy.
draft_: null as any, // set below
// The base copy with any updated values.
copy_: null,
// Called by the `produce` function.
revoke_: null as any,
isManual_: false
}
let target: T = state as any
let traps: ProxyHandler<object | Array<any>> = objectTraps
if (isArray) {
target = [state] as any
traps = arrayTraps
}
const {revoke, proxy} = Proxy.revocable(target, traps)
state.draft_ = proxy as any
state.revoke_ = revoke
return proxy as any
}
- Proxy.revocable
- Proxy.revocable()方法可以用來創(chuàng)建一個(gè)可撤銷的代理對(duì)象浓恶。該方法的返回值是一個(gè)對(duì)象玫坛,其結(jié)構(gòu)為:{"proxy": proxy, "revoke": revoke} revoke 是一個(gè)撤銷方法,一旦某個(gè)代理對(duì)象被撤銷包晰,它將變的幾乎完全不可用湿镀,在它身上執(zhí)行任何的可代理操作都會(huì)拋出TypeError異常。
es5的情況
當(dāng)前環(huán)境不支持Proxy的情況最終會(huì)使用createES5Draft()方法來創(chuàng)建一個(gè)代理對(duì)象杜窄。
function createES5Draft(isArray: boolean, base: any) {
/* 判斷傳入是數(shù)組還是對(duì)象 */
if (isArray) {
const draft = new Array(base.length)
for (let i = 0; i < base.length; i++)
/* 數(shù)組情況則是循環(huán)對(duì)里面的值利用Object.defineProperty()進(jìn)行代理 */
Object.defineProperty(draft, "" + i, proxyProperty(i, true))
return draft
} else {
/* base傳入的是對(duì)象 */
const descriptors = getOwnPropertyDescriptors(base)
delete descriptors[DRAFT_STATE as any]
const keys = ownKeys(descriptors)
for (let i = 0; i < keys.length; i++) {
const key: any = keys[i]
descriptors[key] = proxyProperty(
key,
isArray || !!descriptors[key].enumerable
)
}
/* 這里沒有直接使用Object.defineProperty()肠骆,而是使用了Object.create()的第二個(gè)參數(shù)對(duì)傳入對(duì)象進(jìn)行例如get、set攔截的屬性配置 */
return Object.create(Object.getPrototypeOf(base), descriptors)
}
}
proxyProperty()方法返回代理對(duì)象的配置信息
function proxyProperty(
prop: string | number,
enumerable: boolean
): PropertyDescriptor {
let desc = descriptors[prop]
if (desc) {
desc.enumerable = enumerable
} else {
descriptors[prop] = desc = {
configurable: true,
enumerable,
get(this: any) {
const state = this[DRAFT_STATE]
if (__DEV__) assertUnrevoked(state)
// @ts-ignore
return objectTraps.get(state, prop)
},
set(this: any, value) {
const state = this[DRAFT_STATE]
if (__DEV__) assertUnrevoked(state)
// @ts-ignore
objectTraps.set(state, prop, value)
}
}
}
return desc
}
比較特別的是在傳入需要代理的對(duì)象base的類型是object的情況下沒有直接循環(huán)對(duì)象的屬性進(jìn)行Object.defineProperty()塞耕,而是使用了Object.create()的第二個(gè)參數(shù)對(duì)傳入對(duì)象進(jìn)行例如get蚀腿、set攔截的屬性配置。
Object.create()