继前一篇 精读《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 Hello {profile.name}
;
};
function App() {
const [profile, setProfile] = React.useState(#{
name: 'Sebastien',
});
// state mutation: throws TypeError
profile.name = 'Sebastien updated';
return ;
}
归根结底,我们不会总使用
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
【精读《Records & Tuples for React》】这段写的很啰嗦,其实和代替 useMemo 差不多,即:
const apiFilters = #{ userFilter, companyFilter };
useEffect(() => {
fetchApiData(apiFilters).then(setApiDataInState);
}, [apiFilters]);
你可以把
apiFilters
当做一个引用稳定的原始对象看待,如果它确实变化了,那一定是值改变了,所以才会引发取数。如果把上面的 #
号去掉,每次组件刷新都会取数,而实际上都是多余的。用在 props 属性
可以更方便定义不可变 props 了,而不需要提前 useMemo:
;
将取数结果转化为 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 ;
};
const UserList = React.memo(({ users }) => (
{users.map((user) => (
- {user.name}
))}
));
要避免这个问题就必须
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) => (
))}
>
当然这依然建立在浏览器非常高效实现 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 ;
};
那我们就可以真的保证
usersFilters
是不可变的了。因为在目前阶段,编译时 ts 是完全无法保障变量引用是否会变化。优化 css-in-js
采用 Record 与普通 object 作为 css 属性,对 css-in-js 的区别是什么?
const Component = () => (This has a hotpink background.);
由于 css-in-js 框架对新的引用会生成新 className,所以如果不主动保障引用不可变,会导致渲染时 className 一直变化,不仅影响调试也影响性能,而 Record 可以避免这个担忧。
精读 总结下来,其实 Record 提案并不是解决之前无法解决的问题,而是用更简洁的原生语法解决了复杂逻辑才能解决的问题。这带来的优势主要在于 “不容易写出问题代码了”,或者让 Immutable 在 js 语言的上手成本更低了。
现在看下来这个规范有个严重担忧点就是性能,而 stage2 并没有对浏览器实现性能提出要求,而是给了一些建议,并在 stage4 之前给出具体性能优化建议方案。
其中还是提到了一些具体做法,包括快速判断真假,即对数据结构操作时的优化。
快速判真可以采用类似 hash-cons 快速判断结构相等,可能是将一些关键判断信息存在 hash 表中,进而不需要真的对结构进行递归判断。
快速判假可以通过维护散列表快速判断,或者我觉得也可以用上数据结构一些经典算法,比如布隆过滤器,就是用在高效快速判否场景的。
Record 降低了哪些心智负担
其实如果应用开发都是 hello world 复杂度,那其实 React 也可以很好的契合 immutable,比如我们给 React 组件传递的 props 都是 boolean、string 或 number:
;
比如上面的例子,完全不用关心引用会变化,因为我们用的原始类型本身引用就不可能变化,比如
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如果你想参与讨论,请 点击这里,每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。
关注 前端精读微信公众号
![精读《Records & Tuples for React》](https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg)
文章图片
版权声明:自由转载-非商用-非衍生-保持署名( 创意共享 3.0 许可证)
推荐阅读
- 选择篇(022)-下面代码的输出是什么?
- 从0到1搭建组件库
- JS中 &&、|| 和 & 、| 的使用
- Mac 上制作 SSL 证书
- JavaScript 基本数据类型转换
- 微信商城小程序开发方式有哪些()
- Form 表单在数栈的应用(下)(深入篇)
- 选择篇(019)-下面代码的输出是什么?
- 选择篇(020)-下面代码的输出是什么?
- 选择篇(018)-下面代码的输出是什么?