1. 函数组件与 Hooks 的问题

1. Why 函数组件?

为什么 React 官网推崇函数组件?class 组件 "又不是不能用"。

因为函数组件更合乎 React UI = f(state) 的哲学理念。

于是 Hooks 来了,给函数组件带来了 "外部变量" 与 "副作用",使其性能齐备。同时也是 "逻辑共享" 解决方案。

2. 函数组件的问题

因为函数每次调用,都会把外部变量全都新建一遍,这在开发直觉上,总感觉有些不妥。

UI = f(state) 看起来像是纯函数,传入 state,返回 UI

就像 饭 = 电饭锅(米),但如果 电饭锅 每次煮饭都把 "电路系统" 全都新建一遍,这是反直觉的。

咱们更心愿 f 就是单纯的煮饭,其它性能是曾经 "携带" 的,而不是每次都 "新建"。

3. Hooks 的问题

为解决变量新建问题,React 提供了 useStateuseCallbackuseMemouseRef 等。

state 得用 useState 包一下。传给子组件的简单数据类型(函数、数组、对象),得用 useCallbackuseMemo 包一下(大计算量也得用 useMemo 包一下)。若需保留变量,得用 useRef 包一下。

而在 useEffectuseCallbackuseMemo 的实现里,必须有 deps 这个货色。

以上种种,都让 Hooks 写起来十分反直觉。我不就是用个变量、用个函数,怎么还得包一层?

不能像 Svelte 那样写代码吗?

<img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e606e08582624f0ca881ab5410bb9dfb~tplv-k3u1fbpfcp-zoom-1.image" width="440" alt="" />

2. 解决问题

1. 最合乎直觉的 UI = f(state)

function Demo(state) {  return <div>{state.count}</div>;}

2. React 是这么工作的:

function Demo(props) {  return <div>{props.count}</div>;}

3. 若需组件 "携带" state 与函数,而不是每次新建,那就不能写在组件内:

let count = 0;const onClick = () => {  count += 1;};function Demo() {  return <div onClick={onClick}>{count}</div>;}

离开写毁坏了一体性,不太好。有没有方法让组件既保有内部变量,又写在一个函数内?

4. 自然而然的,咱们想到了闭包(留神外部返回的才是 React 组件):

function createDemo() {  let count = 0;  const onClick = () => {    count += 1;  };  return function Demo() {    return <div onClick={onClick}>{count}</div>;  };}const Demo = createDemo();

此时,onClick 函数不须要用 useCallback 包装,因为它永远不会被新建。应用闭包模式,咱们胜利解除了对 useCallback 的依赖

但闭包有个问题:所有组件实例都共享了一份闭包数据。这当然是不行的。

5. 解决闭包的数据共享问题,动静生成每个组件实例本人的闭包数据即可:

const create = (fn) => (props) => {  const [ins] = useState(() => fn());  return ins(props);};function demo() {  return () => <div />;}const Demo = create(demo);

写到这里,其实曾经讲完了 ... 嗯?那这组件怎么用呢?!

3. 让能力齐备

1. 解决 useState 与组件更新:

// 公共辅助函数const useRender = () => {  const [, setState] = useState(false);  return useCallback(() => setState((s) => !s), []);};function demo() {  let render;  let count = 0;  const onClick = () => {    count += 1;    render();  };  return () => {    render = useRender();    return (      <>        <h1>{count}</h1>        <button onClick={onClick}>Click me</button>      </>    );  };}const Demo = create(demo);

将组件内才有的 setState,"从新赋值" 给内部变量 render,供组件外应用。若需更新,手动调用 render() 即可(当然,函数命名随便比方 update,这里介绍的是设计模式,具体实现没什么束缚)。

于是,咱们胜利解除了对 useState 的依赖

下面曾经是个可用的组件了,在这里试试:codesandbox.io/s/react-split-components-1-ycw80

2. 解决 useMemouseRef,解决 props:

