React Hooks用法详解(二)

React Hooks

在了解React Hooks之前, 我们先看一下 Class函数式 的一般写法

Class组件 一般写法

import React from "react";
import ReactDOM from "react-dom";
import PropTypes from "prop-types";

class App extends React.Component {
  // 默认参数
  static defaultProps = {
    message: "Hello World",
    firstName: 'Yang',
    lastName: 'yu'
  };

  // 传参检查
  static propTypes = {
    message: PropTypes.string
  };

  /**
   * 方便调试
   */
  static displayName = "App";

  // 状态
  state = {
    count: 1
  };

  /**
   * 解决绑定 this 的几种方法
   * 1. onClick={this.increment.bind(this)}
   * 2. 在 constructor中绑定 tihs ==> this.increment = this.increment.bind(this)
   * 3 箭头函数: 浪费内存
   */
  increment = () => {
    this.setState({
      count: 2
    });
  };

  // 计算属性
  get name() {
    return this.props.firstName + this.props.lastName
  }

  // 生命周期
  componentDidMount() {}

  render() {
    return (
      
{this.props.message}
{this.state.count}
{this.name}
); } } const rootElement = document.getElementById("root"); ReactDOM.render(, rootElement);

函数组件一般写法

import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";

interface Props {
  message?: string;
}

const App: React.FunctionComponent = props => {
  /**
   * 如何使用 state,  React版本 > 16.8
   * useState 返回一个数组, 解构
   */
  const [count, setCount] = useState(0);
  const [count1, setCount1] = useState(0);

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

  const increment1 = () => {
    setCount1(count1 + 1);
  };

  /**
   * 生命周期 代替 componetDidMount componentDidUpdate
   */
  useEffect(() => {
    console.log('componentDidMount() 或者 componentDidUpdate()')
  })

  // 第二个参数是 []
  useEffect(() => {
    console.log('只在第一次执行')
    // axios.get()
  }, [])
  
  // 当某个 参数更新, 才触发
  useEffect(() => {
    console.log('count 更新了之后执行, 点击 count1 不会更新')
  }, [count])

  useEffect(() => {
    return () => {
      console.log('我死了')
    }
  })

  return (
    
{props.message}
{count}
{count1}
); }; App.defaultProps = { message: 'Hello World' } App.displayName = 'yym' const rootElement = document.getElementById("root"); ReactDOM.render(, rootElement);

下面我们来看看一些 Hooks API

useState

  • 使用

    const [n, setN] = React.useState(0)
    const [user, setUser] = React.useState({name: 'yym'})
    
  • 不可局部更新

    • 因为useState不会帮我们合并属性

      setUser({
          ...user,
          name: 'yym1'
      })
      
  • 地址会变

    • setState(obj) 如果 obj 地址不变, 那么 React 认为数据没有变化
  • 接收函数

    const [user, setUser] = useState(() => ({name: 'yym2', age: 12})) // 减少多余计算过程
    
    setN(i => i + 1)
    setN(i => i + 1)  // 对  state 多次操作,  
    

简单实现 useState

下面代码可以放到 codesandbox 里面跑一下

看一个简单的例子

const App = () => {
  const [n, setN] = useState(0);
  return (
    
{n}
); }; ReactDOM.render(, document.querySelector("#root")) 1. 进入页面, render App, 调用 App() , n = 0 2. 用户点击 button, 调用 setN(n + 1), 再次 render App, 得到虚拟 DOM, DOM diff 更新真 DOM 3. 每次调用 App, 都会运行 useState(0), n 的值都会改变 QA: 1. 执行 setN 的时候会发生什么? n 会变吗? App() 会重新执行吗? 重新渲染; setN要把n改变, n 不变; App 会重新执行; 2. 如果 App() 会重新执行, 那么 useState(0) 的时候, n 每次值会不同 n 的值会不同

上面的例子我们可以分析:

  • setN
    • setN 一定会修改数据 X, 将n + 1 存入 X, (X 泛指)
    • setN一定会触发, 重新渲染
  • useState
    • useState 会从 X 读取 n 的最新值
  • X
    • 每个组件有自己的数据 X, 我们将其命名为state

