Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

细说 hook API #91

Open
george-es opened this issue Mar 5, 2021 · 0 comments
Open

细说 hook API #91

george-es opened this issue Mar 5, 2021 · 0 comments
Labels

Comments

@george-es
Copy link
Owner

为什么要使用 hook 呢?

为了解决 class 中生命周期函数经常包含不相关的逻辑,但又把相关逻辑分离到几个不同方法中的问题。

它优化了三个方面

  • 在组件之间复用状态逻辑很难
  • 复杂组件变的难以理解
  • 难以理解的 class

基础钩子

useState

它会返回一个数组,第一个是当前状态,第二个是更新它的函数,它类似 class 的 this.setState,但是他不会把新的 state 和旧的 state 进行合并。

const [ count, setCount ]  = useState(0)

React 假设当你多次调用 useState 的时候,你能保证每次渲染时它们的调用顺序是不变的。

  • 调用 useState 方法时候做了什么?

    定义了一个 state 变量,并且提供了一个修改该变量的方法,一般来说函数退出后变量就会消失,而 state 中的变量会被保留。

  • useState 需要哪些参数?

    useState 方法里面唯一的参数就是初始 state。它可以是任意类型

  • useState 的返回值是什么?

    返回值是当前 state 以及更新 state 的函数。

  • 为什么叫 useState 而不叫 createState?

    因为 state 在组件首次渲染时候被创建,下次重新渲染时,useState 返回给我们当前的 state,否则它就不是 state,这也是 hook 的名字总是以 use 开头的原因。

useEffect

useEffect 提供了给函数组件增加了操作副作用的能力,它和 class 组件中的 componentDidMount,componentDidUpdate 和 componentWillUnmount 具有相同的用途,只不过被合并成一个 api。

当你调用 useEffect 时,就是告诉 React 在完成对 DOM 的更改后运行你的”副作用“函数,由于副作用函数是在组件内声明的,所以它们可以访问到组件的 props 和 state。React 会在每次渲染后调用副作用函数 —— 包括第一次渲染的时候。

  • useEffect 的返回值是什么?

    副作用函数还可以通过返回一个函数来指定如何“清除”副作用。

    useEffect(() => {
    	const time = setTimeout(() => {})
    	return () => {clearTimout(time)}
    })
    
  • useEffect 做了什么?

    通过这个 hook,你可以告诉 React 组件需要在渲染后执行某些操作,React 会报错你传递的函数,我们称为 effect,并且在执行 DOM 更新后调用它

  • 为什么在组件内部调用 useEffect?

    为了更方便去读取 state 变量和 props,而不需要引入其他 API,它已经保存在函数作用域中,Hook 使用了闭包机制。

  • useEffect 每次渲染后都会执行吗?

    是的,默认情况下,第一次渲染和每次更新后都会执行,要记住,useEffect 是渲染后更新的,此时 DOM 已更新完毕

    function App() {
    	const [count, setCount] = useState(0)
    	console.log(count)
    	useEffect(() => console.log('test'))
    	console.log(count)
    }
    // output
    0
    0
    'test'
    

    因此 effect 里面每次都是最新的值,而不用担心过期

  • useEffect 和 componentDidMount 或 componentDidUpdate 的区别?

    effect 的调度不会阻塞浏览器的屏幕更新,而后者会。

  • 为什么要在 effect 中返回一个函数?

    这是 effect 可选的清除机制,每个 effect 都可以返回一个清除函数,如此可以将添加和移除订阅的逻辑放在一起,他们都属于 effect 的一部分

  • React 何时清除 effect?

    • React 组件卸载时候会执行清除操作

    • React 会在更新 state 时进行清除

    function App() {
    	const [ count, setCount ] = useState(0)
    	useEffect(() => {
    		return () => console.log('clear')
    	}, [count])
    	return <button onClick={() => setCount(2)}> clear </button>
    }
    // 组件卸载和点击 clear 按钮后会执行 effect 中的清除操作
    
  • useEffect 中的第二个参数是怎么用的?

    第二个参数是一个数组,里面包裹着要监听的 state,数组中的任意一个 state 变化都会触发 effect 的更新,它对变化具有准确性 props.friend.id,仅在 id 变化时候才会更新

hook 使用规则

  • 只在最顶层使用 hook

    不要在循环,条件或嵌套函数中调用 hook,因为这样 React 能够在多次的 useStateuseEffect 调用之间保持 hook 状态的正确

  • React 怎么知道哪个 state 对应哪个 useState

    React 靠的是 Hook 调用的顺序

    示例中,Hook 的调用顺序在每次渲染中都是相同的,所以它能够正常工作

    // ------------
    // 首次渲染
    // ------------
    useState('Mary')           // 1. 使用 'Mary' 初始化变量名为 name 的 state
    useEffect(persistForm)     // 2. 添加 effect 以保存 form 操作
    useState('Poppins')        // 3. 使用 'Poppins' 初始化变量名为 surname 的 state
    useEffect(updateTitle)     // 4. 添加 effect 以更新标题
    
    // -------------
    // 二次渲染
    // -------------
    useState('Mary')           // 1. 读取变量名为 name 的 state(参数被忽略)
    useEffect(persistForm)     // 2. 替换保存 form 的 effect
    useState('Poppins')        // 3. 读取变量名为 surname 的 state(参数被忽略)
    useEffect(updateTitle)     // 4. 替换更新标题的 effect
    
    // ...
    

    只要 Hook 的调用顺序在多次渲染之间保持一致,React 就能正确地将内部 state 和对应的 Hook 进行关联。

    如果使用 if ,影响到 hook 的先前顺序,hook 就找不到了,这就导致了bug,如果我们想要有条件地执行一个 effect,可以将判断放到 Hook 的内部

    useEffect(function persistForm() {
        // 👍 将条件判断放置在 effect 中
        if (name !== '') {
        	localStorage.setItem('formData', name);
        }
    });
    

