react 开发中的一些小技巧,助你一臂之力

蚊子前端博客
发布于 2021-01-28 14:55
我使用 react 开发已经 1 年的时间了,不能说很精通,不过在使用的过程中,确实领悟和总结了一些小技巧,可以加快我们后续的开发

我使用 react 开发已经 1 年的时间了,不能说很精通,不过在使用的过程中,确实领悟和总结了一些小技巧,可以加快我们后续的开发。

自恋中-蚊子的前端博客

1. useState 中的一些技巧

我们在使用 react 开发 function comppoent 时,必然绕不开useState,可谓是随处可见,那么 useState 中有哪些坑和需要注意的地方呢?

1.1 useState 是异步的

有些使用 react 时间较短的同学,经常会这样写,在调用 set 方法设置 state 的值后,立刻去获取这个值:

COPYJAVASCRIPT

const [data, setData] = useState(); const func = () => { console.log(data); }; useEffect(() => { setData({ name: '蚊子' }); func(); }, []);

然而在使用 log 输出时,却发现没有变化。这是因为 useState 中的 set 是异步操作,而函数 func 的调用是同步的,这就导致 func()要比 set 先执行。

那么如何解决这个异步的问题呢?这里有几种方式可以参考:

1.1.1 用 useEffect 辅助

COPYJAVASCRIPT

useEffect(() => { if (data) { // 当满足条件时,执行func() func(); } }, [data]);

1.1.2 将需要的数据直接传入到 func 方法中

COPYJAVASCRIPT

useEffect(() => { const data = { name: '蚊子' }; setData(data); func(data); }, []);

1.1.3 延时触发

延时触发也可以解决这个问题,但是最不建议这种方式,一个异步问题,如果再用一个延时更大的异步操作来兜底,可能还会产生其他一些未知的问题。

COPYJAVASCRIPT

useEffect(() => { setData({ name: '蚊子' }); setTimeout(func, 10); }, []);

1.2 setTimeout 中的 state 永远是初始值

我们想要延迟设置一些数据,例如让一个计数器固定简单的增加+1,我们可能会这样写:

COPYJAVASCRIPT

const [count, setCount] = useState(0); useEffect(() => { const timer = setInterval(() => { setCount(count + 1); }, 1000); return () => clearInterval(timer); // 注意清除定时器 }, []);

可是运行时我们发现,计数器只是从 0 涨到了 1,然后就再也不增加了,在 setInterval 中的回调函数里打 log,发现回调函数还在执行,可是就是 count 没有发生变化。

这是因为 useEffect 的第二个参数依赖项导致的,在我们上面的示例中,依赖项为空数组,这表示在第一次初始化后,内部就再也不发生变化,那么 count 依赖的就永远是第最开始的初始值。

可是如何实现在定时器中持续操作 state 呢?这里有几种解决方式可供参考:

1.2.1 将 state 添加到依赖项中

刚才我们也说是因为依赖项,导致每次读取都是最开始初始化时的值,那么我们就可以把count添加到依赖项中:

COPYJAVASCRIPT

const [count, setCount] = useState(0); useEffect(() => { const timer = setInterval(() => { setCount(count + 1); }, 1000); return () => clearInterval(timer); }, [count]);

这样就能实现计数器一直增加了。

但这样会导致另一个问题:每次 count 发生变化时,都会先执行clearInterval,然后再重启一个新的定时器setInterval,setInterval 就蜕变成了 setTimeout 了,只执行一次 setInterval 就会被清理掉。因此这里其实换成 setTimeout 即可:

COPYJAVASCRIPT

useEffect(() => { const timer = setTimeout(() => { setCount(count + 1); }, 1000); return () => clearTimeout(timer); }, [count]);

还有一种更简单粗暴的方式,就是不添加任何依赖项,但这种方式,会导致所有的 state 变化,都会触发 useEffect 的重新执行:

COPYJAVASCRIPT

useEffect(() => { const timer = setTimeout(() => { setCount(count + 1); }, 1000); return () => clearTimeout(timer); });

这种没有任何依赖项的方式,建议在组件中只有一个 count 的 state 中使用!

1.2.2 使用 useRef 保存上次的 state

我们可以利用 useRef 的特性,来保存刚才发生变化的 state 值,下次使用时,可以直接从 useRef 中获取。useRef 中的值并不受依赖项的限制:

COPYJAVASCRIPT

const [count, setCount] = useState(0); const countRef = useRef(0); // 创建一个ref来保存count的值 useEffect(() => { countRef.current = count; // count发生变化时,将其存储起来 }, [count]); useEffect(() => { const timer = setInterval(() => { setCount(countRef.current + 1); // 每次从countRef.current中获取到最新的值 }, 1000); return () => clearInterval(timer); }, []);

暗中观察-蚊子的前端博客

2. useRef 的妙用

刚才 useRef 已经小试牛刀,试用了一下。现在我们再来看看它具体的用法。

2.1 保存一切可变的值

useRef 可以保存一切可变的值,例如上面 state 中的值,同时还有定时器的 timer,requestAnimationFrame 的 id 等。例如在递归的操作中,timer 和 id 每次都是不一样的,当取消的时候,应当取消哪个就很头疼,这里我们就可以使用 useRef 来进行存储:

COPYJAVASCRIPT

const requestRef = useRef(0); // 开始渲染 const render = () => { console.log('render', Date.now()); requestRef.current = requestAnimationFrame(() => { render(); }); }; // 取消渲染 const cancelRender = () => { cancelAnimationFrame(requestRef.current); };

2.2 同时保存多个变量

我们在上面的例子中,都是只保存一个值,然后再被另一个新值给覆盖掉。可是如果我们想同时保存多个值时,怎么办呢?

如果是单纯的数据的话,那把 ref 初始化成数组,然后按照数组的方式操作即可。

COPYJAVASCRIPT

const listRef = useRef([]); useEffect(() => { const timer = setInterval(() => { listRef.current.push(Date.now()); }, 1000); return () => clearInterval(timer); }, []);

可是我们有一个 dom 元素列表,想把所有的 dom 元素都存储起来?

COPYJAVASCRIPT

const domListRef = useRef([]); const getDomList = useCallback((dom: HTMLDivElement | null) => { if (dom) { domListRef.push(dom); } }, []); <> { list.map((item) => <div key={item.ename} ref={getDomList}></div>); } </>

这里要特别注意,getDomList 方法要用useCallback包裹起来,否则每次 state 变化时,都会导致 domListRef 数据的增加!

2.3 自定义 useInterval 的 hook

我们通过第 1 节和第 2 节了解了 useEffect 中的 setTimeout(setInterval)和 useRef 的表现,基于这两者,我们可以自己实现一个 useTimeout 或者 useInterval。

在之前的文章 如何构建自己的 react hooks 里,我们已经实现过 useInterval,现在理解起来更加轻松一些了。

useInterval 的实现:

COPYJAVASCRIPT

const useInterval = (callback, delay) => { const saveCallback = useRef(); useEffect(() => { // 每次callback发生变化时,都将最新的callback保存起来 saveCallback.current = callback; }); useEffect(() => { function tick() { saveCallback.current(); } if (delay !== null) { let id = setInterval(tick, delay); // setInterval只运行ref.current里的方法 return () => clearInterval(id); } }, [delay]); };

在 useInterval 中执行 state 的 set 操作时,就不用再关心 state 是否会更新的问题了:

COPYJAVASCRIPT

const [count, setCount] = useState(0); useInterval(() => { setCount(count + 1); }, 1000);

我佛慈悲-蚊子的前端博客

3. createPortal 的骚操作

我们在引入组件并渲染时,会按照引入的位置进行渲染。不过对于一些公共组件,例如弹窗、toast、悬浮挂件角标等,不希望这些组件的样式收到父级元素的样式影响(如overflow: hiddentransform(父级元素有 transform 样式时,"position: fixed"会降级为"position: absolute"),z-index等),最好她可以渲染到根目录或者其他指定的目录中。这时应该怎么办呢?

我们就用到 createPortal 了,该方法的使用规则非常简单,就 2 个参数:

  1. 要渲染的组件;

  2. 要挂载的 DOM 节点;

代码说明下:

COPYJAVASCRIPT

import { createPortal } from 'react'; const Modal = () => { return createPortal( <div className="i-modal"> <div className="i-modal-content"></div> </div>, document.body, ); }; export default Modal;

这个<Modal />组件可以在任何地方引用,然后渲染时,会被渲染到 document.body 的最后(类似于 appendChild 操作):

COPYJAVASCRIPT

import Modal from './modal'; const User = () => { return ( <div className="user"> <Modal /> </div> ); };

4. 小结

其实在 react 中还有很多的小技巧操作,如果我们熟悉这些小技巧后,就能快速地用 react 搭建我们的应用。如果这些文章对你有帮助,欢迎转发和点“好看”。

别熬夜了-蚊子的前端博客

标签:
阅读(970)

公众号:

qrcode

微信公众号:前端小茶馆