根据上面的结论, 我们尝试写一下 useState

import React from "react";
import ReactDOM from "react-dom";

let _state: any;
const myUseState = (initialValue: any) => {
  _state = _state === undefined ? initialValue : _state;
  const setState = (newState: any) => {
    _state = newState;
    // 渲染页面
    render();
  };
  return [_state, setState];
};

// 不管这个实现
const render = () => ReactDOM.render(, document.querySelector("#root"));

const App = () => {
  console.log("App 渲染了");
  
  // 使用自己写的 myUseState
  const [n, setN] = myUseState(0);
  return (
    
{n}
); }; export default App;

上面的代码完全可以正常运行, 但当我们的 myUseState 有两个的时候, 就是下面的代码

import React from "react";
import ReactDOM from "react-dom";

let _state: any;
const myUseState = (initialValue: any) => {
  _state = _state === undefined ? initialValue : _state;
  const setState = (newState: any) => {
    _state = newState;
    // 渲染页面
    render();
  };

  return [_state, setState];
};

// 不管这个实现
const render = () => ReactDOM.render(, document.querySelector("#root" ));

const App = () => {
  console.log("App 渲染了");
  const [n, setN] = myUseState(0);
  const [m, setM] = myUseState(1);
  return (
    
{n}-{m}
); }; export default App;

我们运行上面的代码, 发现更新 n, m 也会改变, 更新m, n 也会改变, 因为所有的数据都放在 _state 会冲突, 该怎么解决?

  1. _state 做成对象

    _state = { m: 0, n: 0}, // 感觉不行,  useState(0) 并不知道变量叫 m 还是 n, 
    
  2. _state 做成数组, 尝试一下

    _state = [0, 0]
    
    // 把 _state 初始为数组
    let _state: any[] = [];
    // 下标
    let index = 0;
    
    const myUseState = (initialValue: any) => {
      // 设置唯一的值, 根据 myUseState 的顺序
      const currentIndex = index;
      _state[currentIndex] =
        _state[currentIndex] === undefined ? initialValue : _state[currentIndex];
      const setState = (newState: any) => {
        _state[currentIndex] = newState;
        // 渲染页面
        render();
      };
      index += 1;
      return [_state[currentIndex], setState];
    };
    
    const render = () => {
      // 每次重新渲染 index 重置
      index = 0;
      ReactDOM.render(, document.querySelector("#root"));
    };
    
  3. 上面数组的方法会有一定的缺点

    • useState的调用顺序
      • 若第一次顺序是 n m K
      • 第二次必须保证顺序一致,
      • useState 不能写在 if
      const [n, setN] = React.useState(0);
      let m, setM;
      if (n % 2 === 1) {
        [m, setM] = React.useState(0);
      }
    
    // 会报错 顺序不能变
    React Hook "React.useState" is called conditionally. React Hooks must be called in the exact same order in every component render. (react-hooks/rules-of-hooks)eslint
    

代码在这里, 我们想一下 mpUseState 还有什么问题呢?

  1. App 组件用了 _stateindex , 那其它组件用什么?
    • 给每个组件创建一个_stateindex
  2. 放在全局作用域重名了怎么办?
    • 放在组件对应的虚拟节点对象上

总结 :

  1. 每个函数组件对应一个 React 节点,

  2. 每个节点保存着 _stateidnex

  3. useState 会读取 state[index]

  4. indexuseState 出现顺序决定

  5. setState 会修改 state, 并触发更新

上面的属于简化 useState 的实现, 我们看一个 useState 出现问题的 代码

// 先点击 log , 再点击 incremwntN,  3s 后打印的是 n: 0, 而不是 n: 1
// 先点击 incrementN, 再点击 log, 3s 后 打印 n: 1

// ? 为啥是旧数据
1. setN 不会改变 n, 生成一个 n 的分身, 改变的不是n, 所以 n: 0, 

import React, { useState } from "react";

const App = () => {
  const [n, setN] = useState(0);
  const log = () =>
    setTimeout(() => {
      console.log(`n: ${n}`);
    }, 3000);
  return (
    
{n}
); }; export default App;

如何让状态始终只是一个?

  • 全局变量

    • window.xxx即可
  • useRef

    const App = () => {
      const nRef = React.useRef(0); // { current: 0}
      const log = () =>
        setTimeout(() => {
          console.log(`n: ${nRef.current}`);
        }, 3000);
      return (
        
    {nRef.current} 不是实时刷新
    ); };
    // 手动触发 App 更新, 强制, 更类似于 Vue3
    const App = () => {
      const nRef = React.useRef(0); // { current: 0}
      const log = () =>
        setTimeout(() => {
          console.log(`n: ${nRef.current}`);
        }, 3000);
      return (
        
    {nRef.current}
    ); };
  • useContext

    • useContext 不仅能贯穿始终, 还能贯穿不同组件

      // 上下文
      const themeContext = createContext(null);
      
      const App = () => {
        const [theme, setTheme] = useState("blue");
        return (
          // Provider 类似于作用域, 里面的可以使用 theme, settheme
          
            

      {theme}

      ); }; const ChildA = () => { const { setTheme } = useContext(themeContext); return (
      ); }; const ChildB = () => { const { setTheme } = useContext(themeContext); return (
      ); };

useReducer

使用方法:

  • useReducer 是复杂点的 useState
import React, { useReducer } from "react";
// 1. 创建初始值
const initial = {
  n: 0
};
// 2. 创建所有操作 reducer(state, action)
const reducer = (state: any, action: any) => {
  if (action.type === "add") {
    return { n: state.n + action.number };
  } else if (action.type === "multiple") {
    return { n: state.n * 2 };
  } else {
    throw new Error("unknown word");
  }
};

const App = () => {
  // 3. 传给 useReducer  得到读和写 API
  const [state, dispatch] = useReducer(reducer, initial);

  const onClick = () => {
    // 4. 写
    dispatch({ type: "add", number: 1 });
  };

  const onClick1 = () => {
    dispatch({ type: "add", number: 2 });
  };

  return (
    
{/* 4. 读取值 */}

n: {state.n}

); }; export default App;

但如何代替 Redux

我们先弄一个初始的页面, 在里面一步步实现 Redux 功能, 示例Demo

// 这是我们初始页面, 有三个组件 User Books Movies
import React, { useReducer } from "react";
const App = () => {
  return (
    

); }; const User = () => { return (

个人信息

); }; export default App;
  1. 先把数据集中到 store

    // 代码和初始一样, 只是往里面加代码
    import React, { useReducer } from "react";
    
    const store = {
      user: null,
      books: null,
      movies: null,
    }
    
  2. 创建 reducer, 使用 usereducer 一样的

    const reducer = (state, action) => {
      switch (action.type) {
        case "setUser":
          return { ...state, user: action.user };
        case "setBooks":
          return { ...state, books: action.books };
        case "setMovies":
          return { ...state, movies: action.movies };
        default:
          throw new Error();
      }
    };
    
  1. 我们使用 createContext, 创建一个上下文

    // 创建一个 context
    const Context = createContext(null);
    
  2. 使用useReducer 并把 useReducer 的读写 API, 放进 Context Value

    const App = () => {
      // 使用 useReducer
      const [state, dispatch] = useReducer(reducer, store);
      return (
        // 把 useReducer 的读写 API 放到 Context value里
        
          
          
    ); };
  3. 经过前四步, 我们就可以在在 User, Books, Movie 的组件中使用 Context.Providevalue

    const User = () => {
      // 使用 useContext
      const { state, dispatch } = useContext(Context);
     
      // 异步请求值
      useEffect(() => {
        axios.get("/user").then((user: any) => {
          dispatch({ type: "setUser", user });
        });
      }, []);
    
      return (
        

    个人信息

    name: {state.user ? state.user.name : ""}
    ); };
  4. 所以可以带到一个简单的 Redux 使用,

    // 所有代码
    import React, { useReducer, createContext, useContext, useEffect } from "react";
    
    // store
    const store = {
      user: null,
      books: null,
      movies: null
    };
    
    // reducer
    const reducer = (state, action) => {
      switch (action.type) {
        case "setUser":
          return { ...state, user: action.user };
        case "setBooks":
          return { ...state, books: action.books };
        case "setMovies":
          return { ...state, movies: action.movies };
        default:
          throw new Error();
      }
    };
    
    // 创建一个 context
    const Context = createContext(null);
    
    const App = () => {
      // 使用 useReducer
      const [state, dispatch] = useReducer(reducer, store);
      return (
        // 把 useReducer 的读写 API 放到 Context value里
        
          
        
      );
    };
    
    // user 可以使用 store 的值
    const User = () => {
      const { state, dispatch } = useContext(Context);
    
      useEffect(() => {
        axios.get("/user").then((user: any) => {
          dispatch({ type: "setUser", user });
        });
      }, []);
    
      return (
        

    个人信息

    name: {state.user ? state.user.name : ""}
    ); }; export default App;

当一个项目比较大, 用到的组件比较多, 使用到的 reducer 比较多, 我们可以模块化

  • user, books, movies 单独建一个文件

  • 把对应的东西 export (default) 出去

    // 例: 重构 reducer
    // reducer
    const reducer = (state, action) => {
      switch (action.type) {
        case "setUser":
          return { ...state, user: action.user };
        case "setBooks":
          return { ...state, books: action.books };
        case "setMovies":
          return { ...state, movies: action.movies };
        default:
          throw new Error();
      }
    };
    
    // ==>
    
    const obj = {
      'setUser': (state, action) => {
        return { ...state, user: action.user };
      },
      // ...
    }
    
    const reducer_copy = (state, action) => {
      const fn = obj[action.type]
      if(fn) {
        return fn(state, action)
      } else {
        throw new Error('type 错误')
      }
    }
    
    
    // user_reducer.js
    export default {
      'setUser': (state, action) => {
        return { ...state, user: action.user };
      },
    }
    
    // books_reducer.js
    // ...
    
    // idnex.js
    import userReducer from './reducers/user_reducer'
    
    const obj = {
      ...userReducer,
      ...otherReducer
    }
    

useContext

useContext 改变一个数的时候, 是通过自顶向下, 逐级更新数据做到的

上下文

  • 全局变量是全局的上下文
  • 上下文是局部的全局变量

在上面的 useReducer 中我们也是用了 useContext,

import React, { createContext, useState, useContext } from "react";

// 1. 创建一个 context 上下文
const Context = createContext(null);

function App() {
  console.log("App 执行了");
  const [n, setN] = useState(0);
  return (
    // 2. 使用 Context.Provide 弄一个 作用域 value 值
    
       
); } function Parent() { // 3. 作用域内使用 useContext const { n, setN } = useContext(Context); return (
我是爸爸 n: {n}
); } function Child() { const { n, setN } = useContext(Context); const onClick = () => { setN(i => i + 1); }; return (
我是儿子 我得到的 n: {n}
); }

useEffect & useLayoutEffect

useEffect

  • 副作用: 对环境的改变就是副作用
  • 其实也可以当做afterRender: 每次render后运行
  1. 用途

    • 作为 componentDidMount 使用, []作为第二个参数
    • 作为componentDidUpdate使用, 可制定依赖
    • 作为componentWillUnMount 使用, 通过 return
    • 以上三种可以同时存在
  2. 特点

    • 如果同时存在多个 useEffect, 会按照出现次序执行
  3. Demo

    import React, { useState, useEffect } from "react";
    
    const App = () => {
      const [n, setN] = useState(0);
      const [m, setM] = useState(0);
      const onClick = () => {
        setN(n + 1);
      };
      const onClcik1 = () => {
        setM(m + 1);
      };
      
      // componentDidMount
      useEffect(() => {
        console.log("第一次渲染, n或m变化我也不打印了");
        document.title = "Hello";
      }, []);
    
      // componentDidUpdate
      useEffect(() => {
        console.log("第一二..N次渲染, 任何state变化我就渲染");
      });
      // componentDidUpdate
      useEffect(() => {
        console.log("第一次渲染, 并且只有m变化我再渲染");
      }, [m]);
     
      // componentDidMount & componentWillUnMount
      useEffect(() => {
        const timerId: any = setInterval(() => {
          console.log("定时器");
        }, 1000);
        return () => {
          window.clearInterval(timerId);
        };
      });
      return (
        
    n: {n}
    m: {m}
    ); }; export default App;

