关于react.js:React-Split-Components一种全新的-React-函数组件写法再不需要-Hooks

10次阅读

共计 5959 个字符,预计需要花费 15 分钟才能阅读完成。

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

正文完
 0