从函数式编程理解 React Hooks

2019 年 2 月 6 日,React 官方 推出 React v16.8.0,稳定的 Hooks 功能出世。

React Hooks 和 函数式组件的配合,更能适应函数式编程的思维。

  1. 数学上定义的函数公式是:y = f(x)
  2. 如果将状态 state视为 输入 x视图 UI视为 输出 y,编写的函数组件为 Fn,那么可以写出这样一个式子:UI = Fn(state)

  1. 将 React 的 Hooks,按自变量和因变量划分

从函数式编程理解 React Hooks_第1张图片

自变量

为什么说这三个 hook 是自变量呢?

因为他们如果值发生改变,会导致组件UI更新,和依赖它们这些值的 Hook 发生变化。

按照我们上述的 UI = Fn(state),他们就相当于这些 state

useState

基本使用

// 创建 state 的方式
const [state, setState] = useState(defaultState)

// 更新方式一:传值
setState(nextState);

// 更新方式二:传函数
// 传入的函数,会接收到一个参数是原先 state 的值,该函数的返回值将作为更新后 state 的值
setState(preState => nextState);

示例

// 写一个组件:input 框的值改变,p 标签的值跟着改变
import { useState } from 'react';

function StateDemo(props) {
  const [val, setVal] = useState('');
  const changeHandler = (e) => setVal(e.target.value)

  return (
    

{val}

) } export default StateDemo

useReducer

基本使用

// 创建方式
/**
  * 传入参数:
  *  reducer{Function}: 形如 (state, action) => {},该函数返回的值作为更新的 reducerState
  *  initialArg{any}: 若无 init 初始化函数,则 initialArg 直接作为 reducerState。
  *                   若有 init 初始化函数,则 initialArg 作为 init 参数
  *  init{Function}: init 函数的返回值,作为初始化的 reducerState
  * 输出参数
  *  reducerState{any}: 状态
  *  dispatch{Function}: 用来更新状态
  */
const [reducerState, dispatch] = useReducer(reducer, initialArg, init);

示例

import { useReducer } from 'react';

const reducer = (state, action) => {
  const { count } = state;
  switch (action.type) {
    case 'increment':
      return { count: count + 1 }
    case 'decrement':
      return { count: count - 1 }
    default:
      return { count: count }
  }
}

const init = (initialArg) => {
  return { count: initialArg }
}

function ReducerDemo(props) {
  const [reducerState, dispatch] = useReducer(reducer, 0, init);

  return (
    

{reducerState.count}

) } export default ReducerDemo

useContext

在 useContext 推出之前,我们使用 createContext 的方式如下

// Father 组件
import { Component, createContext } from 'react';
export const Context = createContext();

class Father extends Component {
	state = {
  	name: 'John'
  }
  render() {
  	return (
      
        
      
    )
  }
}
// Son 组件
import { Component } from 'react';
import { Context } from './Father'
// 类组件的写法如下
class Son extends Component {

  render() {
		return (
      
        {
          (value) => 

{value}

}
) } } // 函数式组件的写法如下 function Son(props) { return ( { (value) =>

{value}

}
) } export default Son;

在 useContext 推出之后,我们使用 createContext 的方法有了变化

基本使用

useContext需要和 createContext配合使用

// 创建一个 context 实例
const ContextInstance = createContext(defaultValue);

// 使用 useContext 获取到 context 实例的值
const contextValue = useContext(ContextInstance);

示例

利用上述FatherSon例子,用useContextSon改写

// Son 组件
import { Component } from 'react';
import { Context } from './Father'

function Son(props) {
  const contextValue = useContext(Context);
  return (
    

{contextValue}

) } export default Son;

因变量

为什么说他们是因变量呢?

因为组件state的变化,可能引起他们的变化。

它们依赖于一些值,会随着值的变化而重新执行

useMemo

基本用法

作用:用来缓存任意的值

性能优化:可以使用 useMemo 来阻止昂贵的、资源密集型的功能不必要地运行

