React Hooks知根知底

237 阅读3分钟

hooks概览

  • 单个组件数据管理:useState & useReducer

  • 变量/方法的缓存:useMemo & useCallback

  • 事件触发:useEffect & useLayoutEffect

  • 避免状态改变/触发rerender的数据存储:useRef

  • 通过ref自定义并暴露方法:useImperativeHandle & forwardRef

  • 跨组建数据状态管理:useContext(配合createContext)

  • react 18 rn 0.69新的hooks:useDeferredValue & useTransition ....

hooks使用demo&介绍

1.useState & useReducer

创建组件数据的hook

  • 每次调用改变函数会重新走整个组件方法
  const [test, setTest] = useState(0);
  console.warn(test);
  return (
    <View style={{ marginTop: 100 }}>
      <BlButton
        title="test"
        onPress={() => {
          setTest(current => current + 1);
        }}
      />
    </View>
  );
  • useReducer的基础用法
type myAction =
  | {
      type: 'ADD';
      payload: {
        value: string;
        index: number;
      };
    }
  | {
      type: 'DELETE';
      payload: {
        index: number;
      };
    }
  | {
      type: 'INIT';
      payload: {
        data: string[];
      };
    };
    
const [test, testDispatch] = useReducer((prevState: string[], action: myAction) => {
  switch (action.type) {
    case 'INIT':
      return prevState;
    case 'ADD':
      prevState.splice(action.payload.index, 0, action.payload.value);
      return [...prevState];
    case 'DELETE':
      prevState.splice(action.payload.index, 1);
      return [...prevState];
  }
}, []);
console.warn(test);
return (
  <View style={{ marginTop: 100 }}>
    <BlButton
      title="test"
      onPress={() => {
        testDispatch({ type: 'ADD', payload: { value: '1', index: 0 } });
      }}
    />
  </View>
);
    
const [test, testAppend] = useReducer(
  (prevState: string[], data: string[] | undefined) => (data ? [...prevState, ...data] : []),
  [],
);
console.warn(test);
return (
  <View style={{ marginTop: 100 }}>
    <BlButton
      title="test"
      onPress={() => {
        testAppend(['1', '2']);
      }}
    />
  </View>
);
  • 在useEffect/自定义函数会自动合并,在setTimeout/fetch等异步方法中会调用多次
const [test, setTest] = useState(0);
console.warn(test);
return (
  <View style={{ marginTop: 100 }}>
    <BlButton
      title="test"
      onPress={() => {
        setTest(current => current + 1);
        setTest(current => current + 1);
      }}
    />
  </View>
);
const [test, setTest] = useState(0);
console.warn(test);
return (
  <View style={{ marginTop: 100 }}>
    <BlButton
      title="test"
      onPress={() => {
        setTimeout(() => {
          setTest(current => current + 1);
          setTest(current => current + 1);
        }, 0);
      }}
    />
  </View>
);

会多次setState的页面如果复杂计算没有使用hooks缓存,将会有性能损耗

const [test, setTest] = useState('');
console.time('time');
Array(100000).map((current, index) => Math.pow(index, 2));
console.timeEnd('time');
return (
  <View style={{ marginTop: 100 }}>
    <BlInput style={{ backgroundColor: 'red' }} value={test} onChangeText={setTest} />
  </View>
);

2. useMemo & useCallback

数据/方法的缓存

  • useCallback是useMemo的封装
const test1 = useMemo(() => () => {}, []);
const test2 = useCallback(() => {}, []);
  • 加了缓存,依赖里的内容没有变动,那么内容不会重新加载
const [test, setTest] = useState('');
console.time('time');
const data = useMemo(() => Array(100000).map((current, index) => Math.pow(index, 2)), []);
const data1 = useMemo(() => Array(100000).map((current, index) => Math.pow(index, 2)), []);
const data2 = useMemo(() => Array(100000).map((current, index) => Math.pow(index, 2)), []);
const data3 = useMemo(() => Array(100000).map((current, index) => Math.pow(index, 2)), []);
console.timeEnd('time');
return (
  <View style={{ marginTop: 100 }}>
    <BlInput style={{ backgroundColor: 'red' }} value={test} onChangeText={setTest} />
  </View>
);

  • 依赖内部为浅比较,对象地址变化会导致重复加载
