繼前一篇 精讀《Records & Tuples 提案》,已經有人在思考這個提案可以幫助 React 解決哪些問題了,比如這篇 Records & Tuples for React,就提到了許多 React 痛點可以被解決。
其實我比較擔憂瀏覽器是否能將 Records & Tuples 效能優化得足夠好,這將是它能否大規模應用,或者說我們是否放心把問題交給它解決的最關鍵因素。本文基於瀏覽器可以完美優化其效能的前提,一切看起來都挺美好,我們不妨基於這個假設,看看 Records & Tuples 提案能解決哪些問題吧!
概述
Records & Tuples Proposal 提案在上一篇精讀已經介紹過了,不熟悉可以先去看一下提案語法。
保證不可變性
雖然現在 React 也能用 Immutable 思想開發,但大部分情況無法保證安全性,比如:
const Hello = ({ profile }) => {
// prop mutation: throws TypeError
profile.name = 'Sebastien updated';
return <p>Hello {profile.name}</p>;
};
function App() {
const [profile, setProfile] = React.useState(#{
name: 'Sebastien',
});
// state mutation: throws TypeError
profile.name = 'Sebastien updated';
return <Hello profile={profile} />;
}
歸根結底,我們不會總使用 freeze
來凍結物件,大部分情況下需要人為保證引用不被修改,其中的潛在風險依然存在。但使用 Record 表示狀態,無論 TS 還是 JS 都會報錯,立刻阻止問題擴散。
部分代替 useMemo
比如下面的例子,為了保障 apiFilters
引用不變,需要對其 useMemo
:
const apiFilters = useMemo(
() => ({ userFilter, companyFilter }),
[userFilter, companyFilter],
);
const { apiData, loading } = useApiData(apiFilters);
但 Record 模式不需要 memo,因為 js 引擎會幫你做類似的事情:
const {apiData,loading} = useApiData(#{ userFilter, companyFilter })
用在 useEffect
這段寫的很囉嗦,其實和代替 useMemo 差不多,即:
const apiFilters = #{ userFilter, companyFilter };
useEffect(() => {
fetchApiData(apiFilters).then(setApiDataInState);
}, [apiFilters]);
你可以把 apiFilters
當做一個引用穩定的原始物件看待,如果它確實變化了,那一定是值改變了,所以才會引發取數。如果把上面的 #
號去掉,每次元件重新整理都會取數,而實際上都是多餘的。
用在 props 屬性
可以更方便定義不可變 props 了,而不需要提前 useMemo:
<ExpensiveChild someData={#{ attr1: 'abc', attr2: 'def' }} />;
將取數結果轉化為 Record
這個目前還真做不到,除非用效能非常差的 JSON.stringify
或 deepEqual
,用法如下:
const fetchUserAndCompany = async () => {
const response = await fetch(
`https://myBackend.com/userAndCompany`,
);
return JSON.parseImmutable(await response.text());
};
即利用 Record 提案的 JSON.parseImmutable
將後端返回值也轉化為 Record,這樣即便重新查詢,但如果返回結果完全不變,也不會導致重渲染,或者區域性變化也只會導致區域性重渲染,而目前我們只能放任這種情況下全量重渲染。
然而這對瀏覽器實現 Record 的新能優化提出了非常嚴苛的要求,因為假設後端返回的資料有幾十 MB,我們不知道這種內建 API 會導致多少的額外開銷。
假設瀏覽器使用非常 Magic 的辦法做到了幾乎零開銷,那麼我們應該在任何時候都用 JSON.parseImmutable
解析而不是 JSON.parse
。
生成查詢引數
也是利用了 parseImmutable
方法,讓前端可以精確傳送請求,而不是每次 qs.parse
生成一個新引用就發一次請求:
// This is a non-performant, but working solution.
// Lib authors should provide a method such as qs.parseRecord(search)
const parseQueryStringAsRecord = (search) => {
const queryStringObject = qs.parse(search);
// Note: the Record(obj) conversion function is not recursive
// There's a recursive conversion method here:
// https://tc39.es/proposal-record-tuple/cookbook/index.html
return JSON.parseImmutable(
JSON.stringify(queryStringObject),
);
};
const useQueryStringRecord = () => {
const { search } = useLocation();
return useMemo(() => parseQueryStringAsRecord(search), [
search,
]);
};
還提到一個有趣的點,即到時候配套工具庫可能提供類似 qs.parseRecord(search)
的方法把 JSON.parseImmutable
包裝掉,也就是這些生態庫想要 “無縫” 接入 Record 提案其實需要做一些 API 改造。
避免迴圈產生的新引用
即便原始物件引用不變,但我們寫幾行程式碼隨便 .filter
一下引用就變了,而且無論返回結果是否變化,引用都一定會改變:
const AllUsers = [
{ id: 1, name: 'Sebastien' },
{ id: 2, name: 'John' },
];
const Parent = () => {
const userIdsToHide = useUserIdsToHide();
const users = AllUsers.filter(
(user) => !userIdsToHide.includes(user.id),
);
return <UserList users={users} />;
};
const UserList = React.memo(({ users }) => (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
));
要避免這個問題就必須 useMemo
,但在 Record 提案下不需要:
const AllUsers = #[
#{ id: 1, name: 'Sebastien' },
#{ id: 2, name: 'John' },
];
const filteredUsers = AllUsers.filter(() => true);
AllUsers === filteredUsers;
// true
作為 React key
這個想法更有趣,如果 Record 提案保證了引用嚴格不可變,那我們完全可以拿 item
本身作為 key
,而不需要任何其他手段,這樣維護成本會大大降低。
const list = #[
#{ country: 'FR', localPhoneNumber: '111111' },
#{ country: 'FR', localPhoneNumber: '222222' },
#{ country: 'US', localPhoneNumber: '111111' },
];
<>
{list.map((item) => (
<Item key={item} item={item} />
))}
</>
當然這依然建立在瀏覽器非常高效實現 Record 的前提,假設瀏覽器採用 deepEqual
作為初稿實現這個規範,那麼上面這坨程式碼可能導致本來不卡的頁面直接崩潰退出。
TS 支援
也許到時候 ts 會支援如下方式定義不可變變數:
const UsersPageContent = ({
usersFilters,
}: {
usersFilters: #{nameFilter: string, ageFilter: string}
}) => {
const [users, setUsers] = useState([]);
// poor-man's fetch
useEffect(() => {
fetchUsers(usersFilters).then(setUsers);
}, [usersFilters]);
return <Users users={users} />;
};
那我們就可以真的保證 usersFilters
是不可變的了。因為在目前階段,編譯時 ts 是完全無法保障變數引用是否會變化。
優化 css-in-js
採用 Record 與普通 object 作為 css 屬性,對 css-in-js 的區別是什麼?
const Component = () => (
<div
css={#{
backgroundColor: 'hotpink',
}}
>
This has a hotpink background.
</div>
);
由於 css-in-js 框架對新的引用會生成新 className,所以如果不主動保障引用不可變,會導致渲染時 className 一直變化,不僅影響除錯也影響效能,而 Record 可以避免這個擔憂。
精讀
總結下來,其實 Record 提案並不是解決之前無法解決的問題,而是用更簡潔的原生語法解決了複雜邏輯才能解決的問題。這帶來的優勢主要在於 “不容易寫出問題程式碼了”,或者讓 Immutable 在 js 語言的上手成本更低了。
現在看下來這個規範有個嚴重擔憂點就是效能,而 stage2 並沒有對瀏覽器實現效能提出要求,而是給了一些建議,並在 stage4 之前給出具體效能優化建議方案。
其中還是提到了一些具體做法,包括快速判斷真假,即對資料結構操作時的優化。
快速判真可以採用類似 hash-cons 快速判斷結構相等,可能是將一些關鍵判斷資訊存在 hash 表中,進而不需要真的對結構進行遞迴判斷。
快速判假可以通過維護雜湊表快速判斷,或者我覺得也可以用上資料結構一些經典演算法,比如布隆過濾器,就是用在高效快速判否場景的。
Record 降低了哪些心智負擔
其實如果應用開發都是 hello world 複雜度,那其實 React 也可以很好的契合 immutable,比如我們給 React 元件傳遞的 props 都是 boolean、string 或 number:
<ExpensiveChild userName="nick" age={18} isAdmin />;
比如上面的例子,完全不用關心引用會變化,因為我們用的原始型別本身引用就不可能變化,比如 18
不可能突變成 19
,如果子元件真的想要 19
,那一定只能建立一個新的,總之就是沒辦法改變我們傳遞的原始型別。
如果我們永遠在這種環境下開發,那 React 結合 immutable 會非常美妙。但好景不長,我們總是要面對物件、陣列的場景,然而這些型別在 js 語法裡不屬於原始型別,我們瞭解到還有 “引用” 這樣一種說法,兩個值不一樣物件可能是 ===
全等的。
可以認為,Record 就是把這個顧慮從語法層面消除了,即 #{ a: 1 }
也可以看作像 18
,19
一樣的數字,不可能有人改變它,所以從語法層面你就會像對 19
這個數字一樣放心 #{ a: 1 }
不會被改變。
當然這個提案面臨的最大問題就是 “如何將擁有子結構的型別看作原始型別”,也許 JS 引擎將它看作一種特別的字串更貼合其原理,但難點是這又違背了整個語言體系對子結構的預設認知,Box 裝箱語法尤其彆扭。
總結
看了這篇文章的暢想,React 與 Records & Tulpes 結合的一定會很好,但前提是瀏覽器對其效能優化必須與 “引用對比” 大致相同才可以,這也是較為少見,對效能要求如此苛刻的特性,因為如果沒有效能的加持,其便捷性將毫無意義。
討論地址是:精讀《Records & Tuples for React》· Issue #385 · dt-fe/weekly
如果你想參與討論,請 點選這裡,每週都有新的主題,週末或週一釋出。前端精讀 - 幫你篩選靠譜的內容。
關注 前端精讀微信公眾號
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
版權宣告:自由轉載-非商用-非衍生-保持署名(創意共享 3.0 許可證)