本文從動機脈絡(luò)聊聊對react生態(tài)中的狀態(tài)相關(guān)技術(shù)的演化過程。
個人理解,歡迎討論
響應式渲染框架
這里只聊react的狀態(tài)和視圖渲染相關(guān)內(nèi)容,不聊底層的Virtual DOM
react是一個mvvm框架,作為一個響應式渲染設(shè)計,當自身的模型(狀態(tài))發(fā)生變化時,會自動刷新(re-render)當前視圖顯示最新的模型(狀態(tài))數(shù)據(jù)。
那是如何監(jiān)聽狀態(tài)發(fā)生變化呢?react本著極簡的api設(shè)計理念,遵循函數(shù)式編程中的不可變對象理念,對于狀態(tài)實現(xiàn)的特別簡單,只提供了一個setStateAPI。在react組件中,需要視圖發(fā)生變化時,只需要對調(diào)用setState進行數(shù)據(jù)時圖進行改變,就會觸發(fā)當前組件的re-render,完成更新。所以此時可以理解為,react是響應式設(shè)計的渲染框架,但其狀態(tài)不是響應式模式,是一個命令式狀態(tài)框架。如下面的代碼:
class Page extends React.Component<{}, {no: number}> {
constructor() {
super({});
this.state = {
no: 0,
};
}
render() {
console.log('render執(zhí)行');
return <h1 onClick={() => {
this.setState({
no: 0
});
}}>Hello, {this.state.no}</h1>;
}
}
當執(zhí)行setState時,雖然狀態(tài)no前后都是0, 但是組件的render還是會被重新執(zhí)行。
這樣簡單的設(shè)計,既是react的優(yōu)點又是react的缺點。優(yōu)點是react只提供最底層的狀態(tài)更新api, 使用者可以使用市面上任意的其他狀態(tài)框架,整個框架不顯得笨重。缺點就是不能開箱即用,加重使用者心智負擔。
react中的單向數(shù)據(jù)流設(shè)計,在整個組件樹中,只允許狀態(tài)從頭流到葉子節(jié)點的原因是什么呢?這是由于每一個組件的子節(jié)點都是執(zhí)行在render函數(shù)中,類似于一個遞歸樹。當當前組在re-render時,就會重新調(diào)用子節(jié)點重新執(zhí)行render,也就整個當前組件以下的整個組件都會re-render。所以當只有父向子的單向數(shù)據(jù)流時,這個調(diào)用流程只需要調(diào)用一次就可以把當前變化后的數(shù)據(jù)“響應”到視圖上。代碼效果:
const Foo: React.FC<Record<string, unknown>> = () => {
console.log('Foo被重新渲染了');
return (
<div>
Foo
</div>
);
}
const Bar: React.FC<Record<string, unknown>> = () => {
console.log('Bar被重新渲染了');
return (
<div>
Bar
</div>
);
}
class Parent extends React.Component {
constructor() {
super({});
this.state = {
no: 0,
};
}
render() {
console.log('Parent被重新渲染了');
return (
<div onClick={() => this.setState((preState) => ({ no: preState.no + 1 }))}>
<Foo />
<Bar />
</div>
);
}
}
當Parent中的狀態(tài)發(fā)生變化時,會發(fā)現(xiàn)Parent, Foo, Bar組件都發(fā)生了re-render。
react這種觸發(fā)時圖更新的機制在絕大多數(shù)情況下都會造成性能損失。因為數(shù)據(jù)更新是常態(tài),特別是在一些持續(xù)觸發(fā)的事件中,每一次都更新整個節(jié)點樹,當業(yè)務場景體量稍微大一點導致react組件節(jié)點非常多時,碰到持續(xù)更新狀態(tài)的情況下性能就會非常差。這也就導致react生態(tài)中,狀態(tài)理念,框架層出不窮的根本原因。
雖然組件重新調(diào)用渲染函數(shù)(
render)由于Virtual DOM的diff算法不一定更新dom結(jié)構(gòu)(也就是最終視圖),但是render函數(shù)的反復執(zhí)行,也開銷特別大。
react單向數(shù)據(jù)流的規(guī)定保證當當前組件發(fā)生變化時,只需要重新渲染自己, 不會去渲染父組件和兄弟組件。所以下面的用法:
const Foo: React.FC = ({ actionRef }) => {
console.log('Foo被重新渲染了');
const [no, setNo] = React.useState(0);
React.useImperativeHandle(actionRef, () => ({ no }));
return (
<div onClick={() => setNo(preNo => preNo + 1)}>
Foo
</div>
);
}
const Bar: React.FC = ({ no }) => {
console.log('Bar被重新渲染了');
return (
<div>
Bar, {no}
</div>
);
}
const Parent: React.FC<Record<string, unknown>> = (props) => {
console.log('Parent被重新渲染了');
const actionRef = React.useRef(null);
return (
<div>
<Foo actionRef={actionRef} />
<Bar no={(actionRef.current || {}).no || 0} />
</div>
);
};
當在組件Foo中觸發(fā)狀態(tài)改變,只會觸發(fā)Foo組件re-render,雖然Parent和Bar也都是用了Foo的數(shù)據(jù)(注意是數(shù)據(jù)而不是狀態(tài),通過ref傳遞了), 但是不會re-reder。
SCU
為了解決react默認狀態(tài)變更時觸發(fā)整個當前組件整個子節(jié)點樹更新的性能問題,react提供了SCU(shouldComponentUpdate)機制。使用者可以在這個生命周期函數(shù)中,根據(jù)觸發(fā)當前組件re-render的props,state和context跟當前還未re-render值進行對比,決定該組件是否的re-render。
為了簡化SCU的操作,react提供了PureComponent提供默認的比對算法,也就是對屬性集對象(props),狀態(tài)集對象(state)和上下文對象context進行頂層屬性的對比(淺對比),對象值采用的是引用對比方式。這樣在祖先節(jié)點狀態(tài)更新觸發(fā)整個節(jié)點樹更新時,當前組件會判斷如果傳入的屬性對比后發(fā)現(xiàn)沒有更新時,或者當前組件調(diào)用了setState但是狀態(tài)的值沒有發(fā)生變化時,都會跳過本組件的re-render,進而提高性能。此時React從命令式響應框架轉(zhuǎn)為比對式數(shù)據(jù)響應框架(Comparison reactivity)。
如下面的代碼點擊文本將不會觸發(fā)render的函數(shù)重新執(zhí)行(如果不是繼承PureComponent的話render中的日志會持續(xù)打印):
class Welcome extends React.PureComponent<{}, {name: string}> {
constructor() {
super({});
this.state = {
name: '123',
};
}
render() {
console.log('render執(zhí)行');
return <h1 onClick={() => {
this.setState({
name: '123'
});
}}>Hello, {this.state.name}</h1>;
}
}
但PureComponent也會導致如下面代碼的問題:
class Welcome extends React.PureComponent<{}, {foo: {name: string}}> {
constructor() {
super({});
this.state = {
foo: {name: '123'},
};
}
render() {
console.log('render執(zhí)行');
return <h1 onClick={() => {
const { foo } = this.state;
foo.name = '456';
this.setState({ foo });
}}>Hello, {this.state.foo.name}</h1>;
}
}
當我們點擊后是期望視圖有更新,顯示為456,但實際情況下不會。因為如上文所說,PureComponent只會對比props,state和context中頂級屬性值,并且對象值只采用引用對比(淺對比模式)。而在代碼中,狀態(tài)foo對象雖然內(nèi)容變了,但是引用不變,所以react會認為狀態(tài)沒有發(fā)生改變,從而跳過更新。為了解決這個問題,react提出了不可變狀態(tài)對象的理念。簡單的理解為,存放在state中的對象數(shù)據(jù),在自身引用沒有發(fā)生變化時,不允許其內(nèi)部的值發(fā)生變化,也就是下面的代碼是不推薦的:
const { address, user, dataList } = this.state;
// 禁止在user引用值沒有變化時,改變了其內(nèi)部值
user.name = 'foo';
// 特別容易發(fā)生在數(shù)組中
dataList.push('newItem');
// 下面這種是常犯的一種
address.city = 'changsha';
this.setState({
address,
});
而是推薦下面這種:
this.setState({
user: {
name: 'foo',
...this.state.user,
},
dataList: [...this.state.dataList, 'newItem'],
address: {
city: 'changsha',
...address,
},
});
整個react渲染就像動畫片放映一樣,不是局部內(nèi)容的變化,而是一幀一幀的整體替換。當需要畫面變化時,就需要構(gòu)建從上一幀復制內(nèi)容到下一幀,然后在變化。禁止直接對老的幀直接改動。
上面的案例中,平常開發(fā)中稍微注意就可以遵循。但在一些復雜的場景下,如可編輯表格的每個行數(shù)據(jù)操作,在不方便對整個狀態(tài)對象(深度封裝下)進行創(chuàng)建新的對象時,就容易誤操作。
為了避免無意中沒有遵循react的immutable理念,可以采取兩種方式:
- 使用一些保證狀態(tài)為不可變對象的的lint規(guī)則(本人尚未發(fā)現(xiàn)社區(qū)有這一塊的內(nèi)容);
- 使用immutable.js;
Hook
在class componets開發(fā)過程中,如果使用原生的react狀態(tài)的話,將會有以下缺陷(使用hook動機):
- 在組件之間復用狀態(tài)邏輯很難;
react中一切皆組件,對于公共代碼可以封裝成新的組件。但是對于一些公共的狀態(tài)邏輯,在mixin被廢除之后,卻沒有提供好的方式去封裝。而Hook可以在不改變組件結(jié)構(gòu),就可以復用狀態(tài)邏輯。相對于使用控制組件,Hook使用簡單,二次封裝非??焖?。相對于使用mixin,Hook可以理解為mixin的升級版,維持住了調(diào)用鏈,解決了mixin中調(diào)試困難的難題。 - 復雜組件變得難以理解
由于以前狀態(tài)邏輯難以服用,就會導致一些組件中堆砌了大量的狀態(tài)邏輯。特別是一些作為控制組件的容器組件,其中堆滿了各種子組件之間用于狀態(tài)通信的邏輯。使用Hook之后,可以快速方便的對狀態(tài)進行分類放入不同的模塊中,組件代碼干凈清爽。 - 完全函數(shù)式編程
使用hook可以完全擺脫class變成,擺脫怪異的this工作方式。函數(shù)式編程更稱靈活,可測試。hook可以理解為函數(shù)式編程中狀態(tài)的實現(xiàn),不僅僅在react中使用。
hook中的關(guān)于狀態(tài)這一塊的api為useState,可以理解為把class中的this.State可以拆成多份去執(zhí)行,一個useState就是一個狀態(tài),廢棄了狀態(tài)集對象概念。并且在useState中一個更大的進步是吸取了以前教訓,直接引入了對比式更新,如果設(shè)置根當前值一樣的值時,整個組件將不會re-render:
const Foo: React.FC<Record<string, unknown>> = (props) => {
const [no, setNo] = React.useState(0);
console.log('Foo重新渲染了');
return (
<div onClick={() => setNo(0)}>
Foo, {no}
</div>
);
};
比對式更新不僅僅在useState中被使用,在其他的hook如useMemo, useEffect中的deps的參數(shù)中,都采取同樣的方式。
有了比對式更新,hook引語了一些響應式狀態(tài)流中的計算屬性概念(useMemo)。
跨組件傳遞狀態(tài)
上文中的狀態(tài)都是處于單個組件內(nèi)部,在實際的場景中,還需要考慮在組件之間進行狀態(tài)通信。
react自帶方案
對于簡單的向另一個組件內(nèi)傳遞狀態(tài),可以使用props。props可以看作是父組件的狀態(tài)。當父組件的狀態(tài)發(fā)生變化時,會觸發(fā)當前組件的re-render(默認情況下),從而獲取到了最新的狀態(tài)。這種方式跟木偶組件有點像,容器(父)組件負責狀態(tài)邏輯,展示節(jié)點將狀態(tài)呈現(xiàn)在視圖。
如果一個組件需要接收祖先節(jié)點的狀態(tài),此時如果使用props的話,會特別繁瑣,需要在整個樹路徑上都維持這個屬性傳遞下來(props透傳)。這種方式造成了整個鏈路都耦合底層組件的狀態(tài)使用,違反了編程原則造成后期維護特別困難。為了解決這個問題,react提供了Context方案。
但Context也只解決了同一個鏈路下組件的通信問題,如果是兄弟節(jié)點,或者是“親戚”(沒有直系關(guān)系)節(jié)點之間如何通信呢?react推薦使用狀態(tài)提升方式:對于需要通信的兩個組件,首先找到它們的共有祖先節(jié)點(對應組件可以稱為容器組件或者控制組件),然后將需要通信的數(shù)據(jù)作為這個祖先節(jié)點的狀態(tài)。當任意一個組件改變共享狀態(tài)時,會觸發(fā)整個祖先節(jié)點的re-render,默認情況下,這個祖先節(jié)點的所有子節(jié)點也會re-render, 也就是另一個組件就會獲取到最新的狀態(tài)值,完成整個狀態(tài)傳遞。
除react自帶方案之后,下面將會講幾種react生態(tài)中常見幾種類型的狀態(tài)庫。他們有的是基于react自帶方案的工具庫,有的是為了解決狀態(tài)提升而采取的其他方式。
unstated-next
unstated-next是unstated在react hooks中理念的重新實現(xiàn)。一個偽代碼的實現(xiàn)為:
function createContainer(useHook) {
const Context = React.createContext(null);
function Provider(props) {
const value = useHook(props.initialState);
return React.createElement(Context.Provider, { value }, props.children);
}
function useContainer() {
return React.useContext(Context) ?? throw new Error("Component must be wrapped with <Container.Provider>");
}
return { Provider, useContainer };
}
簡單的理解就是,將你自定義的hooks中的狀態(tài)存儲在context中進行組件共享。
那么它的優(yōu)點就是:簡單,其實就是對context二次封裝,雖然react16中context相對于以前版本簡便性已經(jīng)有了極大的提高,但是在修改context中數(shù)據(jù)的方式下沉到自組件中還是比較繁瑣,而unstated-next恰恰可以解決這個點,可以狀態(tài)跟update函數(shù)快速維護。如:
function useCounter() {
let [count, setCount] = useState(initialState)
let decrement = () => setCount(count - 1)
let increment = () => setCount(count + 1)
return { count, setCount, decrement, increment }
}
let Counter = createContainer(useCounter)
同時它也解決了一個狀態(tài)提升帶來的問題:當一個容器下的組件需要通信的數(shù)據(jù)過多時,會發(fā)現(xiàn)這個容器下堆滿了各種狀態(tài)。且不同組件之間相互通信的狀態(tài)直接堆積在控制節(jié)點中,非常難以維護。unstated-next可以幫助我們對堆砌在容器內(nèi)點中的各種狀態(tài)進行封裝管理,維護在單獨的數(shù)據(jù)文件中,保持容器組件的清爽。
缺點:本質(zhì)上是一個工具庫,狀態(tài)提升帶來的其他問題它都有。
unstated是unstated-next在class components時代同理念庫,也是對context的二次封裝工具庫,不在單獨拿出來講解。
簡單事件流實現(xiàn)
狀態(tài)提升的方式解決組件通信會導致狀態(tài)集中在上層組件中,在渲染的過程中也會導致過多的額外組件re-render,造成性能低下。那有什么方式可以精準的找到并只渲染需要渲染的組件呢?可以用事件流。
如下面的代碼:
import { TinyEmitter } from 'tiny-emitter';
const emitter = new TinyEmitter();
const Foo: React.FC<Record<string, unknown>> = () => {
const [message, setMessage] = React.useState<string>('init');
React.useEffect(() => {
emitter.on('updateMessage', (messageArg: string) => {
setMessage(messageArg);
});
}, []);
return (
<div>
Foo: {message}
</div>
);
}
const Bar: React.FC<Record<string, unknown>> = () => {
return (
<div onClick={() => {
emitter.emit('updateMessage', 'barClick');
}}>
Bar
</div>
);
}
const Normal: React.FC<Record<string, unknown>> = () => {
console.log('normal被重新渲染了');
return (
<div>
Normal
</div>
);
}
const Parent: React.FC<Record<string, unknown>> = (props) => {
console.log('props', props);
return (
<div>
<Foo />
<Bar />
<Normal />
</div>
);
};
當Bar組件跨組件非直系節(jié)點觸發(fā)Foo狀態(tài)變化時,只有Foo組件會re-render, 其他兄弟節(jié)點Normal和父節(jié)點Parent都不會re-render, 連Bar自己都不會re-render。
在實際使用過程中,一般不會像案例這樣使用。事件流偏向于命令式編程,且只是傳遞了想要修改狀態(tài)的指令,對于如何修改,需要放在監(jiān)聽器里面,也就是提供修改狀態(tài)的組件內(nèi)部。也就是外部組件在當前組件沒有提供狀態(tài)修改事件之前,是無法進行狀態(tài)修改的。在需要大量數(shù)據(jù)狀態(tài)需要通信時,由于事件流太偏向于底層,大量開發(fā)時不方便復用。一般都是基于事件流中改造為發(fā)布訂閱模式,進行聲明式的狀態(tài)管理。
如將上面的案例中事件流狀態(tài)傳遞,流程圖示意:

這里做一個小的知識點(個人以前的疑惑,所以花費篇幅說明),在案例中事件的方式并不需要使用Context,為什么那些狀態(tài)框架,如redux, mobx都有一個放在根節(jié)點位置Provider呢? 如:
import { Provider } from 'react-redux'
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
這是由于如上面的簡單事件流案例,會有一個事件實例emitter,如果需要支持多實例或者方便在組件中獲取到,就需要提供一個Provider存放在Context中。但是由于不是將數(shù)據(jù)存在了Context中,而是狀態(tài)管理器的實例存儲在Context中,所以狀態(tài)變化時,是不會觸發(fā)頂層節(jié)點re-render從而導致整個節(jié)點樹都re-render?;谑录鞯目蚣軙ㄟ^事件精準的找到目標組件并觸發(fā)他的re-render。所以相對來說,redux, mbox這種狀態(tài)管理框架,比使用react原生提供的方案性能會好的多。
redux
redux將所有的狀態(tài)都收歸在自己內(nèi)部,不在使用react狀態(tài)。狀態(tài)由react中托管到redux后,對比于上面的簡單事件流流程過程,就變成了:

使用redux時,組件中通過dispatch觸發(fā)事件,事件的值傳遞為aciton(真實事件系統(tǒng)里面稱為event對象),這個事件首先會被reducer監(jiān)聽到。redux規(guī)定只能在reducer中修改狀態(tài),當狀態(tài)修改之后,就會觸發(fā)的subscribe,在subscribe會觸發(fā)目標的組件re-render, 如案例:
import { createStore } from 'redux'
function counterReducer(state = { value: 0 }, action) {
switch (action.type) {
case 'counter/incremented':
return { value: state.value + 1 }
case 'counter/decremented':
return { value: state.value - 1 }
default:
return state
}
}
let store = createStore(counterReducer)
store.subscribe(() => console.log(store.getState()))
store.dispatch({ type: 'counter/incremented' }) // 會打印出{value: 1}
store.dispatch({ type: 'counter/incremented' }) // 會打印出{value: 2}
store.dispatch({ type: 'counter/decremented' }) // 會打印出{value: 1}
redux中使用reducer替代了react中的setState函數(shù),沒有命令式的改變狀態(tài)的含義(但個人覺得其實就是放在了命令式調(diào)用dispatch而已),也就是說一旦觸發(fā)了reducer的執(zhí)行,就意味有狀態(tài)由發(fā)生變化。就會觸發(fā)監(jiān)聽器subscribe。
根據(jù)上面的案例,會發(fā)現(xiàn)任意一個狀態(tài)發(fā)生變化時(執(zhí)行dispatch),所有的副作用都會執(zhí)行。這需要在subscribe中對組件進行是否需要re-render時,需要深入判斷當前組件依賴的狀態(tài)是否發(fā)生變化。在react-redux(8.0版本connect)中實現(xiàn)的邏輯是:
actualChildProps = useSyncExternalStore(
subscribeForReact,actualChildPropsSelector,
getServerState
? () => childPropsSelector(getServerState(), wrapperProps)
: actualChildPropsSelector
)
其中useSyncExternalStore為官方提供的hook,也就是說當redux中的狀態(tài)發(fā)生變化時,就會觸發(fā)各個connectHOC中的訂閱器,訂閱器會執(zhí)行傳遞進去的mapStateToProps函數(shù),獲取當前組件需要從store獲取的狀態(tài),拿到狀態(tài)后,還會進行淺對比(跟react hook對比算法一致,對比各個狀態(tài)的引用值),如果發(fā)現(xiàn)狀態(tài)沒有變化,那么返回的是一個歷史值(不會觸發(fā)更新),如果狀態(tài)有變化,則返回新的狀態(tài)對象,觸發(fā)當前組件re-render。
在新版本的redux中,直接提供了hook useSelector來觸發(fā)目標組件的re-render, 核心邏輯跟connect中基本一致:
const { store, subscription, getServerState } = useReduxContext()!
const selectedState = useSyncExternalStoreWithSelector(
subscription.addNestedSub,
store.getState,
getServerState || store.getState,
selector,
equalityFn
)
在上下文中獲取當前的store實例,然后實現(xiàn)React.useSyncExternalStore, 在store中的狀態(tài)發(fā)生變化時,根據(jù)選擇器判斷是否需要觸發(fā)當前狀態(tài)發(fā)生改變,從而決定當前的組件是否需要re-render。
另外由于所有的狀態(tài)都是推薦使用redux去管理(單一數(shù)據(jù)源),那么存放在redux中的狀態(tài)將會非常多。為了方便管理,redux提供了命名空間的概念。
從上面的討論可以看出,redux跟react的思想是極其相近的,都是遵循狀態(tài)的不可變immutable,都采用比較式數(shù)據(jù)響應框架(Comparison reactivity)。redux提出了一些新的理念(方法論),利用一些編程范式,規(guī)范整個狀態(tài)的變化周期,防止誤操作。但對于如何準確的找到需要渲染的組件,redux還是在react-redux中使用老辦法,對于狀態(tài)進行前比較。這導致其還是沒有解決react中的狀態(tài)管理的一些缺點:
- immutable編程帶來的一些心智負擔
- 誤操作導致一些非必要的組件re-render
而對于redux中無法準確找到需要re-render的組件的難題,而社區(qū)慢慢出現(xiàn)利用一些代理的技術(shù)手段(es5中的Object.definePropert或者es6的ProxyAPI)進行狀態(tài)管理自動收集的方式來解決。這些解決方案可以降低開發(fā)者心智負擔和難度,下文將要講解的mbox就是這其中的一種。
mobx
類似框架: Recoil, zustand, jotai
react是一直走不可變對象immutable理念,但mutable實在是太香了,特別競爭框架vue,Solidjs,Svelte都采用了。故meta公司也出了一個mutable框架,支持在react中使用訂閱響應式狀態(tài)管理(Subscription reactivity)。
mobx跟redux一樣,都是將所有的狀態(tài)從react中拿出來自己管。但不同于redux的單一數(shù)據(jù)流理念,可以根據(jù)需要通信的數(shù)據(jù)靈活創(chuàng)建不同的狀態(tài)對象,方便搭配hook使用。
相對比于簡單事件流,在狀態(tài)放入內(nèi)部管理后,mobx不僅利用Object.definePropert或者ProxyAPI對創(chuàng)建的數(shù)據(jù)對象進行攔截監(jiān)聽,還能收集到使用這些狀態(tài)的代碼自動設(shè)置監(jiān)聽器(在mobx中叫做派生)。這樣在對象的值發(fā)生變化時,就會自動觸發(fā)事件,執(zhí)行對應的監(jiān)聽器。
在這種方式下,需要開發(fā)者做的事情就只剩下定義狀態(tài),聲明副作用(派生函數(shù))即可。整個流程為:

在這里我們不過多的討論mobx狀態(tài)的底層原理和它的一些新的概念。對于我們關(guān)注的mobx如何將它內(nèi)部的狀態(tài)變化后觸發(fā)對應的組件re-render的流程,通過下面的代碼可以用于討論:
const state = observable({ value: 0 });
const increment = action(() => {
state.value++
});
autorun(() => {
console.log("Energy level:", state.value);
})
increment(); // Energy level: 1
action中可以直接改變狀態(tài),當某個狀態(tài)被改變后,mobx會自動執(zhí)行它的的派生函數(shù)(類似于上文說的監(jiān)聽器),并且由于整個狀態(tài)都是響應式的,所以派生函數(shù)可以延長,實現(xiàn)具有緩存作用的計算屬性機制。最后會觸發(fā)一個派生函數(shù)(autorun)。mobx會自動對派生函數(shù)中使用的狀態(tài)進行收集,保證只有使用的狀態(tài)發(fā)生變化時才會觸發(fā)該autorun函數(shù)。
整個效果可以看到,在mobx中,完全可以廢棄setState這種命令式的通知框架狀態(tài)已經(jīng)更新的方式。采用Object.definePropert或者ProxyAPI實現(xiàn)聲明式的監(jiān)聽到狀態(tài)的變化。并且通知具體的組件的re-render,也不需要中間加一個對比層,直接通過執(zhí)行過程中維護的監(jiān)聽隊列,自動完成對應組件的更新觸發(fā)。
關(guān)于mobx是符合自動收集到派生函數(shù)中狀態(tài)的使用信息,從而自動根據(jù)狀態(tài)的變化只觸發(fā)需要變化的申請操作是如何實現(xiàn)的。個人猜測是在初次執(zhí)行的時候,對狀態(tài)的get操作進行攔截收集的。推測代碼如下:
const state = observable({
value1: 0,
value2: 0,
});
const increment = action(() => {
state.value1++;
});
autorun(() => {
console.log("autorun1 value1:", state.value1);
})
let a = false;
autorun(() => {
if (a) {
console.log("autorun2 value1:", state.value1);
}
console.log("autorun2 value2:", state.value2);
})
const Foo: React.FC<Record<string, unknown>> = () => {
return (
return <h1 onClick={() => {
a = true;
increment();
}}>Hello, {this.state.no}</h1>;
);
};
當調(diào)用increment函數(shù)后,你會發(fā)現(xiàn)autorun2也不會被執(zhí)行,只有autorun1函數(shù)被執(zhí)行了。這是由于在第一次執(zhí)行兩個autorun函數(shù)時,由于對于變量a為false,導致只收集到了autorun2對value2的依賴,所以當value1發(fā)生變化時,autorun2還是不會執(zhí)行。 對上面的代碼進行下改造:
const state = observable({
value1: 0,
value2: 0,
value3: 0,
});
const increment = action(() => {
state.value1++;
if (state.value1 > 5 && !state.value3) {
state.value3 = 1;
}
});
autorun(() => {
console.log("state1 value1:", state.value1);
})
autorun(() => {
if (state.value3 > 0) {
console.log("state2 value:", state.value1, state.value3);
}
console.log("state2 value2:", state.value2);
})
可以發(fā)現(xiàn),前面五次執(zhí)行increment對state.value1的變化,不會觸發(fā)autorun2的執(zhí)行,當?shù)谖宕螌?code>state.value3也進行改變后,后續(xù)的每一次state.value1的變化(此時state.value3已經(jīng)不在變化)也會觸發(fā)autorun2的執(zhí)行,所以可以推測出收集不僅僅在第一次執(zhí)行的時候收集完畢就一成不變,而是在每次執(zhí)行后會更新對應狀態(tài)的監(jiān)聽隊列。