ObservedBits: React Context的祕密功能
上回講到 ofollow,noindex" target="_blank">使用React Context要防止重複渲染 ,但是,即使我們做到避免Context.Provider重複渲染子元件(不知道我在說什麼的請先看上回文章),依然避免不了多個Context.Consumer會因為和自己不相關的內容改變而重複渲染。
舉個例子,有一個Context Provider有兩個Consumer,從來沒有人規定說一個Provider只能對應一個Consumer啊,然後context上的資料差不多是這樣。
{ theme: /* 關於主題 */ count: /* 給最佳hello world獎得住 counter */ }
從邏輯上說,一個Consumer A(我們叫它Content)只依賴於theme欄位,另一個Consumer B(我們叫它Counter)只依賴於count欄位,所以,理想狀況是,只有theme更改的時候,Content重新渲染,只有count更改的是偶,Counter重新渲染。

可惜,做不到,因為React中Context的邏輯是,只要Provider的value一變,所有的Consumer都會得到通知,才不管你只依賴於value中某個欄位呢。
在傳統Context API中,Content元件就是這樣。
const Content = () => ( <Context.Consumer> { contextValue => { // 其實這裡只有contextValue.theme改變的時候才需要被呼叫啊 // 但是,對不起,只要contextValue改了,這裡就要被呼叫 } } </Context.Consumer> );
如果使用Hooks,那就是這樣。
const Content = () => ( const {theme} = useContext(Context); // 你看我真的只需要theme,但是Context的value一改,這個Content總是被重新渲染。 );
現在你應該明白這個窘境了,簡單來說就是這樣,Context的Consumer中需要一個途徑告訴React:"我不需要更新,用我上一次渲染的結果就行。"
以前我以為React沒有這種途徑,最近一個偶然的機會,我發現其實React還是提供了類似功能的,不過這個功能非常祕密,叫做observedBits。
先給FBI警告:warning:,這個功能處於unstable狀態,僅供參考,如果要用,後果自負。
React利用createContext函式建立一個"上下文"(Context),第一個引數是預設值,其實就連第一個引數一般都不怎麼用,所以第二個可選引數就更加被人忽略了,第二個引數可以是一個函式,每當Context的值發生變化時,這個函式被用來呼叫。
const Context = createContext(null, (prev, next) => { //prev是上一次context的值 //next是新的context值 });
React預期這個函式引數返回一個數字,以bit的方式代表值的哪些部分發生了變化,我希望你理解什麼叫"位元操作",簡單說就是……算了,你要是忘了就去翻電腦科學課程吧。
就接著上面Content和Counter兩個Consumer的例子說,現在Content只關心theme欄位的辯護啊,Counter只關心count欄位的變化,所以,我們用一位bit來代表theme的變化,另一個bit位來代表theme的變化。
別寫magic number,用常數定義下來。
const ThemeColorChangedBits = 0b10; const ThemeCountChangedBits = 0b01;
比較prev和next,如果只有theme變化,就返回0b10;如果只有count變化,就返回0b01;如果theme和count都變化,就返回0b11;如果theme和count都沒變化……都沒變化這個函式根本不會被呼叫啊!
Context就像下面這樣來建立。
const Context = createContext(null, (prev, next) => { let result = 0; if (prev.theme !== next.theme) { result |= ThemeColorChangedBits; } if (prev.count !== next.count) { result |= ThemeCountChangedBits; } console.log("calculatedBits ", result); return result; });
上面說的是建立Context,我們賦予了這個Context一個神力,就是通過createContext的第二個函式引數判斷改了哪些東西,用bits代表。
接下來看怎麼用這些bits。
以只關心theme欄位的Content元件為例,程式碼這麼寫。
function Content() { return ( <Context.Consumer unstabled_observedBits={ThemeColorChangedBits}> { ({theme, switchTheme}) => { /* 只有theme改變才呼叫到這裡 */ return ( <> <h1 style={theme}>Hello world</h1> <button onClick={() => switchTheme(redTheme)}>Red Theme</button> <button onClick={() => switchTheme(greenTheme)}>Green Theme</button> </> ); } } </Context.Consumer> ); }
這樣一來,原本每次Context值變化都會被呼叫的Consumer子元件部分,變成只有theme欄位改變才呼叫了。
上面用的是傳統Context API,通過prop名unstable_observedBits就看得出來是一個不穩定API,不過,如果你用上Hooks,就看不出來這是一個unstable的東西了,程式碼如下。
function Content() { console.log("render Content"); const { theme, switchTheme } = useContext(Context, ThemeColorChangedBits); return ( <> <h1 style={theme}>Hello world</h1> <button onClick={() => switchTheme(redTheme)}>Red Theme</button> <button onClick={() => switchTheme(greenTheme)}>Green Theme</button> </> ); }
完整demo程式碼在這裡可以看到 observedBits - CodeSandbox 。

大家通過Console上打出的內容可以判斷,當切換主題時,Counter真的沒有重新渲染;當點+按鈕時,Content也真的沒有重新渲染。
總結
這個祕密API怎麼樣呢?
我個人覺得不怎麼樣,以為動用了bit操作,bit操作本身就很煩,讓應用層API去搞bit操作,總是感覺怪怪的。
而且,一個數字的bit位是有限的,也就是說能夠代表context修改的部分數量也是有限的,沒法擴展出很多可能獨立修改的部分。
目前observedBits這招還是unstable,我覺得將來肯定會廢棄掉,至於React會提供什麼樣的API實現同樣的目的,我們拭目以待吧。
最後做個廣告: 加入我的知識星球《進擊的React》,獲取最新React技術諮詢,加入後分享邀請好友加入還可以獲得獎金哦 。