hook 的逻辑状态复用

目前 React 中有两种流行的方式来共享组件之间的状态逻辑,render props 和高阶组件。

当我们想在两个函数之间共享逻辑时,我们会把它提取到第三个函数中。这时候要用到自定义 hook

自定义 Hook 是一个函数,其名称以 “use” 开头,函数内部可以调用其他的 Hook。

function useStatus(id) {
	const [status, setStatus] = useState(false)
	useEffect(() => {
		... // 异步操作
		setStatus(!!(id + xxx))
	})
	return status
}

与 React 组件不同的是,自定义 Hook 不需要具有特殊的标识。我们可以自由的决定它的参数是什么,以及它应该返回什么(如果需要的话),但是它的名字应该始终以 use 开头。

使用

function FriendStatus(props) {
  const isOnline = useStatus(props.friend.id);

  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}
function FriendListItem(props) {
  const isOnline = useStatus(props.friend.id);

  return (
    <li style={{ color: isOnline ? 'green' : 'black' }}>
      {props.friend.name}
    </li>
  );
}

这就完成了逻辑状态复用,FriendStatus 和 FriendListItem 公用了 useStatus 这个状态逻辑,它们只是将两个函数之间一些共同的代码提取到单独的函数中。自定义 Hook 是一种自然遵循 Hook 设计的约定,而并不是 React 的特性。

  • 自定义 Hook 必须以 “use” 开头吗?

    是的,必须如此,这是约定,不遵循的话,由于无法判断某个函数是否包含对其内部 hook 的调用,React 将无法自动检查你的 Hook 是否违反了 hook 的规则。

  • 在两个组件中使用相同的 Hook 会共享 state 吗?

    不会,自定义 Hook 是一种重用状态逻辑的机制(例如设置为订阅并存储当前值),所以每次使用自定义 Hook 时,其中的所有 state 和副作用都是完全隔离的。

  • 自定义 Hook 如何获取独立的 state?

    每次调用 Hook,它都会获取独立的 state。

useContext

额外钩子

useReducer

const [state, dispatch] = useReducer(reducer, initialArg, init);

它和 redux 类似,是 useState 的一种替代方案,返回当前 state 和 dispatch 方法

const initialState = {count: 0};

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    default:
      throw new Error();
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
    </>
  );
}

useCallback

返回一个记忆(memoization)值

在计算机领域,记忆(memoization)是主要用于加速程序计算的一种优化技术,它使得函数避免重复演算之前已被处理过的输入,而返回已缓存的结果。

Memoization 的原理就是把函数的每次执行结果都放入一个对象中,在接下来的执行中,在对象中查找是否已经有相应执行过的值,如果有,直接返回该值,没有才真正执行函数体的求值部分。在对象里找值是要比执行函数的速度要快的。

const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);

以上代码说明,在 a 和 b 的变量值不变的情况下,memoizedCallback 的引用不变,即 useCallback 的第一个入参函数会被缓存,从而达到渲染性能优化的目的。

useCallback(fn, deps) 相当于 useMemo(() => fn, deps)

useMemo

返回一个记忆(memoization) 值

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

上面代码说明,在 a 和 b 变量值不变的情况下, memoizedValue 的值不变,即 useMemo 函数的第一个入参函数不会被执行,从而达到节省计算量的目的。

记住,传入 useMemo 的函数会在渲染期间执行。请不要在这个函数内部执行与渲染无关的操作,诸如副作用这类的操作属于 useEffect 的适用范畴,而不是 useMemo

如果没有提供依赖项数组,useMemo 在每次渲染时都会计算新的值。

你可以把 useMemo 作为性能优化的手段

useCallback 和 useMemo 的区别?

useCallback 和 useMemo 都可缓存函数的引用或值,但是更细腻来说, useCallback 缓存函数的引用,useMemo 缓存计算数据的值。

useRef

返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)返回的 ref 对象在组件的整个生命周期内保持不变。

应用场景就是为了获取 dom 节点

function TextInputWithFocusButton() {
  const inputEl = useRef(null);
  const onButtonClick = () => {
    // `current` 指向已挂载到 DOM 上的文本输入元素
    inputEl.current.focus();
  };
  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  );
}

useImperativeHandle

useImperativeHandle(ref, createHandle, [deps])

useImperativeHandle 可以让你在使用 ref 时自定义暴露给父组件的实例值,大多数情况下,应该避免使用 ref 这样的命令式代码。

useLayoutEffect

useDebugValue

hook Q & A

  • 如何获取上一轮的 props 或 state?

可以通过 ref 来手动实现

const prevCountRef = useRef();
const prevCount = prevCountRef.current;
@george-es george-es added the react label Mar 5, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant