zoukankan      html  css  js  c++  java
  • React Hooks 深入系列

    本文基于近段时间对 hooks 碎片化的理解作一次简单梳理, 个人博客。同时欢迎关注基于 hooks 构建的 UI 组件库 —— snake-design

    在 class 已经融入 React 生态的节点下, React 推出的 Hooks 具有如下优势:

    • 更简洁的书写;
    • 相对类中的 HOCrender Props, Hooks 拥有更加自由地组合抽象的能力;

    使用 Hooks 的注意项

    • hooks 中每一次 render 都有自己的 stateprops, 这与 class 中存在差异, 见 Hooks 每次渲染都是闭包

      • class 中可以用闭包模拟 hooks 的表现, 链接, hooks 中可以使用 ref 模拟 class 的表现, 链接;
    • 写出 useEffect 的所用到的依赖

    在以下 demo 中, useEffect 的第二个参数传入 [], 希望的是 useEffect 里的函数只执行一次(类似在 componentDidMount 中执行一次, 但是注意这里仅仅是类似, 详细原因见上一条注意项), 页面上每隔 1s 递增 1。

    function Demo() {
      const [count, setCount] = useState(0);
    
      useEffect(() => {
        const id = setInterval(() => {
          setCount(count + 1);
        }, 1000);
        return () => {
          clearInterval(id);
        };
      }, []);
    
      return count;
    }
    

    但这样达到我们预期的效果了么? demo, 可以看到界面上只增加到 1 就停止了。原因就是传入的第二个参数 [] 搞的鬼, [] 表示没有外界状态对 effect 产生干扰。流程大致如下:

    1. 第一次调用 useEffect 传入的 count 为 0, 于是 setCount(0 + 1);
    2. useEffect 第二个参数 [] 的影响,count 仍然为 0, 所以相当于还是 setCount(0 + 1);

    那如何修正上述问题呢? 方法有两个(方法一为主, 方法二为辅):

    • 方法一: 将 [] 改为 [count]
    • 方法二: 将 setCount(count + 1) 改为 setCount(count => count + 1)。这种方法的思想是修正状态的值而不依赖外面传进的状态。

    不过遇到 setCount(count => count + 1) 的情况就可以考虑使用 useReducer 了。

    何时使用 useReducer

    使用 useState 的地方都能用 useReducer 进行替代。相较 useState, useReducer 有如下优势:

    • useReducerhow(reducer) 和 what(dispatch(action)) 进行抽离; 使用 reducer 逻辑状态进行集中化维护;
    • 相比 useState, useReducer 没有闭包问题;
    • 当状态的一个 state 依赖状态中的另一个 state 时, 这种情况最好使用 useReducer; 可以参考 decoupling-updates-from-actions 中 Dan 列举的 demo。

    处理 useEffect 中的公用函数

    function Demo() {
      const [count, setCount] = useState(0);
    
      function getFetchUrl(query) {
        return `http://demo${query}`
      }
    
      useEffect(() => {
        const url = getFetchUrl('react')
      }, [getFetchUrl]);
    
      useEffect(() => {
        const url = getFetchUrl('redux')
      }, [getFetchUrl]);
    
      return count;
    }
    

    此时 useEffect 中传入的第二个参数 getFetchUrl 相当于每次都是新的, 所以每次都会请求数据, 那除了 [getFetchUrl] 将改为 [] 这种不推荐的写法外,有两种解决方法:

    *. 方法一: 提升 getFetchUrl 的作用域;
    *. 方法二: 使用 useCallback 或者 useMemo 来包裹 getFetchUrl;

    React.memo 修饰一个函数组件, useMemo 修饰一个函数。它们本质都是运用缓存。

    React Hooks 内部是怎么工作的

    为了理解 React Hooks 内部实现原理, 对 useStateuseEffect 进行了简单的实现。

    useState 的简单实现

    使用闭包来实现 useState 的简单逻辑:

    // 这里使用闭包
    const React = (function() {
      let _val
    
      return {
        useState(initialValue) {
          _val = _val || initialValue
    
          function setVal(value) {
            _val = value
          }
    
          return [_val, setVal]
        }
      }
    })()
    

    测试如下:

    function Counter() {
      const [count, setCount] = React.useState(0)
    
      return {
        render: () => console.log(count),
        click: () => setCount(count + 1)
      }
    }
    
    Counter().render() // 0
    Counter().click()  // 模拟点击
    Counter().render() // 1
    

    useEffect 的简单实现

    var React = (function() {
      let _val, _deps
    
      return {
        useState(initialValue) {
          _val = _val || initialValue
    
          function setVal(value) {
            _val = value
          }
    
          return [_val, setVal]
        },
        useEffect(callback, deps) {
          const ifUpdate = !deps
    
          // 判断 Deps 中的依赖是否改变
          const ifDepsChange = _deps ? !_deps.every((r, index) => r === deps[index]) : true
    
          if (ifUpdate || ifDepsChange) {
            callback()
    
            _deps = deps || []
          }
        }
      }
    })()
    

    测试代码如下:

    var {useState, useEffect} = React
    
    function Counter() {
      const [count, setCount] = useState(0)
    
      useEffect(() => {
        console.log('useEffect', count)
      }, [count])
    
      return {
        render: () => console.log('render', count),
        click: () => setCount(count + 1),
        noop: () => setCount(count), // 保持不变, 观察 useEffect 是否被调用
      }
    }
    
    Counter().render() // 'useEffect' 0, 'render', 0
    Counter().noop()
    Counter().render() // 'render', 0
    Counter().click()
    Counter().render() // 'useEffect' 1, 'render', 1
    

    处理多次调用的情形

    为了在 hooks 中能使用多次 useState, useEffect, 将各个 useState, useEffect 的调用存进一个数组中, 在上面基础上进行如下改造:

    const React = (function() {
      const hooks = []
      let currentHook = 0
    
      return {
        render(Component) {
          const component = Component()
          component.render()
          currentHook = 0 // 重置, 这里很关键, 将 hooks 的执行放到 hooks 队列中, 确保每次执行的顺序保持一致。
          return component
        },
        useState(initialValue) {
          hooks[currentHook] = hooks[currentHook] || initialValue
    
          function setVal(value) {
            hooks[currentHook] = value
          }
    
          return [hooks[currentHook++], setVal]
        },
        useEffect(callback, deps) {
          const ifUpdate = !deps
    
          // 判断 Deps 中的依赖是否改变
          const ifDepsChange = hooks[currentHook] ? !hooks[currentHook].every((r, index) => r === deps[index]) : true
    
          if (ifUpdate || ifDepsChange) {
            callback()
    
            hooks[currentHook++] = deps || []
          }
        }
      }
    })()
    

    测试代码如下:

    var {useState, useEffect} = React
    
    function Counter() {
      const [count, setCount] = useState(0)
      const [type, setType] = useState('hi')
    
      useEffect(() => {
        console.log('useEffect', count)
        console.log('type', type)
      }, [count, type])
    
      return {
        render: () => console.log('render', count),
        click: () => setCount(count + 1),
        noop: () => setCount(count), // 保持不变, 观察 useEffect 是否被调用
      }
    }
    
    /* 如下 mock 执行了 useEffect、render; 这里使用 React.render 的原因是为了重置 currentHook 的值 */
    let comp = React.render(Counter) // useEffect 0 type hi render 0
    
    /* 如下 mock 只执行了 render */
    comp.noop()
    comp = React.render(Counter) // render 0
    
    /* 如下 mock 重新执行了 useEffect、render */
    comp.click()
    React.render(Counter) // useEffect 1, render 1
    

    相关资源

  • 相关阅读:
    spring @Transactional 事务注解
    vue 父子组件的方法调用
    spring boot使用TestRestTemplate集成测试 RESTful 接口
    JS实现网站内容的禁止复制和粘贴、另存为
    vue把localhost改成ip地址无法访问—解决方法
    spring mvc spring boot 允许跨域请求 配置类
    JIRA安装过程中链接mysql的问题!
    vue开发中v-for在Eslint的规则检查下出现:Elements in iteration expect to have 'v-bind:key' directives
    Linux进程启动/指令执行方式研究
    反弹Shell原理及检测技术研究
  • 原文地址:https://www.cnblogs.com/MuYunyun/p/10852005.html
Copyright © 2011-2022 走看看