/**
  * 输入参数
  *   fn{Function}: 形如 () => value,返回的值,将作为 useMemo 的输出
  *   dependencies{Array | undefined}: useMemo 依赖的所有自变量,任意一个自变量变化,都会让 useMemo 重新计算返回值
  *                                    dependencies 为 undefined 时,函数组件每次执行,useMemo 都会重新计算返回值 
  * 输出参数:
  *   memoriedValue{any}:输入参数 fn 返回的值
  */
const memoizedValue = useMemo(fn, dependencies);

示例

如下组件,新增 todo 列表会卡顿,原因是每次都触发 expensiveCalculation

解决方式这里列举两种:

  1. 使用 useMemo
  2. 状态下移
import { useState } from "react";
// 繁重的计算
const expensiveCalculation = (num) => {
  console.log("Calculating...");
  for (let i = 0; i < 1000000000; i++) {
    num += 1;
  }
  return num;
};

const App = () => {
  const [count, setCount] = useState(0);
  const [todos, setTodos] = useState([]);
  const calculation = expensiveCalculation(count);

  const increment = () => {
    setCount((c) => c + 1);
  };
  const addTodo = () => {
    setTodos((t) => [...t, "New Todo"]);
  };

  return (
    

My Todos

{todos.map((todo, index) => { return

{todo}

; })}

Count: {count}

Expensive Calculation

{calculation}
); }; export default App

使用 useMemo

import { useState, useMemo } from "react";

// 繁重的计算
const expensiveCalculation = (num) => {
  console.log("Calculating...");
  for (let i = 0; i < 1000000000; i++) {
    num += 1;
  }
  return num;
};

const App = () => {
  const [count, setCount] = useState(0);
  const [todos, setTodos] = useState([]);
  // 使用 useMemo
  const calculation = useMemo(() => expensiveCalculation(count), [count]);

  const increment = () => {
    setCount((c) => c + 1);
  };
  const addTodo = () => {
    setTodos((t) => [...t, "New Todo"]);
  };

  return (
    

My Todos

{todos.map((todo, index) => { return

{todo}

; })}

Count: {count}

Expensive Calculation

{calculation}
); }; export default App

状态下移

将 todo 列表抽成一个组件,todo 的状态变化,不会引起 expensiveCalculation 的重新计算

import { useState } from "react";
import Todo from './Todo'
// 繁重的计算
const expensiveCalculation = (num) => {
  console.log("Calculating...");
  for (let i = 0; i < 1000000000; i++) {
    num += 1;
  }
  return num;
};

const App = () => {
  const [count, setCount] = useState(0);
  const calculation = useMemo(() => expensiveCalculation(count), [count]);

  const increment = () => {
    setCount((c) => c + 1);
  };

  return (
    

Count: {count}

Expensive Calculation

{calculation}
); }; export default App import { useState } from "react"; const Todo = () => { const [todos, setTodos] = useState([]); const addTodo = () => { setTodos((t) => [...t, "New Todo"]); }; return (

My Todos

{todos.map((todo, index) => { return

{todo}

; })}
) } export default Todo;

useCallback

作用:用来缓存函数

基本用法

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

/**
  * 输入参数
  *   fn{Function}: 形如 () => value,这个函数将作为 useCallback 的输出
  *   dependencies{Array | undefined}: useCallback 依赖的所有自变量,任意一个自变量变化,都会让 useCallback 重新生成一个函数返回
  *                                    dependencies 为 undefined 时,函数组件每次执行,useCallback 都会重新生成一个函数返回
  * 输出参数:
  *   memorieFn{Function}:参数 fn
  */
const memorieFn = useCallback(fn, dependencies);

useEffect

在了解 useEffect 之前,我们先来了解一下什么是 副作用。

在计算机科学中,函数副作用指当调用函数时,除了返回函数值之外,还对主调用函数产生附加的影响 —— 维基百科

副作用是函数调用过程中除了返回值以外,对外部上下文的影响。

从函数式编程理解 React Hooks_第2张图片

基本用法

在 useEffect 中,常做的副作用操作有

  • 操作 DOM 元素
  • 修改数据:比如 setState、修改 ref 指向
  • 发送 HTTP 请求

