0

精读《Records & Tuples for React》

 2 years ago
source link: https://zhuanlan.zhihu.com/p/452989809
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

精读《Records & Tuples for React》

前端开发话题下的优秀答主

继前一篇 精读《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.stringifydeepEqual,用法如下:

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 } 也可以看作像 1819 一样的数字,不可能有人改变它,所以从语法层面你就会像对 19 这个数字一样放心 #{ a: 1 } 不会被改变。

当然这个提案面临的最大问题就是 “如何将拥有子结构的类型看作原始类型”,也许 JS 引擎将它看作一种特别的字符串更贴合其原理,但难点是这又违背了整个语言体系对子结构的默认认知,Box 装箱语法尤其别扭。

总结

看了这篇文章的畅想,React 与 Records & Tulpes 结合的一定会很好,但前提是浏览器对其性能优化必须与 “引用对比” 大致相同才可以,这也是较为少见,对性能要求如此苛刻的特性,因为如果没有性能的加持,其便捷性将毫无意义。

讨论地址是:精读《Records & Tuples for React》· Issue #385 · dt-fe/weekly

如果你想参与讨论,请 点击这里,每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。

关注 前端精读微信公众号

版权声明:自由转载-非商用-非衍生-保持署名(创意共享 3.0 许可证


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK