共计 5959 个字符,预计需要花费 15 分钟才能阅读完成。
1. 函数组件与 Hooks 的问题
1. Why 函数组件?
为什么 React 官网推崇函数组件?class 组件 “ 又不是不能用 ”。
因为函数组件更合乎 React UI = f(state)
的哲学理念。
于是 Hooks 来了,给函数组件带来了 “ 外部变量 ” 与 “ 副作用 ”,使其性能齐备。同时也是 “ 逻辑共享 ” 解决方案。
2. 函数组件的问题
因为函数每次调用,都会把外部变量全都新建一遍,这在开发直觉上,总感觉有些不妥。
UI = f(state)
看起来像是纯函数,传入 state
,返回 UI
。
就像 饭 = 电饭锅 (米)
,但如果 电饭锅
每次煮饭都把 “ 电路系统 ” 全都新建一遍,这是反直觉的。
咱们更心愿 f
就是单纯的煮饭,其它性能是曾经 “ 携带 ” 的,而不是每次都 “ 新建 ”。
3. Hooks 的问题
为解决变量新建问题,React 提供了 useState
、useCallback
、useMemo
、useRef
等。
state 得用 useState
包一下。传给子组件的简单数据类型(函数、数组、对象),得用 useCallback
、useMemo
包一下(大计算量也得用 useMemo
包一下)。若需保留变量,得用 useRef
包一下。
而在 useEffect
与 useCallback
、useMemo
的实现里,必须有 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. 解决 useMemo
、useRef
,解决 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);
props
与 render
一样以 “ 从新赋值 ” 传递进来。而后咱们认真想一下:通过闭包,useMemo
与 useRef
其实曾经不须要了。
而 useMemo
的相似 computed 的运算机制,可改为手动触发的「命令式编程」(当然,也能够用 Proxy
等自行实现相似的 computed 性能,不过这不是重点)。
而 useMemo
的相似 computed 的运算机制,改成手动触发即可。把 useMemo
的申明式写法改为 “ 手动调用 ” 的命令式写法,这更合乎直觉(就像 class 组件时代一样)。
于是,咱们胜利解除了对 useMemo
、useRef
的依赖。
上文代码,在这里试试:codesandbox.io/s/react-split-components-2-wl46b
3. 解决 useEffect
与 useLayoutEffect
:
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
,则只有最初一个失效。
于是,咱们胜利解除了对 useEffect
、useLayoutEffect
的依赖。
在这里试试: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
目前为止,咱们曾经解决了 useState
、useEffect
、useCallback
、useMemo
、useRef
、useLayoutEffect
,这些是日常开发中最罕用的。官网 Hooks 里还剩下 4 个:useContext
、useReducer
、useImperativeHandle
、useDebugValue
,就不一一解决了。
简略来说:如果某个组件内能力拿到的变量,须要在组件外应用,就以从新赋值的形式传出去。
在此设计模式下,任何已有需要都是能够被实现的,所谓 “ 性能齐备 ”。
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)
。但如果这么写,watch
的 deps
就须要 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