由于 useEffect 是在组件渲染完成之后调用的,所以在这个时机,进行副作用的操作

useEffect(()=>{
	// 执行需要的副作用操作
  
  // 返回的函数,会在该组件被卸载时调用
  return () => {
  	// 组件卸载时,执行的副作用操作
  }
}, dependencies)

示例

  1. 操作 DOM
import { useEffect } from 'react';
const EffectDemo = () => {

  useEffect(() => {
    const img = document.getElementById('img');
    img.src = 'https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fup.enterdesk.com%2Fedpic%2F30%2F90%2F40%2F309040a0602c672cebc6ab3a1bbbc8cd.jpg&refer=http%3A%2F%2Fup.enterdesk.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1646197473&t=21786df0de7c0a297437b6d14bbfd5af';
  }, [])

  return (
    
  )
}
export default EffectDemo
  1. 发送 HTTP 请求并修改数据
import { useState, useEffect } from 'react';
import axios from 'axios';
const EffectDemo = () => {
  const [lists, setList] = useState([])
  useEffect(() => {
    async function fetchData() {
      return await axios.get('https:/test.com/api/getList');
    }
    const data = fetchData();
    setList(data)
  }, [])

  return (
    
    {lists.map(item =>
  • {item.name}
  • )}
) } export default EffectDemo

用于追踪的 useRef

由于 useRef 返回的 ref 对象,该对象在组件的整个生命周期都保持不变,只能手动改变值。

⭐ ref 值的变化,不会引起组件的更新。

补充:createRef 创建的 ref,如果是在组件内声明的,组件更新时,会创建新的 ref

为什么要叫追踪的 useRef 呢,因为它创建的值只能手动改变,它不会变化。

利用这个特点,我们把 DOM 元素 / 某些数据 存放在 ref 上。即使组件更新,引用值还是没变。

相当于我们追踪了某个东西,不管他跑到那,都追着他不放。

或者叫做 用于访问的 ref 也不错。

当我们用 ref 绑定了组件内部某个数据,暴露给组件外界使用时,外界可以访问组件内部的数据。

为什么 react 官方说,少点用 ref 呢?

个人理解是,react 希望组件的编写更加符合函数式编程,如果外界可以访问组件内部的数据,甚至修改组件内部数据。那么根据函数式编程的  UI = Fn(state) 就变成了

Fn = f(ref) 与 UI = Fn(state)

Fn会变得不确定,怎么符合函数式编程的思想:固定输入(state) 产生 固定输出(UI)

基本用法

/*
 * 输入参数:
 *     initalValue{any}: 任何的数据。
 * 返回参数:
 *     refContainer{object}: { current: initialValue }
 */
const refContainer = useRef(initialValue);

示例

1. 追踪某个 DOM 元素

const RefDemo = () => {
  const [count, setCount] = useState(0)
  const ref = useRef();

  useEffect(() => {
    console.log(ref); // ref.current = 
{count}
}, []) return (
    {count}
) }

2. 追踪类组件实例

import { useRef, Component, useEffect } from 'react';

const Father = () => {
  const classRef = useRef();
  useEffect(() => {
    console.log(classRef);
  }, [])

  return (
    
{ classRef.current = a }} /> {/* 方式二: */}
) } class SonClassCompoent extends Component { render() { return (
sonClass
) } } export default Father

3. 配合 forwardRef 追踪函数式组件中的数据

3.1 追踪函数式组件中的 DOM 元素

从函数式编程理解 React Hooks_第3张图片

import { useRef, forwardRef, useEffect } from 'react';

const SonFunctionCompoent = (props, ref) => {
  return (
sonFunction
) } // 函数式组件用 forwardRef 包裹一层 // forwardRef 会将外界传进来的 ref 属性,转发给函数式组件 SonFunctionCompoent 的第二个参数 const SonFnWithForwardRef = forwardRef(SonFunctionCompoent); const Father = () => { const fnRef = useRef(); useEffect(() => { console.log(fnRef); }, []) return (
) } export default Father

3.2 追踪函数式组件的 state

