共计 5383 个字符,预计需要花费 14 分钟才能阅读完成。
react 18 新增了启发式的并发渲染机制,副作用函数会因为组件重渲染可能调用屡次,为了帮忙用户理清正确的副作用应用形式,在开发模式启用 StrictMode 时,会刻意的成心调用两次副作用函数,来达到走查用户逻辑的成果,但此举也给局部降级用户带来了困扰,本文将探讨 helux 如何躲避此问题。
helux 简介
helux 是一个主打轻量、高性能、0 老本接入的 react 状态库,你的利用仅需替换 useState
为useShared
,而后就能够在其余代码 一行都不必批改 的状况下达到晋升 react 部分状态为全局共享状态的成果,可拜访此在线示例理解更多。
import React from 'react';
+ import {createShared, useShared} from 'helux';
+ const {state: sharedObj} = createShared({a:100, b:2});
function HelloHelux(props: any) {- const [state, setState] = React.useState({a: 100, b: 2});
+ const [state, setState] = useShared(sharedObj);
// 以后组件仅依赖 a 变更才触发重渲染
// helux 会动静收集以后组件每一轮渲染的最新依赖,以确保做到准确更新
return <div>{state.a}</div>;
}
默认共享对象是非响应的,冀望用户依照 react 的形式去变更状态,如用户设置 enableReactive
为 true 后,则可创立响应式对象
const {state, setState} = createShared({a: 100, b: 2}, true);
// or
const {state, setState} = createShared({a: 100, b: 2}, {enableReactive: true});
// 将更新所有应用 `sharedObj.a` 值的组件实例
sharedObj.a++;
setState({a: 1000});
2.0 带来了什么
2.0
版本做了以下三个调整
精简 api 命名
原来的 useSharedObject
api 从新导出为更精简的 useShared
,配合createShared
以便进步用户的书写效率和浏览体验。
新增信号记录(试验中)
外部新增了信号相干的记录数据,为未来要公布的 helux-signal
(一个基于 helux 封装的 react signal 模式实现库)做好相干根底建设,helux-signal
还在原型阶段,在适合的时机会公布 beta
版本体验。
不应用信号时,须要 createShared
和 useShared
来两者一起搭配,createShared
创立共享状态,useShared
负责生产共享状态,它返回具体的可读状态值和更新函数。
const {state: sharedObj} = createShared({a:100, b:2}); // 创立
function HelloHelux(props: any) {const [state, setState] = useShared(sharedObj); // 应用
// render logic ...
}
应用信号时,仅须要调用 helux-signal
一个接口 createSignal
既能够实现状态的创立,而后组件可跳过 useShared
钩子函数间接读取共享状态。
需注意,目前 helux 2 仅是外部实现了相干根底建设,下层负责具体实现的 helux-signal 还在试验阶段,现阶段社区已有 preact 提供的 signals-react 库来反对 signal 模式开发 react 组件。
一个料想的残缺的基于 helux-signal
开发 react 组件示例如下:
import {createSignal, comp} from 'helux-signal';
const {state, setState} = createSignal({a:100, b:2}); // 创立信号
// 以下两种形式都将触发组件重渲染
state.a++;
setState({a: 1000});
// <HelloSignal ref={ref} id="some-id" />
const HelloSignal = comp((props, ref)=>{ // 创立可读取信号的 react 组件
return <div>{state.a}</div>; // 以后组件的依赖是 state.a
})
新增 useEffect、useLayoutEffect
v2 版本新增了 useEffect
,useLayoutEffect
两个接口,这也是本文要重点探讨的两个接口,为何 helux
提供这两个接口来代替原生接口呢?且看上面的内容一一详解。
react18 的副作用
react 18 新增了启发式的并发渲染机制,副作用函数会因为组件重渲染可能调用屡次,为了帮忙用户发现未正确应用副作用带来的可能问题(例如忘了做清理行为),在开发模式启用 StrictMode 时,会刻意的成心调用两次副作用函数,来达到走查用户逻辑的成果,冀望用户正确的了解副作用函数。
理论状况什么状况会产生屡次挂载行为呢?新文档特意提到了一个例子,因为在 18 里 react 会拆散组件的状态与卸载行为(非用户代码管制的卸载),即组件卸载了状态仍然放弃,再次挂载时会由 react 外部还原回来,例如 离屏渲染 场景须要此个性。
双调用的困扰
但此举也给局部降级用户带来了困扰,以上面例子为例:
function UI(props: any) {useEffect(() => {console.log("mount");
return () => {console.log("clean up");
};
}, []);
}
在 strcit 模式打印如下
mount
clean up
mount
用户真正卸载组件后还有一次 clean up
打印,由此让很多用户误以为是 bug,去 react 仓库提 issue 形容降级 18 后 useEffect 产生了两次调用,起初 react 官网专门解释了此问题是失常景象,为辅助用户存在不合理的副作用函数刻意做的双调用机制。
但有些场景用户确实冀望开发时也只产生一次调用(例如组件的数据初始化),于是就有了以下各种花式反抗双调用的形式。
移除 StrcitMode
最简略粗犷的形式,就是移除根组件处的 StrcitMode
包裹,彻底屏蔽此双调用行为。
root.render(
- <React.StrictMode>
<App />
- </React.StrictMode>
);
用户可能只须要某些中央无双调用,其余中央须要双调用查看副作用的正确性的话,但此举属于一杆子打死所有场景行为,不太通用。
部分包裹 StrcitMode
StrcitMode
除了包裹根组件,也反对包裹任意子组件,用户能够在须要的中央包裹
<React.StrictMode><YourComponent /></React.StrictMode>
相比全局移除,此办法较为温和,但包裹 StrictMode 是一个强迫性的行为,须要代码处导出安顿哪里须要包裹那里不须要包裹,较为麻烦,有没有既能在根组件包裹 StrcitMode
又能部分屏蔽双调用机制的形式呢?用户们开始从代码层面动手,精确的说是 useEffect
回调里动手
应用 useRef 标记执行状态
大体思路是应用 useRef
记录一个副作用函数是否已执行的状态,让第二次调用被疏忽。
function Demo(props: any) {const isCalled = React.useRef(false);
React.useEffect(() => {if (isCalled.current === false) {await somApi.fetchData();
isCalled.current = true;
}
}, []);
}
此举有肯定的局限性,就是如果加上依赖后,isCalled
无法控制,按思维会副作用清理函数里置 isCalled.current
为 false,这样在组件的存在期过程中变更 id 值时,只管有双调用行为也不会打印两次mock api fetch
React.useEffect(() => {if (isCalled.current === false) {
isCalled.current = true;
console.log('mock api fetch');
return ()=>{
isCalled.current = false;
console.log('clean up');
};
}
}, [id]); // id 变更时,发动新的申请
但如上写法,在组件首次挂载时还是产生两次调用,打印程序为
mock api fetch
clean up
mock api fetch
有没有真正的完满计划,让基于根组件包裹 StricMode
时,子组件首次挂载和存在期始终副作用只产生一次调用呢?接下来让 helux
提供的 useEffect
来彻底解决此问题吧
应用 helux 的 useEffect
咱们只有外围了解 react 双调用的原由:让组件卸载和状态拆散,即组件再次挂载时存在期的已有状态会被 还原,既然有一个还原的过程,那么动手点就很容易了,次要就是察看在组件还原那一刻的运行日志来查找法则。
先标记一个序列自增 id 当做组件示例 id,察看挂载行为针对是哪一个实例
let insKey = 0;
function getInsKey() {
insKey++;
return insKey;
}
function TestDoubleMount() {const [insKey] = useState(() => getInsKey());
console.log(`TestDoubleMount ${insKey}`);
React.useEffect(() => {console.log(`mount ${insKey}`);
return () => console.log(`clean up ${insKey}`);
}, [insKey]);
return <div>TestDoubleMount</div>;
}
可察看到日志如下图,可发现灰色的打印 TestDoubleMount
是 react 成心发动的第二次调用,副作用都是针对 2 号示例,1 号作为一次冗余的调用被 react 抛弃掉。
因为 id 是自增的,react 会刻意的对同一个组件发动两次调用,抛弃第一个并针对第二个调用反复执行副作用(mount–>clean–>mount —> 组件卸载后 clean),那么咱们在第二个副作用执行时只有查看前一个示例是否存在副作用记录,同时记录第二个副作用的执行次数,就很容易做到屏蔽第二次模式出的副作用了,即(mount–>clean–>mount —> 组件卸载后 clean)被批改为(mount —> 组件卸载后 clean),在组件真正执行卸载时执行设定的 clean。
伪代码如下
function mayExecuteCb(insKey: number, cb: EffectCb) {markKeyMount(insKey); // 记录以后实例 id 挂载次数
const curKeyMount = getKeyMount(insKey); // 获取以后实例挂载信息
const pervKeyMount = getKeyMount(insKey - 1); // 获取前一个实例的挂载信息
if (!pervKeyMount) { // 前一个示例无挂载信息则是双调用行为
if (curKeyMount && curKeyMount.count > 1) { // 以后实例第二次挂载才正在执行用户的副作用函数
const cleanUp = cb();
return () => {clearKeyMount(insKey); // 清理以后实例挂载信息
cleanUp && cleanUp(); // 返回清理函数};
}
}
}
在此基础上封装一个 useEffect
给用户即可达到咱们下面说的目标:让基于根组件包裹 StricMode
时,子组件首次挂载和存在期始终副作用只产生一次调用。
function useEffect(cb: EffectCb, deps?: any[]) {const [insKey] = useState(() => getInsKey()); // 写为函数防止 key 自增开销
React.useEffect(() => {return mayExecuteCb(insKey, cb);
}, deps);
}
如果感兴趣 useEffect
的具体实现可查看仓库代码
当初你能够像应用原生的 useEffect
那样应用 helux
导出的useEffect
,同时享受到某些场景不须要双调用检测的益处了。
import {useEffect} from 'helux';
useEffect(() => {console.log('mock api fetch', id);
return () => {console.log('mock clean up');
};
}, [id]); // id 变更时,发动新的申请
结语
理解 双调用
的设计初衷与流程有助于帮忙咱们更清晰的了解副作用函数如何治理,同时也能够帮忙咱们为防止双调用机制做出更好的决策。
helux 属于模块联邦 sdk hel-micro 子包,初衷是为 react 提供一种更灵便、更低廉老本的状态共享形式,如果你对 helux 或 hel-micro 感兴趣,欢送关注并给予咱们更多的改良反馈意见。