這是 Pastate.js 響應(yīng)式 react state 管理框架系列教程罚勾,歡迎關(guān)注,持續(xù)更新献雅。
原理剖析
Pastate 的核心是一個(gè)管理不變式和響應(yīng)式 state 的 pastore, 下面來(lái)詳細(xì)介紹 pastore 的內(nèi)容睦霎。
帶路徑信息的 immutable state
Pastore 內(nèi)部使用一種獨(dú)特的帶路徑信息且不可變(immutable)的 state 作為應(yīng)用的數(shù)據(jù)源, 掛載在 store 的 imState 屬性上。在實(shí)現(xiàn)上犀勒,Pastore 把非空數(shù)據(jù)源都轉(zhuǎn)化為包裝類(lèi)型,具體流程如下圖,以 string 類(lèi)型的節(jié)點(diǎn)為例:
其中 __store__
為 state 所在的 store 實(shí)例的引用贾费,__xpath__
為該 state 節(jié)點(diǎn)在當(dāng)前 store 的 state 的路徑钦购,如 __xpath__ == '.foo.bar.baz'
。
為什么要使用 immutable 數(shù)據(jù)褂萧?
Pastate 使用一種自定義的具有 immutable 特性的數(shù)據(jù)類(lèi)型押桃, 而不是 immutable.js 的數(shù)據(jù)類(lèi)型。 Immutable 特性指的是數(shù)據(jù)是不可變的导犹,新的數(shù)據(jù)只能根據(jù)老的不可變數(shù)據(jù)去重新組合而成唱凯,下面舉個(gè)例子:
/** 可變數(shù)據(jù) */
let mutableData = {
foo: 'good',
bar: 'very good'
}
// 可以使用直接賦值的模式去改變數(shù)據(jù)
mutableData.foo = 'so good'
newData = mutableData
/** 不可變數(shù)據(jù) */
let immutableData = {
foo: 'good',
bar: 'very good'
}
// 不可修改老數(shù)據(jù),需要根據(jù)老數(shù)據(jù)去構(gòu)建新數(shù)據(jù)
newData = Object.assgin({}, immutableData, {foo: 'so good'}) // 可以使用 ES6 的 Object.assgin 函數(shù)
newData = {...immutableData, foo: 'so good'} // 也可以使用 ES7 的對(duì)象擴(kuò)展符
immutableData 實(shí)現(xiàn)了兩種效果:
- 可以把 state 變化過(guò)程的每個(gè)狀態(tài)保存起來(lái)谎痢,這為應(yīng)用的調(diào)試帶來(lái)了很多新的方法磕昼,如時(shí)間旅行調(diào)試
- 對(duì)象內(nèi)部的數(shù)據(jù)發(fā)生改變時(shí),該對(duì)象的引用需要進(jìn)行更新节猿,這符合函數(shù)式編程的要求票从,可以啟用 react 的 pure render ( PureComponent ) 按需快速渲染功能,使應(yīng)用的性能得到極大提高
為什么要帶路徑信息滨嘱?
如果我們手動(dòng)實(shí)現(xiàn) immutable state 的更新邏輯纫骑,會(huì)非常麻煩,假設(shè)有這樣一個(gè) state :
let state = {
foo: {
bar: {
baz: {
name: 'Peter'
}
}
},
}
// 我們手動(dòng)來(lái)修改 name 屬性的值
// 1. 如果是 mutable data
state.foo.bar.baz.name = 'Tom'
newData = state
// 2. 如果是 immutable data
newData = Object.assgin({}, state,
Object.assgin({}, state.foo,
Object.assgin({}, state.foo.bar,
Object.assgin({}, state.foo.bar.baz, {
name: 'Tom'
})
)
)
)
可見(jiàn)九孩,在手動(dòng)更新 immutble data 的深層嵌套數(shù)據(jù)時(shí)先馆,非常麻煩!而且如果有多個(gè)相同或不同嵌套深度的 state 節(jié)點(diǎn)需要更改躺彬,且這些更改時(shí)在運(yùn)行上連續(xù)進(jìn)行的但是在代碼上是封裝開(kāi)的煤墙,容易導(dǎo)致重復(fù)進(jìn)行沒(méi)意義的引用更新:
let state = {
foo: {
bar: {
baz: {
name: 'Peter'
},
id: '1234'
}
},
}
// 我們封裝 name 的修改邏輯
function changeName(){
Object.assgin..., Object.assgin..., Object.assgin..., Object.assgin...
}
// 我們封裝 age 的修改邏輯
function changeAge(){
Object.assgin..., Object.assgin..., Object.assgin...
}
function handleClick(){
shouldChangeName && changeName()
shouldChangeAge && changeAge()
}
上面的代碼,在 handleClick 的實(shí)現(xiàn)邏輯中宪拥,是否修改 name 或 age 是有可能單獨(dú)或同時(shí)發(fā)生的仿野,一般我們會(huì)把修改邏輯獨(dú)立封裝并按需調(diào)用。如果某個(gè)情況下需要連續(xù)運(yùn)行 changeName 和 changeAge她君, 那么在 changeAge 中的 3 個(gè) Object.assgin 是重復(fù)的脚作,沒(méi)有實(shí)際意義。
你可能會(huì)考慮使用 immutable.js 庫(kù)的數(shù)據(jù)結(jié)構(gòu)缔刹,immutable.js 提供完善的 immutable 數(shù)據(jù)類(lèi)型和操作方式支持球涛,但在 react 項(xiàng)目中使用 immutable.js 有以下不足:
- immutable.js 的數(shù)據(jù)格式與原生 JS 格式互不兼容,在渲染時(shí)校镐,經(jīng)常需要在合適的位置使用 xxx.get() 或 xxx.toJS() 獲取原生 js 格式(使用 toJS() 會(huì)使所有子節(jié)點(diǎn)的引用更新亿扁,如果直接使用 fromJS(state).toJS() 只是對(duì) state 進(jìn)行 "深層復(fù)制", 不能實(shí)現(xiàn)用 immutable 數(shù)據(jù)來(lái)實(shí)現(xiàn)快速按需渲染的效果)
- immutable.js 的數(shù)據(jù)操作模式是基于"節(jié)點(diǎn)名稱(chēng) / 節(jié)點(diǎn)名稱(chēng)數(shù)組" 來(lái)索引節(jié)點(diǎn)的,如
imState.setIn(['info', 'basic', 'name'], 'new Name')
鸟廓,而不是用原生 JS 對(duì)象屬性模式來(lái)索引節(jié)點(diǎn), 如state.info.basic.name
从祝。因此編輯器無(wú)法分析可選屬性襟己,也無(wú)法索引結(jié)果的類(lèi)型,這使得編程起來(lái)效率低牍陌,而且比較容易出錯(cuò)擎浴。 - immutable.js 確實(shí)完備,但過(guò)于龐大復(fù)雜毒涧,學(xué)習(xí)成本高贮预,許多功能在一般的實(shí)際開(kāi)發(fā)中不需要使用到,對(duì)初學(xué)者不太友好链嘀。
因此,Pastate 在每個(gè) state 節(jié)點(diǎn)附加了 節(jié)點(diǎn)路徑信息 档玻,并實(shí)現(xiàn) state 的自動(dòng)化 immutable 且按需更新引用的功能怀泊!讓我們可以享受 immutable 數(shù)據(jù)的優(yōu)勢(shì)的同時(shí),拋開(kāi)使用 immutable 數(shù)據(jù)的復(fù)雜性误趴。
pastate imState 內(nèi)部操作機(jī)制
Pastore 內(nèi)部實(shí)現(xiàn)了一個(gè)異步操作(operation)列表霹琼,并定義了一套 operation 壓入方法:
- set: 設(shè)置值
set(imState.foo.bar, newValue)
- merge: 合入新值
merge(imState.foo, {bar: newValue})
- update: 更新值
update(imState.foo.bar, n => n+ '!')
這三個(gè)方法會(huì)向 store 提交一個(gè) operation, store 把收到 operation 后,暫時(shí)存在一個(gè) operation 隊(duì)列中凉当,在下一個(gè)事件循環(huán)(event loop)時(shí)對(duì) operation 隊(duì)列進(jìn)行統(tǒng)一處理枣申,再批量處理中實(shí)現(xiàn)按需引用更新,具體流程圖如下:
在每個(gè) operation 被 push 的前后及在 operation 被 reduce 的前后看杭,pastate 都設(shè)計(jì)了相應(yīng)的生命周期函數(shù)忠藤,你可以通過(guò)這些生命周期函數(shù)接收、過(guò)濾或控制operation楼雹,實(shí)現(xiàn)自定義邏輯模孩。
-
stateWillAddOperation
將要增加 operation 時(shí)會(huì)被調(diào)用 -
stateWillReduceOperations
將要執(zhí)行 operations 時(shí)會(huì)被調(diào)用 -
stateWillApplyOperation
將要執(zhí)行一個(gè) operation 時(shí)會(huì)被調(diào)用 -
stateDidAppliedOperation
執(zhí)行完一個(gè) operation 后會(huì)被調(diào)用 -
stateDidReducedOperations
執(zhí)行完 operations 時(shí)執(zhí)行
詳見(jiàn) API文檔。
響應(yīng)式 state 鏡像
直接使用 set(imState.foo.bar, newValue)
或 merge(imState.foo, {bar: newValue})
等來(lái)進(jìn)行 immutable state 的操作雖然已經(jīng)挺方便贮缅,但和我們平時(shí)修改 js 變量的模式還是不太一樣榨咐,因此 pastate 為 immutable state 構(gòu)建了一套響應(yīng)式的鏡像 state 來(lái)自動(dòng)調(diào)用 set、merge谴供、或 update 來(lái)生成 operation, 使得我們可以用賦值符號(hào) =
或數(shù)組操作參數(shù)如 push
块茁、pop
等來(lái)間接地對(duì) immutable state 進(jìn)行操作,對(duì)于處學(xué)者來(lái)說(shuō)無(wú)需任何學(xué)習(xí)成本桂肌。 Pastate 內(nèi)部使用 Object.defineProperty 的方式為響應(yīng)式 state 節(jié)點(diǎn)定義相應(yīng)的 getter 和 setter, 以實(shí)現(xiàn)通過(guò)賦值或數(shù)組函數(shù)發(fā)起 set, merge 或 update operation数焊,對(duì)接 pastate 異步 operation 處理引擎,流程圖如下:
視圖響應(yīng)引擎
當(dāng) pastore 完成一批 operations 的 reduce 過(guò)程時(shí)崎场,會(huì)對(duì)外發(fā)出一個(gè)視圖更新信號(hào)昌跌,通過(guò) pastore 的 dispatch 成員函數(shù)發(fā)出。Pastate 內(nèi)部實(shí)現(xiàn)了 pastate-redux 連接器, 默認(rèn)使用 redux 作為 state 發(fā)生改變時(shí)的響應(yīng)器照雁,并由 react-redux 去連接 react 組件組件蚕愤,從而實(shí)現(xiàn) pastate-react答恶。同時(shí), pastate 使用 redux 的基本規(guī)則實(shí)現(xiàn)多模塊邏輯萍诱,因此悬嗓,你可以使用 redux 生態(tài)系統(tǒng)相關(guān)組件或開(kāi)發(fā)工具類(lèi)配合 pastate 應(yīng)用,我們非常擁抱 redux 生態(tài)系統(tǒng)裕坊。
另一方面包竹,你完全可以自行實(shí)現(xiàn)一套視圖響應(yīng)邏輯, 甚至實(shí)現(xiàn) pastore 與 vue 或 angular 對(duì)接,歡迎嘗試籍凝。
API 文檔
其他信息
在 typescript 中使用
Pastate 使用 typescript 進(jìn)行開(kāi)發(fā)周瞎,具有完善的類(lèi)型支持,可以直接引入 pastate 并直接在 React Typescript 項(xiàng)目中使用饵蒂。
在 react-native 中使用
我們正嘗試在 react-native 中使用 pastate, 你也可以加入嘗試声诸,如果發(fā)現(xiàn)什么問(wèn)題,歡迎提交 issue 告訴我們~
在 preact 或 nerv 中使用
Pastate 在原理上可以與類(lèi) react (react alternative) 框架如 preact 或 nerv 配合使用退盯,但未進(jìn)行全面測(cè)試彼乌,如果你嘗試了,歡迎提交 issue 告訴我們~
相關(guān)資源
敬請(qǐng)期待渊迁,如果你基于 pastate 開(kāi)發(fā)一些項(xiàng)目慰照,歡迎提交 issue 告訴我們~
貢獻(xiàn)指引
- fork pastate 的 github 倉(cāng)庫(kù): https://github.com/BirdLeeSCUT/pastate
- clone 并安裝 npm 包
$ git clone <your repo>
$ cd pastate
$ yarn install
- 運(yùn)行 pastate 開(kāi)發(fā)包
$ yarn start
- 實(shí)時(shí)測(cè)試 pastate 開(kāi)發(fā)包
$ yarn test
- 編譯 pastate 開(kāi)發(fā)包
$ yarn dist
目標(biāo)代碼會(huì)編譯到 /dist 目錄下
- 提交代碼,發(fā)起 pull request琉朽。
英文翻譯
如果覺(jué)得 pastate 非常不錯(cuò)毒租,歡迎參與 pastate 文檔英文翻譯計(jì)劃,讓更多的國(guó)外友人也能?chē)L試 pastate 箱叁。如果你有意愿蝌衔,請(qǐng)?jiān)?github 聯(lián)系。