从函数式编程理解 React Hooks_第4张图片
import { useRef, forwardRef, useEffect, useState } from 'react';

const SonFunctionCompoent = (props, ref) => {
  const [count, setCount] = useState(0);

  useEffect(() => {
    ref.current = count;
  }, [count, ref])

  return (
    
) } const SonFnWithForwardRef = forwardRef(SonFunctionCompoent); const Father = () => { const fnRef = useRef(); return (
) } export default Father

4. 配合 useImperativeHandle 追踪函数式组件的自定义 ref

先来看看 useImperativeHandle 的用法

/**
  * 输入参数:
  *    ref{Ref 实例}: 函数式组件外部传入的 ref
  *    createHandle{Function}: 该函数返回的值,将作为 ref.current 的值
  *    deps: 依赖的参数,参数变化,重新计算 ref
  */

useImperativeHandle(ref, createHandle, [deps]);

如果需要限制外部访问组件内部,特定数据的属性方法,可以考虑使用这个函数

从函数式编程理解 React Hooks_第5张图片

import { useRef, forwardRef, useState, useImperativeHandle } from 'react';

const SonFunctionCompoent = (props, ref) => {
  const [data, setData] = useState({});

  useImperativeHandle(ref, () => {
    return {
      name: data.name,
      pwd: data.pwd
    }
  }, [data])

  const handleSubmit = (e) => {
    const formElement = e.target;
    const nameElement = formElement[0];
    const pwdElement = formElement[1];
    const data = {
      name: nameElement.value,
      pwd: pwdElement.value
    }
    setData(data);
  }

  return (
    
{/* 阻止 form 表单默认跳转行为 */}
) } const SonFnWithForwardRef = forwardRef(SonFunctionCompoent); const Father = () => { const fnRef = useRef(); return (
) } export default Father

自定义 hook

概念

hooks 专注的就是逻辑复用,使我们的项目,不仅仅停留在组件复用的层面上。

自定义 hooks 让我们可以将一段通用的逻辑存封起来。

我们自定义的 hooks 大概应该长这样

从函数式编程理解 React Hooks_第6张图片

自定义 hook 的执行时机

hook 本质就是一个函数。

每次组件更新,都会导致执行自定义 hook。

示例

  1. 用于获取请求的 useFetch
import { useState, useCallback, useEffect, useRef } from 'react'
import axios from 'axios'

export const useFetch = (options) => {
  const [loading, setLoad] = useState(false);
  const [data, setData] = useState();
  const [error, setError] = useState('');
  const fetchConfig = useRef(options); // 缓存请求配置
  /**
   * 缓存请求执行函数
   * data{any}: 当 isReset 为 true 时,请求配置为 data
   * isReset{boolean}: 是否需要重置
   */
  const run = useCallback((data, isReset = false) => {
    return new Promise(async (resolve, reject) => {
      setLoad(true);
      if (data) {
        if (isReset) fetchConfig.current = data;
        else {
          if (fetchConfig.method.toLowerCase() === 'get') {
            fetchConfig.current.params = data;
          } else {
            fetchConfig.current.data = data;
          }
        }
      }
      try {
        const res = await axios(data);
        setLoad(false);
        setData(res)
        resolve(res);
      } catch (error) {
        setLoad(false);
        setError(error);
        reject(error);
      }
    })
  }, [])

  // 如果第一次有具体的请求数据才发
  useEffect(() => {
    if (options.data || options.params) {
      setLoad(true);
      axios(fetchConfig.current).then(res => {
        setLoad(false);
        setData(res)
      }).catch(err => {
        setLoad(false);
        setError(err);
      })
    }
    return () => options.data = null;
    // eslint-disable-next-line
  }, [])

  return { loading, data, error, run };

}

参考资料

《函数式组件与类组件有何不同?》

《useEffect 完整指南》

《React useMemo Hook》

《新年第一篇:一起来简单聊一下副作用 - effect》

《ahooks —— 一个好用的 hook 库》

《玩转react-hooks,自定义hooks设计模式及其实战》

你可能感兴趣的:(javascript,前端,react.js)