useLayoutEffect

  • 其函数签名与 useEffect 相同,但它会在所有的 DOM 变更之后同步调用 effect。可以使用它来读取 DOM 布局并同步触发重渲染。在浏览器执行绘制之前,useLayoutEffect 内部的更新计划将被同步刷新。
  • useEffect在浏览器渲染完成后执行
  • useLayout 在浏览器渲染前执行

需要截图 老是的白板

  1. 特点
    • useLayoutEffect总是比 useEffect 先执行 (上面说useEffect按次序执行, 如果有useLayoutEffect, 先执行)
    • useLayoutEffect 里的任务最好影响了 Layout
    • 尽可能使用标准的 useEffect 以避免阻塞视觉更新
    • 优先使用useEffect

useMemo & useCallback

  1. 在了解 React.useMemo 之前, 我们先来了解一下 React.memo, 直接看代码

    • 我们平常写React, 经常会有多余的render
    • React.memo 帮助我们控制何时重新渲染组件, 可以和函数式组件一起使用
    // 这是一个普通的父子组件, 子组件使用了父组件的 state
    const App = () => {
      const [n, setN] = useState(0);
      const [m, setM] = useState(0);
      const onClick = () => {
        setN(n + 1)
      }
      return (
        
    }; const Child = (props: any) => { console.log('child 执行了'); // TODO: 大量代码 return
    child: {props.data}
    }
    • 上面的代码我每次更新 n , 在控制台看到会更新 Child组件, 但是我们的 Child 组件, 只依赖m, 但 m 没有变化, 我们不希望Child 执行
    • props 不变, 没必要执行函数组件
    • 但我们怎么优化呢? React.memo
    // 上面的代码改变一下
    
    // 使用 React.memo 包裹一下, 只有当我们改变 m 的时候, Child 才会执行
    const Child = React.memo((props: any) => {
      console.log("child 执行了");
      // TODO: 大量代码
      return 
    child: {props.data}
    ; });
    • React.memo 使得一个组件只有在它的 props变化在执行一遍, 再次执行
  1. 但是React.memo 有一个问题: 添加监听函数, 还是会执行 Child, 看代码

    const App = () => {
      const [n, setN] = useState(0);
      // 每次执行 m = 0, 值是一样的
      const [m, setM] = useState(0);
      const onClick = () => {
        // n 变化不会执行 Child
        setN(n + 1);
      };
      
      // 每次App 执行, 都会重新执行
      // 之前是一个空函数, 现在是另一个空函数, 不是同一个空函数, 引用类型, 地址不同
      const onClickChild = () => {};
      return (
        
    {/* 传一个函数 */}
    ); }; const Child = React.memo((props: any) => { console.log("child 执行了"); // TODO: 大量代码 // 在这里添加了 onClick事件 return
    child: {props.data}
    ; });
    • 怎么解决呢? react.useMemo
  1. React.useMemo

    • 如何使用?

      // render 时: 先根据[name]里面的 name判断, 因为 useMemo 作为一个有着暂存能力, 暂存了上一次的结果
      // 对比上一次 name, name值不变, data就不重新赋值成新的对象, 没有新的对象, 没有新的内存地址, 就不会
      // 重新渲染
      
      const data = useMemo(()=>{
        return {}
      },[m, n])
      
      // 1. 第一个参数 () => value
      // 2. 第二个参数依赖 [m, n]
      // 3. 当依赖变化, 重新计算新的 value
      // 4. 以来不变, 使用之前的 value
      
      //函数
      const data = useMemo(() => {
        return () => {}
      })
      
    • 上面使用 React.memo 改造成React.useMemo

      const App = () => {
        const [n, setN] = useState(0);
        const [m, setM] = useState(0);
        const onClick = () => {
          setN(n + 1);
        };
        
        // useMemo
        const onClickChild = useMemo(() => {
          return () => {};
        }, [m]);
        return (
          
      {/* dlkfs */}
      ); }; const Child = React.memo((props: any) => { console.log("child 执行了"); // TODO: 大量代码 return
      child: {props.data}
      ; });
  1. 有点难用, 于是有了 useCallback, 作用和 useMemo 一样, 是useMemo的语法糖

    • 用法

      useCallback( () => { callback }, [input],)
      
      // 等价于
      
      useMemo(() => () => { callback }, [input])
      
      const onClickChild = useMemo(() => {
        return () => {};
      }, [m]);
      
      const onClickChild = useCallback(() => {}, [m]);
      

useRef

  1. 使用

    目的: 
    1. 如果需要一个值, 在组建不断 render 时保持不变
    使用:
    1. 初始化 => const count = useRef(0)
    2. 读取: count.current
    
    const App = () => {
      const [n, setN] = useState(0);
      // 初始化一个值
      const count = useRef(0);
      const onClick = () => {
        setN(n + 1);
      };
    
      useEffect(() => {
        // 通过 .current 来读取, 更新一次 +1
        count.current += 1;
        console.log(count.current);
      });
      return (
        
    ); };
  2. 为什么需要current ?

    • 为了保证两次 useRef 是同一个值 (只有引用能做到)
  3. 做几个 Hook 对比

    • useState/useReducer 每次都变
    • useMemo/useCallback 依赖变化才变
    • useRef 永远不变
  4. 对比 Vue3 的 ref

    // vue
    
    
    
    // react 的 ref 不会自动更新ui 
    const App = () => {
      const [n, setN] = useState(0);
      const count = useRef(0);
      const onClick = () => {
        setN(n + 1);
      };
    
      const onClick2 = () => {
        count.current += 1;
        // 会改变值 +1
        console.log(count.current, "count...");
      };
    
      return (
        
    {/* 页面没有更新 */}
    ); }; // 在 实现 useState 中我们有如何手动刷新页面的方法, 使用 useState

forwardRef & useImperativeHandle

  1. 简单看个例子

    const App = () => {
      const buttonRef = useRef(null);
      return (
        
    {/* 我们想要获取子组件的 DOM 引用 */} 按钮
    ); }; const ButtonComponent = (props: any) => { console.log(props, 'props....') // {children: "按钮"} 没有得到 ref return
  1. 使用forwardRef

    // 函数组件希望接收别的传来的 ref, 需要 forwardRef 包起来
    // forwardRef 只是多加了一个 ref 参数
    // 改造上面的
    const App = () => {
      const buttonRef = useRef(null);
      return (
        
    按钮
    ); }; // forwardRed 包裹 const Button2 = forwardRef((props, ref) => { return
  1. 总结:

    useRef: 
    1. 可以用来引用 DOM 对象
    2. 也可以用来引用普通对象
    
    forwardRef
    1. 函数式组件 由于 props 不包含 ref, 所以需要 forwardRef
    
  1. forwardRef 相关的 useImperativeHandle, 减少暴露给父组件的属性

    // 官方例子:
    function FancyInput(props, ref) {
      const inputRef = useRef();
      
      // 可以自定义暴露出去的实例值
      // 其实可以叫做 setRef
      useImperativeHandle(ref, () => ({
        focus: () => {
          inputRef.current.focus();
        }
      }));
      return ;
    }
    FancyInput = forwardRef(FancyInput);
    

自定义 Hook

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

你可能感兴趣的:(React Hooks用法详解(二))