function demo() {  let render;  let props;  const getPower = (x) => x * x;  let count = 0;  let power = getPower(count); // for useMemo  const countRef = { current: null }; // for useRef  const onClick = () => {    // props 解构必须写在函数内,因为内部初始 props 值为 undefined    const { setTheme } = props;    setTheme();    count += 1;    power = getPower(count);    render();  };  return (next) => {    render = useRender();    props = next;    const { theme } = next;    return (      <>        <h1>{theme}</h1>        <h1 ref={countRef}>{count}</h1>        <h1>{power}</h1>        <button onClick={onClick}>Click me</button>      </>    );  };}const Demo = create(demo);

propsrender 一样以 "从新赋值" 传递进来。而后咱们认真想一下:通过闭包,useMemouseRef 其实曾经不须要了。

useMemo 的相似 computed 的运算机制,可改为手动触发的「命令式编程」(当然,也能够用 Proxy 等自行实现相似的 computed 性能,不过这不是重点)。

useMemo 的相似 computed 的运算机制,改成手动触发即可。把 useMemo 的申明式写法改为 "手动调用" 的命令式写法,这更合乎直觉(就像 class 组件时代一样)。

于是,咱们胜利解除了对 useMemouseRef 的依赖

上文代码,在这里试试:codesandbox.io/s/react-split-components-2-wl46b

3. 解决 useEffectuseLayoutEffect

const useRender = () => {  // 省略其它代码...  const [layoutUpdated, setLayoutUpdated] = useState();  const [updated, setUpdated] = useState();  useLayoutEffect(() => layoutUpdated?.(), [layoutUpdated]);  useEffect(() => updated?.(), [updated]);  return useCallback((onUpdated, isLayoutUpdate) => {    // 省略其它代码...    if (typeof onUpdated === 'function') {      (isLayoutUpdate ? setLayoutUpdated : setUpdated)(() => onUpdated);    }  }, []);};function demo() {  let render;  let count = 0;  const onClick = () => {    count += 1;    render(() => {      console.log(count); // 将在 useEffect 中调用    });  };  return () => {    render = useRender();    return (      <>        <h1>{count}</h1>        <button onClick={onClick}>Click me</button>      </>    );  };}const Demo = create(demo);

利用已有的 render 函数来实现 useEffect,这样更简洁(当然也能够另加函数)。

此时,render() 能够间接调用,也能够传入参数,render(onUpdated, isLayoutUpdate)isLayoutUpdate 决定 onUpdated 是在 useEffect 还是 useLayoutEffect 中调用。留神:实践上 render 能够调用屡次,但 React 只触发一次更新,所以如果每次都传入 onUpdated,则只有最初一个失效。

于是,咱们胜利解除了对 useEffectuseLayoutEffect 的依赖

在这里试试:codesandbox.io/s/react-split-components-3-zw6tk

4. 解决 "useMount"

React 组件有个十分根底的需要,在 didMount 中发送接口申请。Hooks 将 didMount 和 didUpdate 对立为 useEffect 后,此需要就多了一个了解步骤,于是有数我的项目里自行实现了 "useMount"。

上文计划中,内部变量得在组件首次渲染后才赋值,这带来了一个问题:render 在首次 useEffect 之后才可用(所以特意将参数命名为 onUpdated),那 "useMount" 怎么实现呢?咱们利用一下 useRender 的参数。

const useRender = (onMounted, isLayoutMount) => {  // 省略其它代码...  const layoutMountedRef = useRef(isLayoutMount && onMounted);  const mountedRef = useRef(!isLayoutMount && onMounted);  useLayoutEffect(() => layoutMountedRef.current?.(), []);  useEffect(() => mountedRef.current?.(), []);  // 省略其它代码...};function demo() {  let render;  let data;  const onMounted = () => {    request().then((res) => {      data = res.data;      render();    });  };  return () => {    render = useRender(onMounted);    return (      <>        <h1>{JSON.stringify(data)}</h1>      </>    );  };}const Demo = create(demo);

这样就行了,在这里试试:codesandbox.io/s/react-split-components-4-y8hn8

5. 其它 Hooks

目前为止,咱们曾经解决了 useStateuseEffectuseCallbackuseMemouseRefuseLayoutEffect,这些是日常开发中最罕用的。官网 Hooks 里还剩下 4 个:useContextuseReduceruseImperativeHandleuseDebugValue,就不一一解决了。

简略来说:如果某个组件内能力拿到的变量,须要在组件外应用,就以从新赋值的形式传出去

在此设计模式下,任何已有需要都是能够被实现的,所谓 "性能齐备"。

4. 隆重介绍 React Split Components (RiC)

就像 Higher-Order Components 一样,这种设计模式得有个命名。

思考到它是把 "变量 + 逻辑" 与 "组件体" 拆散的闭包写法,学习 React Server Components 命名格局,我将其命名为 React Split Components,可简称 RiC,小 i 在这里能够很好的表白 "拆散" 的特点(次要是搜寻后发现,RSC、RPC、RLC、RTC 居然全被占了,天啊,"split" 一共就 5 个字母)。

React Split Components 的特点:

1. 解除对 Hooks 的依赖,但不是指纯函数组件

通过闭包,人造无需 Hooks 包裹。这能让 React 开发者从 "函数组件的反直觉" 与 "Hooks 的繁琐" 中解放出来,写出相似 Svelte 的纯 JS 直觉代码。

毕竟闭包是 JS 的人造个性。

2. 仅在写法层面,无需 ESLint 反对

其实在设计 useEffect 实现的时候,我想到了一种利用现有代码的写法:将 useEffect(fn, deps) 变为 watch(deps, fn)。但如果这么写,watchdeps 就须要 ESLint 插件反对了(因为 Hooks deps 就须要插件反对,否则很容易出错)。

若无必要,勿增实体。咱们要将实现尽可能天然、尽可能简化、尽可能合乎直觉。

3. 相似高阶组件,是一种 "设计模式",非 API,无需库反对

它不是 React 官网 API,无需构建工具反对(比方 React Server Components 就须要)。

它无需第三方库反对(其实 useRender 能够封装为 npm 包,但思考到每个人习惯不一、需要不一,所以尽能够本人来实现辅助函数,上文代码可作为参考)。

React Split Components 最终代码示例:codesandbox.io/s/react-split-components-final-9ftjx

5. Hello, RiC

React Split Components (RiC) 示例:

function demo() {  let render;  let count = 0;  const onClick = () => {    count += 1;    render();  };  return () => {    render = useRender();    return (      <>        <h1>{count}</h1>        <button onClick={onClick}>Click me</button>      </>    );  };}const Demo = create(demo);

如许 Svelte,如许直觉,如许性能主动最优化 bye bye Hooks。

GitHub: github.com/nanxiaobei/react-split-components