最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • Hook的那些事

    正文概述 掘金(ZWLoong)   2020-12-10   485

    Hook的那些事

    React16.8发布以来,Hook深入人心,给react开发者带来更多讨论和追捧。社区内从科普Hook Api怎么使用,到讨论出现Hook的动机是什么,到Hook日常使用需要注意坑点,甚至进行横向比较,与class Component比较,是否可替代redux,随着Hook实践丰富后,更多去讨论Hooks带来的变化,去拥抱Hook的变化。

    Hook概念

    Hook是什么

    那么,什么是Hooks?这是我们认识一个新事物的过程。

    换句话说,Hook 是一些可以让你在函数组件里“钩入” React state 及生命周期等特性的函数,在不使用class也可以编程react代码;

    Hook与函数组件

    复习一下, React 的函数组件是这样的:

    const Example = (props) => {
      // 你可以在这使用 Hook
      return <div />;
    }
    

    或者是这样的

    function Example(props) {
      // 你可以在这使用 Hook
      return <div />;
    }
    

    你之前可能把它们叫做“无状态组件”。但现在我们为它们引入了使用 React state 的能力,所以我们更喜欢叫它”函数组件”。
    Hook 在 class 内部是不起作用的。但你可以使用它们来取代 class 。

    Hook内置API
    // 基础 Hook
    useState
    useEffect
    useContext
    
    // 额外的 Hook
    useReducer
    useCallback
    useMemo
    useRef
    useImperativeHandle
    useLayoutEffect
    useDebugValue
    

    更多的Hook相关api

    Hook出现动机

    Hook 解决了我们五年来编写和维护成千上万的组件时遇到的各种各样看起来不相关的问题。无论你正在学习 React,或每天使用,或者更愿尝试另一个和 React 有相似组件模型的框架,你都可能对这些问题似曾相识。

    在组件之间复用状态逻辑很难

    React 没有提供将可复用性行为“附加”到组件的途径(例如,把组件连接到 store)。如果你使用过 React 一段时间,你也许会熟悉一些解决此类问题的方案,比如 render props 和 高阶组件。但是这类方案需要重新组织你的组件结构,这可能会很麻烦,使你的代码难以理解。如果你在 React DevTools 中观察过 React 应用,你会发现由 providers,consumers,高阶组件,render props 等其他抽象层组成的组件会形成“嵌套地狱”。尽管我们可以在 DevTools 过滤掉它们,但这说明了一个更深层次的问题:React 需要为共享状态逻辑提供更好的原生途径。

    你可以使用 Hook 从组件中提取状态逻辑,使得这些逻辑可以单独测试并复用。Hook 使你在无需修改组件结构的情况下复用状态逻辑。 这使得在组件间或社区内共享 Hook 变得更便捷。

    具体将在自定义 Hook 中对此展开更多讨论。

    复杂组件变得难以理解

    我们经常维护一些组件,组件起初很简单,但是逐渐会被状态逻辑和副作用充斥。每个生命周期常常包含一些不相关的逻辑。例如,组件常常在 componentDidMount 和 componentDidUpdate 中获取数据。但是,同一个 componentDidMount 中可能也包含很多其它的逻辑,如设置事件监听,而之后需在 componentWillUnmount 中清除。相互关联且需要对照修改的代码被进行了拆分,而完全不相关的代码却在同一个方法中组合在一起。如此很容易产生 bug,并且导致逻辑不一致。

    在多数情况下,不可能将组件拆分为更小的粒度,因为状态逻辑无处不在。这也给测试带来了一定挑战。同时,这也是很多人将 React 与状态管理库结合使用的原因之一。但是,这往往会引入了很多抽象概念,需要你在不同的文件之间来回切换,使得复用变得更加困难。

    为了解决这个问题,Hook 将组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据),而并非强制按照生命周期划分。你还可以使用 reducer 来管理组件的内部状态,使其更加可预测。

    我们将在使用 Effect Hook 中对此展开更多讨论。

    难以理解的 class

    除了代码复用和代码管理会遇到困难外,我们还发现 class 是学习 React 的一大屏障。你必须去理解 JavaScript 中 this 的工作方式,这与其他语言存在巨大差异。还不能忘记绑定事件处理器。没有稳定的语法提案,这些代码非常冗余。大家可以很好地理解 props,state 和自顶向下的数据流,但对 class 却一筹莫展。即便在有经验的 React 开发者之间,对于函数组件与 class 组件的差异也存在分歧,甚至还要区分两种组件的使用场景。

    另外,React 已经发布五年了,我们希望它能在下一个五年也与时俱进。就像 Svelte,Angular,Glimmer等其它的库展示的那样,组件预编译会带来巨大的潜力。尤其是在它不局限于模板的时候。最近,我们一直在使用 Prepack 来试验 component folding,也取得了初步成效。但是我们发现使用 class 组件会无意中鼓励开发者使用一些让优化措施无效的方案。class 也给目前的工具带来了一些问题。例如,class 不能很好的压缩,并且会使热重载出现不稳定的情况。因此,我们想提供一个使代码更易于优化的 API。

    为了解决这些问题,Hook 使你在非 class 的情况下可以使用更多的 React 特性。 从概念上讲,React 组件一直更像是函数。而 Hook 则拥抱了函数,同时也没有牺牲 React 的精神原则。Hook 提供了问题的解决方案,无需学习复杂的函数式或响应式编程技术

    Hook使用注意点

    Hook使用规则

    Hook 本质就是 JavaScript 函数,但是在使用它时需要遵循两条规则。我们提供了一个 linter 插件来强制执行这些规则:

    • 只在最顶层使用 Hook

    遵守这条规则,你就能确保 Hook 在每一次渲染中都按照同样的顺序被调用。这让 React 能够在多次的 useState 和 useEffect 调用之间保持 hook 状态的正确。(如果你对此感到好奇,我们在下面会有更深入的解释。)

    • 只在 React 函数中调用 Hook

    不要在普通的 JavaScript 函数中调用 Hook。你可以:

    ✅ 在 React 的函数组件中调用 Hook
    ✅ 在自定义 Hook 中调用其他 Hook 遵循此规则,确保组件的状态逻辑在代码中清晰可见。

    Hook带来的变化

    思维模式

    从 React 官网可以了解到,Hooks的设计动机在于简化组件间的状态逻辑的复用,支持开发者将关联的逻辑抽象为更小的函数,降低了认知成本,不用去理解JS Class令人窒息的this。在这样的动机下,hook弱化了组件生命周期的概念,强化了状态与行为之间的依赖关系,这容易引导我们更多的关注“做什么”,而非“怎么做”。
    假设有这么一个场景:组件 Detail 中依赖父级组件传入的 query 参数进行数据请求,那么无论是基于类组件还是 Hooks,我们都需要定义一个异步请求方法 getData。不同的是,在类组件的开发模式中,我们要思考的更倾向于“怎么做”:在组件挂载完成时请求数据,并在组件发生更新时,比较新旧 query 值,必要时重新调用 getData 函数。

    class Detail extends React.Component {
      state = {
        keyword: '',
      }
    
      componentDidMount() {
        this.getData();
      }
    
      getSnapshotBeforeUpdate(prevProps, prevState) {
        if (this.props.query !== prevProps.query) {
          return true;
        }
        return null;
      }
    
      componentDidUpdate(prevProps, prevState, snapshot) {
        if (snapshot) {
          this.getData();
        }
      }
    
      async getData() {
        // 这是一段异步请求数据的代码
        console.log(`数据请求了,参数为:${this.props.query}`);
        this.setState({
          keyword: this.props.query
        })
      }
    
      render() {
        return (
          <div>
            <p>关键词: {this.state.keyword}</p>
          </div>
        );
      }
    }
    

    而在应用了Hook的函数组件中,我们思考“做什么”:不同的query值,展示不同的数据。

    function Detail({
      query
    }) {
      const [keyword, setKeyword] = useState('');
    
      useEffect(() => {
        const getData = async () => {
          console.log(`数据请求了,参数为:${query}`);
          setKeyword(query);
        }
    
        getData();
      }, [query]);
    
      return (
        <div>
          <p>关键词: {keyword}</p>
        </div>
      );
    }
    

    在这种主导下,开发者在编程过程中的思维模式也应随之改变,需要考虑数据与数据、数据与行为的同步关系。这种模式可以更简洁地将相关代码组合到一起,甚至抽象成自定义hook,实现逻辑的共享,视乎有了插拔式编程的味道。
    虽然 Dan Abramov 在自己的博客中提到,从生命周期的角度思考并决定何时执行副作用是在逆势而为[2],但是了解各个 hooks 在组件渲染过程中的执行时机,有助于我们与 React 保持理解的一致性,能够更加准确地专注于“做什么”。 Donavon 以图表形式梳理对比了 hooks 范式与生命周期范式[3],能够帮助我们理解 hooks 在组件中的工作机制。每次组件发生更新时,都会重新调用组件函数,生成新的作用域,这种变化也对我们开发者提出了新的编码要求。 Hook的那些事

    作用域

    在类组件中,组件一旦实例化后,便有了自己的作用域,从创建到销毁,作用域始终不变。因此,在整个组件的生命周期中,每次渲染时内部变量始终都指向同一个引用,我们可以很轻易在每次渲染中通过this.state拿到最新的状态值,也可以使用this.xxx获取到同一个内部变量。

    class Timer extends React.Component {
      state = {
        count: 0,
        interval: null,
      }
    
      componentDidMount() {
        const interval = setInterval(() => {
          this.setState({
            count: this.state.count + 1,
          })
        }, 1000);
    
        this.setState({
          interval
        });
      }
    
      componentDidUnMount() {
        if (this.state.interval) {
          clearInterval(this.state.interval);
        }
      }
    
      render() {
        return (
          <div>
            计数器为:{this.state.count}
          </div>
        );
      }
    }
    

    Hook中,render与state的关系更像是闭包和局部变量。每次渲染时,都会生成新的state变量,React会向其写入当次渲染的状态值,并在当次渲染过程中保持不变。也即每次渲染相互独立,都有自己的状态值。同理,组件内的函数、定时器、副作用等也是独立的,内部访问的也是当次渲染的状态值,因此常常遇到定时器/订阅器内无法读取到最新值的情况。

    function Timer() {
      const [count, setCount] = useState(0);
    
      useEffect(() => {
        const interval = setInterval(() => {
          setCount(count + 1);    // 始终只为 1 
        }, 1000);
    
        return () => {
          clearInterval(interval);
        }
      }, []);
    
      return (
        <div>
          计数器为:{count}
        </div>
      );
    }
    

    如果我们想要拿到最新值,有俩种解决方法:一是利用 setCount 的 lambada 形式,传入一个以上一次的状态值为参数的函数;二是借助 useRef 钩子,在其 current 属性中存储最新的值。

    function Timer() {
      const [count, setCount] = useState(0);
    
      useEffect(() => {
        const interval = setInterval(() => {
          setCount(c => c + 1);
        }, 1000);
    
        return () => {
          clearInterval(interval);
        }
      }, []);
    
      return (
        <div>
          计数器为:{count}
        </div>
      );
    }
    

    在 hook-flow 的图中,我们可以了解到当父组件发生重新渲染时,其所有(状态、局部变量等)都是新的。一旦子组件依赖于父组件的某一个对象变量,那么无论对象是否发生变化,子组件拿到的都是新的对象,从而使子组件对应的 diff 失效,依旧会重新执行该部分逻辑。在下面的例子中,我们的副作用依赖项中包含了父组件传入的对象参数,每次父组件发生更新时,都会触发数据请求。

    function Info({
      style,
    }) {
      console.log('Info 发生渲染');
    
      useEffect(() => {
        console.log('重新加载数据'); // 每次发生重新渲染时,都会重新加载数据
      }, [style]);
    
      return (
        <p style={style}>
          这是 Info 里的文字
        </p>
      );
    }
    
    function Page() {
      console.log('Page 发生渲染');
    
      const [count, setCount] = useState(0);
      const style = { color: 'red' };
    
      // 计数器 +1 时,引发 Page 的重新渲染,进而引发 Info 的重新渲染
      return (
        <div>
          <h4>计数值为:{count}</h4>
          <button onClick={() => setCount(count + 1)}> +1 </button>
          <Info style={style} />
        </div>
      );
    }
    

    React Hook给我们提供了解决方案,useMemo允许我们缓存传入的对象,仅当依赖项发生变化时,才重新计算并更新相应的对象。

    function Page() {
      console.log('Page 发生渲染');
    
      const [color] = useState('red');
      const [count, setCount] = useState(0);
      const style = useMemo(() => ({ color }), [color]); // 只有 color 发生实质性改变时,style 才会变化
    
      // 计数器 +1 时,引发 Page 的重新渲染,进而引发 Info 的重新渲染
      // 但是由于 style 缓存了,因此不会触发 Info 内的数据重新加载
      return (
        <div>
          <h4>计数值为:{count}</h4>
          <button onClick={() => setCount(count + 1)}> +1 </button>
          <Info style={style} />
        </div>
      );
    }
    

    数据流

    React Hooks 在数据流上带来的变化有两点:一是支持更友好的使用 context 进行状态管理,避免层级过多时向中间层承载无关参数;二是允许函数参与到数据流中,避免向下层组件传入多余的参数。
    useContext 作为 hooks 的核心模块之一,可以获取到传入 context 的当前值,以此达到跨层通信的目的。React 官网有着详细的介绍,需要关注的是一旦 context 值发生改变,所有使用了该 context 的组件都会重新渲染。为了避免无关的组件重绘,我们需要合理的构建 context ,比如从第一节提到的新思维模式出发,按状态的相关度组织 context,将相关状态存储在同一个 context 中。
    在过去,如果父子组件用到同一个数据请求方法 getData ,而该方法又依赖于上层传入的 query 值时,通常需要将 query 和 getData 方法一起传递给子组件,子组件通过判断 query 值来决定是否重新执行 getData。

    class Parent extends React.Component {
       state = {
        query: 'keyword',
      }
    
      getData() {
        const url = `https://mocks.alibaba-inc.com/mock/fO87jdfKqX/demo/queryData.json?query=${this.state.query}`;
        // 请求数据...
        console.log(`请求路径为:${url}`);
      }
    
      render() {
        return (
          // 传递了一个子组件不渲染的 query 值
          <Child getData={this.getData} query={this.state.query} />
        );
      }
    }
    
    class Child extends React.Component {
      componentDidMount() {
        this.props.getData();
      }
    
      componentDidUpdate(prevProps) {
        // if (prevProps.getData !== this.props.getData) { // 该条件始终为 true
        //   this.props.getData();
        // }
        if (prevProps.query !== this.props.query) { // 只能借助 query 值来做判断
          this.props.getData();
        }
      }
    
      render() {
        return (
          // ...
        );
      }
    }
    

    在 React Hooks 中 useCallback 支持我们缓存某一函数,当且仅当依赖项发生变化时,才更新该函数。这使得我们可以在子组件中配合 useEffect ,实现按需加载。通过 hooks 的配合,使得函数不再仅仅是一个方法,而是可以作为一个值参与到应用的数据流中。

    function Parent() {
      const [count, setCount] = useState(0);
      const [query, setQuery] = useState('keyword');
    
      const getData = useCallback(() => {
        const url = `https://mocks.alibaba-inc.com/mock/fO87jdfKqX/demo/queryData.json?query=${query}`;
        // 请求数据...
        console.log(`请求路径为:${url}`);
      }, [query]);  // 当且仅当 query 改变时 getData 才更新
    
      // 计数值的变化并不会引起 Child 重新请求数据
      return (
        <>
          <h4>计数值为:{count}</h4>
          <button onClick={() => setCount(count + 1)}> +1 </button>
          <input onChange={(e) => {setQuery(e.target.value)}} />
          <Child getData={getData} />
        </>
      );
    }
    
    function Child({
      getData
    }) {
      useEffect(() => {
        getData();
      }, [getData]);	// 函数可以作为依赖项参与到数据流中
    
      return (
        // ...
      );
    }
    

    代码精简

    总结

    回到最初的问题:“ hooks 有哪些易造成内存泄露的点?”,我理解造成内存泄露风险的在于 hooks 所带来的作用域的变化。由于每次渲染都是独立的,一旦有副作用引用了局部变量,并且未在组件销毁时及时释放,那么就极易造成内存泄露。关于如何更好的使用 hooks, Sandro Dolidze 在博客中列了一个 checkList[4],我觉得是个不错的建议,可以帮助我们写出正确的 hooks 应用。


    起源地下载网 » Hook的那些事

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元