react狀態(tài)總結(jié)

本文從動機脈絡(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,雖然ParentBar也都是用了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,statecontext跟當前還未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,statecontext中頂級屬性值,并且對象值只采用引用對比(淺對比模式)。而在代碼中,狀態(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),可以使用propsprops可以看作是父組件的狀態(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)傳遞,流程圖示意:

image.png

這里做一個小的知識點(個人以前的疑惑,所以花費篇幅說明),在案例中事件的方式并不需要使用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后,對比于上面的簡單事件流流程過程,就變成了:

image.png

使用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ù))即可。整個流程為:


image.png

在這里我們不過多的討論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ù)時,由于對于變量afalse,導致只收集到了autorun2value2的依賴,所以當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í)行incrementstate.value1的變化,不會觸發(fā)autorun2的執(zhí)行,當?shù)谖宕螌?code>state.value3也進行改變后,后續(xù)的每一次state.value1的變化(此時state.value3已經(jīng)不在變化)也會觸發(fā)autorun2的執(zhí)行,所以可以推測出收集不僅僅在第一次執(zhí)行的時候收集完畢就一成不變,而是在每次執(zhí)行后會更新對應狀態(tài)的監(jiān)聽隊列。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容