const myData = { content: '一段文本' };
const [test, setTest] = useState('');
console.time('time');
const data = useMemo(() => {
  console.warn(myData);
  return Array(100000).map((current, index) => Math.pow(index, 2));
}, [myData]);
console.timeEnd('time');
return (
  <View style={{ marginTop: 100 }}>
    <BlInput style={{ backgroundColor: 'red' }} value={test} onChangeText={setTest} />
  </View>
);

3.useEffect & useLayoutEffect

useLayoutEffect会在页面渲染前触发,一般不使用,多数用于navigation.setOptions()方法,设置同一页面的动态headerLeft / title / headerRight 等

  • 所有hooks的第二个数组参数为浅比较
const [test, setTest] = useState({ a: 'test' });
useEffect(() => {
  console.warn(test.a);
}, [test]);
useEffect(() => {
  setTest(current => {
    current.a = 'test1';
    return current;
  });
}, []);
  • 对于类似搜索的场景,无需手动调用方法
const [searchText, setSearchText] = useState('');
const timer = useRef();
const update = useCallback(() => {
  timer.current && clearTimeout(timer.current);
  timer.current = setTimeout(() => {
    console.warn(searchText);
  }, 1000);
}, [searchText]);
useEffect(() => {
  update();
}, [update]);
  • 针对上面场景,可以换种写法减少一次函数绑定
const [searchText, setSearchText] = useState('');
const timer = useRef();
const update = useCallback((value?: string) => {
  timer.current && clearTimeout(timer.current);
  timer.current = setTimeout(() => {
    console.warn(value);
  }, 1000);
}, []);
useEffect(() => {
  update(searchText);
}, [update, searchText]);

4.useRef

在组件首次加载后,不再初始化,改变内容不会触发re-render。也可作为组件的标识来调用组件内部方法

  • 比如在提交表单时,可以使用ref来避免re-render
const [data, setData] = useState('');
const submit = useCallback(() => {
  console.warn(data);
}, [data]);

return (
  <View style={{ marginTop: 100 }}>
    <BlInput style={{ backgroundColor: 'red' }} onChangeText={setData} />
    <BlButton title="test" onPress={submit} />
  </View>
);

改进:

const dataRef = useRef('');
const changeData = useCallback((value: string) => {
  dataRef.current = value;
}, []);
const submit = useCallback(() => {
  console.warn(dataRef.current);
}, []);

return (
  <View style={{ marginTop: 100 }}>
    <BlInput style={{ backgroundColor: 'red' }} onChangeText={changeData} />
    <BlButton title="test" onPress={submit} />
  </View>
);

5. useImperativeHandle & forwardRef

暴露子组件内部方法,在外部可以通过useRef调用

const InnerComponent = forwardRef(
  memo((props, ref) => {
    const toast = useCallback(() => {
      console.warn('toast');
    }, []);
    useImperativeHandle(ref, () => ({ toast }));
    return <View style={{ width: 200, height: 200, backgroundColor: 'red' }} />;
  }),
);

const innerRef = useRef();
const onPress = useCallback(() => {
  innerRef.current?.toast();
}, []);
return (
  <TouchableOpacity onPress={onPress}>
    <InnerComponment ref={innerRef} />
  </TouchableOpacity>
);

6.useContext

需要配合createContext使用,跨组建数据传递。优点是后退组件销毁后不占内存;每个dispatch各自独立,分多个对象存储,数据层面有一定安全性。

const [test, setText] = useState('');
const TestContext = createContext();

<TextContent.Provider>
  <InnerComponent />
</TextContent.Provider>

const InnerComponent = () => {
  const {test, setTest} = useContext(TestContext);
  console.warn(test);
  return (
    <View style={{ marginTop: 100 }}>
      <BlInput style={{ backgroundColor: 'red' }} onChangeText={setTest} />
    </View>
  );
}

作者:@刘力瑞 黑湖科技移动开发工程师