组合优于继承,我们一直在寻求「 解耦 」来把复杂的业务代码简单化。而Hook的最大优点就是代码复用,更简洁。而且Hook在写法上也遵循了「 单一职责模式 」。
今天主要分享的是在学习Hook之前,我们应该做哪些思考。
- 数据绑定、状态管理
- 生命周期、组件挂载卸载
- 组件传值
- 节点元素获取
- 复用逻辑抽离
数据绑定
在react中state的概念是内部状态的管理,它的变更直接会影响页面的渲染。
在hook中把setState拆开了。每一个单个的state的都会是一个单个的状态,单个状态的变更不会影响其他state。我们可以通过useState实现单个状态的初始化定义。
useState的参数可以是一个数字、字符串、布尔值、数组或对象,也可以是一个函数。
同样我们避免不了会使用集合(对象)来处理一些逻辑。
- const [count, setCount] = useState(0);
- const [count, setCount] = useState(() => 0);
- const [obj, setObj] = useState({});
- setObj((prevObj) => {
- // 也可以使用 Object.assign
- return { ...prevObj, age: 23 };
- });
一般我们会定义一个初始值initialState,如果这个初始值,需要额外的计算开销,我们可以定义一个函数来处理。需要注意的是,useState 函数只会在初始渲染的时候被调用。
- const [state, setState] = useState(() => {
- // 额外的操作someExpensiveComputation
- const initialState = someExpensiveComputation(props);
- return initialState;
- });
对于多个state集合的处理,还有另一种方案就是useReducer。
- 比如单个 state 的状态会影响多个 state 的值的时候。
- 比如多个 state 的状态会随着某种类型的改变而改变。
如下多个 state 会随着登录、登出、刷新 token 这三种状态的改变而改变。
- const [state, dispatch] = React.useReducer(
- (prevState, action) => {
- switch (action.type) {
- case "RESTORE_TOKEN":
- return {
- ...prevState,
- userToken: action.token,
- isLoading: false,
- };
- case "SIGN_IN":
- return {
- ...prevState,
- isSignout: false,
- userToken: action.token,
- };
- case "SIGN_OUT":
- return {
- ...prevState,
- isSignout: true,
- userToken: null,
- };
- }
- },
- {
- isLoading: true,
- isSignout: false,
- userToken: null,
- }
- );
副作用
hook 提供了一种新的概念来代替生命周期函数,就是useEffect副作用。它被看作是从 React 的纯函数式世界通往命令式世界的逃生通道。
它的执行时机是在屏幕元素渲染结束后延迟执行。
它的作用有:
- 它可以监控 state 值的变化
- 它可以处理只运行一次的逻辑,有点类似生命周期 componentDidMount 和 componentWillUnmount 的思维模式,
- 添加订阅、设置定时器、发送网络请求
- 更多其他
- // 通过的第二个参数来实现只执行一次或监控state值
- useEffect(() => {
- // ...
- }, []);
- // useEffect第一个参数的返回函数就是componentWillUnmount的思想,在组件卸载之前进行
- useEffect(() => {
- const subscription = props.source.subscribe();
- return () => {
- // 清除订阅
- subscription.unsubscribe();
- };
- });
但是这种延迟执行的机制不能满足我们所有的场景,如果我们想要实现屏幕绘制和副作用同步执行,比如实时修改dom结构等这样的场景,useEffect无法满足,会出现闪屏的效果。
我们可以通过useLayoutEffect来实现,它的执行时机是在组件加载完成后,屏幕绘制之前进行。但这样也有缺点就是阻塞屏幕渲染,可能会出现白屏或停顿。
所以useLayoutEffect的使用场景:
- 防止闪烁,比较耗时的计算
- Dom操作
- componentDidMount和componentDidUpdate的场景
如果只是单独的获取(get操作)就没有必要使用useLayoutEffect。
组件传值
组件传值的核心:
- 父传子,通过在子组件设置属性;
- 子传父,通过回调。
- 多级组件,通过中间状态管理
- // 父组件
- function Home() {
- const [currentTab, setCurrentTab] = useState("msg");
- return (
- <>
- <View style={styles.logo}>
- // 父传子
- <TabView currentTab={currentTab} setCurrentTab={setCurrentTab} />
- // 子传父
- <CodeView code={code} changeCode={(code)=>setCurrentTab(code)} />
- </View>
- <Text>{currentTab}</Text>
- </>
- );
- }
- //子组件
- function TabView({ currentTab, setCurrentTab }) {
- return (
- <View style={styles.logo}>
- <Text>{currentTab}</Text>
- <Button
- title="修改tab"
- onPress={() => {
- setCurrentTab("pass");
- }}
- />
- </View>
- );
- }
- //子传父
- function CodeView({ code, changeCode }) {
- return (
- <View style={styles.logo}>
- <Text>{code}</Text>
- <Button
- title="修改tab"
- onPress={changeCode}
- />
- </View>
- );
- }
多组件的传值,可以通过context来处理。
- export const MyContent = React.createContext({});
- function Home() {
- return (
- <MyContent.Provider
- value={{
- currentTab,
- phoneValue,
- codeValue,
- setPhoneValue,
- setCodeValue,
- }}
- >
- <FormItem />
- <SwitchItemView />
- </MyContent.Provider>
- );
- }
- function FormItem() {
- const { phoneValue, setPhoneValue } = useContext(MyContent);
- return (
- <View style={styles.logo}>
- <Text>{phoneValue}</Text>
- {/* ...*/}
- </View>
- );
- }
- function SwitchItemView() {
- const { codeValue, setCodeValue } = useContext(MyContent);
- return (
- <View style={styles.logo}>
- <Text>{phoneValue}</Text>
- {/* ...*/}
- </View>
- );
- }
元素节点操作
hook通过useRef来创建节点对象,然后通过ref挂载,通过current来获取。
- function TextInputWithFocusButton() {
- const inputEl = useRef(null);
- const onButtonClick = () => {
- inputEl.current.focus();
- };
- return (
- <>
- <input ref={inputEl} type="text" />
- <button onClick={onButtonClick}>Focus the input</button>
- </>
- );
- }
我们可能会封装一些逻辑,自定义一些属性,暴露给父元素,那么我们就会用到useImperativeHandle和forwardRef。
- function FancyInput(props, pref) {
- const inputRef = useRef();
- useImperativeHandle(ref, () => ({
- focus: () => {
- inputRef.current.focus();
- }
- }));
- return <input ref={inputRef} ... />;
- }
- FancyInput = forwardRef(FancyInput);
- <FancyInput ref={inputRef} />
- // 父组件可以直接调用inputRef.current.focus()
因为 ref 对象不会把当前 ref 值的变化通知给我们,所以我们必须通过useState和useCallback实现。
- function MeasureExample() {
- const [height, setHeight] = useState(0);
- const measuredRef = useCallback((node) => {
- if (node !== null) {
- setHeight(node.getBoundingClientRect().height);
- }
- }, []);
- return (
- <>
- <h1 ref={measuredRef}>Hello, world</h1>
- <h2>The above header is {Math.round(height)}px tall</h2>
- </>
- );
- }
自定义hook
在代码中,我们会有一些共用的逻辑,我们可以抽离出来比如自定义的防抖节流,自定义 Hook 是一个函数,其名称以 “use” 开头,函数内部可以调用其他的 Hook。
- const useDebounce = (fn, ms = 30, deps = []) => {
- let timeout = useRef();
- useEffect(() => {
- if (timeout.current) clearTimeout(timeout.current);
- timeout.current = setTimeout(() => {
- fn();
- }, ms);
- }, deps);
- const cancel = () => {
- clearTimeout(timeout.current);
- timeout = null;
- };
- return [cancel];
- };
- export default useDebounce;
- const Home = (props) => {
- const [a, setA] = useState(0);
- const [b, setB] = useState(0);
- const [cancel] = useDebounce(
- () => {
- setB(a);
- },
- 2000,
- [a]
- );
- const changeIpt = (e) => {
- setA(e.target.value);
- };
- return (
- <div>
- <input type="text" onChange={changeIpt} />
- {b} {a}
- </div>
- );
- };
性能优化
单向数据流,各组件层次分明,状态明确,但是当项目体量大,组件嵌套多的时候,性能损耗也非常大,所以我们会做一些性能优化的工作。
可能的优化场景有:
- 单个 state 的更新会影响全局,有一点需要注意的是,被context包裹的组件,只要value的任何一个变动,都会重新渲染,useMemo和useCallback就会失效。
- 相同的输入,不再重新计算
- 父组件的函数在子组件使用的时候
其实useMemo和useCallback的核心思想相同,都是记录上一次的输入,如果下一次输入与上一次相同,将不会计算,直接获取上一次的结果。他们的区别只是形式上的,useMemo返回一个 memoized 值。而useCallback返回的是memoized回调函数。
useMemo缓存计算结果的值。
useCallback主要用于缓存函数。
useCallback(fn, deps) 相当于 useMemo(() => fn, deps)。
- const memoizedCallback = useCallback(() => {
- doSomething(a, b);
- }, [a, b]);
- // useMemo
- const [count, setCount] = useState(1);
- const [val, setValue] = useState("");
- const expensive = useMemo(() => {
- let sum = 0;
- for (let i = 0; i < count * 100; i++) {
- sum += i;
- }
- return sum;
- }, [count]);
- return (
- <div>
- <h4>
- {count}-{expensive}
- </h4>
- {val}
- <div>
- <button onClick={() => setCount(count + 1)}>+c1</button>
- <input value={val} onChange={(event) => setValue(event.target.value)} />
- </div>
- </div>
- );
捎带了解一下memoized,简单讲就是把函数的计算结果缓存起来,比如递归。
- const memoize = function(fn) {
- const cache = {};
- return function() {
- const key = JSON.stringify(arguments);
- var value = cache[key];
- if(!value) {
- console.log('新值,执行中...'); // 为了了解过程加入的log,正式场合应该去掉
- value = [fn.apply(this, arguments)]; // 放在一个数组中,方便应对undefined,null等异常情况
- cache[key] = value;
- } else {
- console.log('来自缓存'); // 为了了解过程加入的log,正式场合应该去掉
- }
- return value[0];
- }
- }
- module.exports = memoize;
- const memoize = require('./memoize.js');
- const log = console.log;
- // 斐波那契数组
- const fibonacci = (n) => {
- return n < 2
- ? n
- : fibonacci(n - 1) + fibonacci(n - 2);
- };
- const memoizeFibonacci = memoize(fibonacci);
- log(memoizeFibonacci(45)); // 新值,执行中...; 1134903170 // 等待时间比较长
- log(memoizeFibonacci(45)); // 来自缓存; 1134903170
- log(memoizeFibonacci(45)); // 来自缓存; 1134903170
- log(memoizeFibonacci(45)); // 来自缓存; 1134903170
本文转载自微信公众号「 惊天码盗」,可以通过以下二维码关注。转载本文请联系 惊天码盗公众号。