美文网首页React
React Hooks用法详解(二)

React Hooks用法详解(二)

作者: YM雨蒙 | 来源:发表于2020-09-14 17:04 被阅读0次

    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 (
          <div>
            <div>{this.props.message}</div>
            <div>{this.state.count}</div>
            <button onClick={this.increment}>点击</button>
            <div>{this.name}</div>
          </div>
        );
      }
    }
    
    const rootElement = document.getElementById("root");
    ReactDOM.render(<App />, rootElement);
    

    函数组件一般写法

    import React, { useState, useEffect } from "react";
    import ReactDOM from "react-dom";
    
    interface Props {
      message?: string;
    }
    
    const App: React.FunctionComponent<Props> = 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 (
        <div>
          <div>{props.message}</div>
          <div>{count}</div>
          <div>{count1}</div>
    
          <button onClick={increment}>按钮</button>
          <button onClick={increment1}>按钮1</button>
        </div>
      );
    };
    
    App.defaultProps = {
      message: 'Hello World'
    }
    App.displayName = 'yym'
    
    const rootElement = document.getElementById("root");
    ReactDOM.render(<App />, 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 (
        <div>
          <div>{n}</div>
          <button onClick={() => setN(n + 1)}>incrementN</button>
        </div>
      );
    };
    ReactDOM.render(<App />, 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一定会触发<App />, 重新渲染
    • 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(<App />, document.querySelector("#root"));
    
    const App = () => {
      console.log("App 渲染了");
      
      // 使用自己写的 myUseState
      const [n, setN] = myUseState(0);
      return (
        <div>
          <div>{n}</div>
          <button onClick={() => setN(n + 1)}>incrementN</button>
        </div>
      );
    };
    
    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(<App />, document.querySelector("#root" ));
    
    const App = () => {
      console.log("App 渲染了");
      const [n, setN] = myUseState(0);
      const [m, setM] = myUseState(1);
      return (
        <div>
          <div>
            {n}-{m}
          </div>
          <button onClick={() => setN(n + 1)}>incrementN</button>
          <button onClick={() => setM(m + 1)}>incrementM</button>
        </div>
      );
    };
    
    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(<App />, 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 (
        <div>
          <div>{n}</div>
          <button onClick={() => setN(n + 1)}>incrementN</button>
          <button onClick={log}>log</button>
        </div>
      );
    };
    
    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 (
          <div>
            <div>{nRef.current} 不是实时刷新</div>
            <button onClick={() => (nRef.current += 1)}>incrementN</button>
            <button onClick={log}>log</button>
          </div>
        );
      };
      
      
      // 手动触发 App 更新, 强制, 更类似于 Vue3
      const App = () => {
        const nRef = React.useRef(0); // { current: 0}
        const log = () =>
          setTimeout(() => {
            console.log(`n: ${nRef.current}`);
          }, 3000);
        return (
          <div>
            <div>{nRef.current}</div>
            <button onClick={() => (nRef.current += 1)}>incrementN</button>
            <button onClick={log}>log</button>
          </div>
        );
      };
      
    • useContext

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

        // 上下文
        const themeContext = createContext(null);
        
        const App = () => {
          const [theme, setTheme] = useState("blue");
          return (
            // Provider 类似于作用域, 里面的可以使用 theme, settheme
            <themeContext.Provider value={{ theme, setTheme }}>
              <div style={{ backgroundColor: theme === "red" ? "red" : "blue" }}>
                <p>{theme}</p>
                <div>
                  <ChildA />
                </div>
                <div>
                  <ChildB />
                </div>
              </div>
            </themeContext.Provider>
          );
        };
        
        const ChildA = () => {
          const { setTheme } = useContext(themeContext);
          return (
            <div>
              <button onClick={() => setTheme("red")}>red</button>
            </div>
          );
        };
        
        const ChildB = () => {
          const { setTheme } = useContext(themeContext);
          return (
            <div>
              <button onClick={() => setTheme("blue")}>blue</button>
            </div>
          );
        };
        

    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 (
        <div>
          {/* 4. 读取值 */}
          <h1>n: {state.n}</h1>
    
          <button onClick={onClick}>add+1</button>
          <button onClick={onClick1}>add+2</button>
        </div>
      );
    };
    
    export default App;
    
    

    但如何代替 Redux

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

    // 这是我们初始页面, 有三个组件 User Books Movies
    import React, { useReducer } from "react";
    const App = () => {
      return (
        <div>
          <User />
          <hr />
          <Books />
          <Movies />
        </div>
      );
    };
    const User = () => {
      return (
        <div>
          <h1>个人信息</h1>
        </div>
      );
    };
    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里
          <Context.Provider value={{ state, dispatch }}>
            <User />
            <hr />
            <Books />
            <Movies />
          </Context.Provider>
        );
      };
      
      
    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 (
          <div>
            <h1>个人信息</h1>
            <div>name: {state.user ? state.user.name : ""}</div>
          </div>
        );
      };
      
      
    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里
          <Context.Provider value={{ state, dispatch }}>
            <User />
          </Context.Provider>
        );
      };
      
      // user 可以使用 store 的值
      const User = () => {
        const { state, dispatch } = useContext(Context);
      
        useEffect(() => {
          axios.get("/user").then((user: any) => {
            dispatch({ type: "setUser", user });
          });
        }, []);
      
        return (
          <div>
            <h1>个人信息</h1>
            <div>name: {state.user ? state.user.name : ""}</div>
          </div>
        );
      };
      
      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 值
        <Context.Provider value={{ n, setN }}>
           <div className="App">
             <Parent />
           </div>
        </Context.Provider>
      );
    }
    
    function Parent() {
      // 3. 作用域内使用 useContext
      const { n, setN } = useContext(Context);
      return (
        <div>
          我是爸爸 n: {n} <Child />
        </div>
      );
    }
    
    function Child() {
      const { n, setN } = useContext(Context);
      const onClick = () => {
        setN(i => i + 1);
      };
      return (
        <div>
          我是儿子 我得到的 n: {n}
          <button onClick={onClick}>+1</button>
        </div>
      );
    }
    

    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 (
          <div>
            n: {n}
            <button onClick={onClick}>+n</button>
            <hr />
            m: {m}
            <button onClick={onClcik1}>+m</button>
          </div>
        );
      };
      
      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 (
          <div>
            <button onClick={onClick}>update: {n}</button>
            <Child data={m} />
          </div>
      };
      
      const Child = (props: any) => {
        console.log('child 执行了');
        // TODO: 大量代码
        return <div>child: {props.data}</div>
      }
      
      
      • 上面的代码我每次更新 n , 在控制台看到会更新 Child组件, 但是我们的 Child 组件, 只依赖m, 但 m 没有变化, 我们不希望Child 执行
      • props 不变, 没必要执行函数组件
      • 但我们怎么优化呢? React.memo
      // 上面的代码改变一下
      
      // 使用 React.memo 包裹一下, 只有当我们改变 m 的时候, Child 才会执行
      const Child = React.memo((props: any) => {
        console.log("child 执行了");
        // TODO: 大量代码
        return <div>child: {props.data}</div>;
      });
      
      • 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 (
          <div>
            <button onClick={onClick}>update: {n}</button>
            {/* 传一个函数 */}
            <Child data={m} onClick={onClickChild} />
          </div>
        );
      };
      
      const Child = React.memo((props: any) => {
        console.log("child 执行了");
        // TODO: 大量代码
        // 在这里添加了 onClick事件
        return <div onClick={props.onClick}>child: {props.data}</div>;
      });
      
      • 怎么解决呢? 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 (
            <div>
              <button onClick={onClick}>update: {n}</button>
              {/* dlkfs */}
              <Child data={m} onClick={onClickChild} />
            </div>
          );
        };
        
        const Child = React.memo((props: any) => {
          console.log("child 执行了");
          // TODO: 大量代码
          return <div onClick={props.onClick}>child: {props.data}</div>;
        });
        
    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 (
          <div>
            <button onClick={onClick}>update: {n}</button>
          </div>
        );
      };
      
    2. 为什么需要current ?

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

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

      // vue
      <template>
       <div @click="increment">
          ref会更新: {count}
        </div>
      </template>
      <script>
        import  { ref } from 'vue'
       export default {
          setup() {  
            // 直接使用 ref
            const count = ref(0)
               const increment = () => {
              count.value++
            }
            return { count, increment}
          }
        }
      </script>
      
      // 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 (
          <div>
            <button onClick={onClick}>update: {n}</button>
            {/* 页面没有更新 */}
            <button onClick={onClick2}>update count: {count.current}</button>
          </div>
        );
      };
      
      // 在 实现 useState 中我们有如何手动刷新页面的方法, 使用 useState
      

    forwardRef & useImperativeHandle

    1. 简单看个例子

      const App = () => {
        const buttonRef = useRef(null);
        return (
          <div>
            {/* 我们想要获取子组件的 DOM 引用 */}
            <ButtonComponent ref={buttonRef}>按钮</ButtonComponent>
          </div>
        );
      };
      
      const ButtonComponent = (props: any) => {
        console.log(props, 'props....')  // {children: "按钮"} 没有得到 ref
        return <button className="red" {...props} />;
      };
      
      // warning:  Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?
      
      // 上面的报错, 函数组件不支持 ref, 可以使用 React.forwardRef()
      
    1. 使用forwardRef

      // 函数组件希望接收别的传来的 ref, 需要 forwardRef 包起来
      // forwardRef 只是多加了一个 ref 参数
      // 改造上面的
      const App = () => {
        const buttonRef = useRef(null);
        return (
          <div>
            <Button2 ref={buttonRef}>按钮</Button2>
          </div>
        );
      };
      
      // forwardRed 包裹
      const Button2 = forwardRef((props, ref) => {
        return <button className="red" ref={ref} {...props} />;
      });
      
    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 <input ref={inputRef} ... />;
      }
      FancyInput = forwardRef(FancyInput);
      

    自定义 Hook

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

    相关文章

      网友评论

        本文标题:React Hooks用法详解(二)

        本文链接:https://www.haomeiwen.com/subject/zvwdqhtx.html