最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 当我们讨论 hooks 时到底在讨论什么

    正文概述 掘金(ES2049)   2020-12-03   481

    在使用 React 开发的这段时间里,我最大的感受就是 “这是 React 最好的时代,也是最坏的时代” !「好」在于 hooks 开启了不一样的开发模式,在思考方式上要求更关注于数据之间的依赖关系,同时书写方式更加简便,总体上提升了开发效率;「坏」在于项目中经常是类组件与函数组件共存,而类组件以类编程思想为主导,开发过程中更关注于整个组件的渲染周期,在维护项目时常常需要在两种思维模式中左右横跳,这还不是最坏的一点。

    某日,老王问我:“你一直在「每周一瞥」搬运 hooks 的文章,你觉得 hooks 有哪些易造成内存泄露的点?” 引发了我的深思(因为我的脑子一片空白)。我们一直在讨论 hooks,到底在讨论什么?虽然社区内关于 hooks 的讨论很多,但更多的是科普 Hooks API 怎么使用,亦或是将其与类组件生命周期、redux 进行对比,而缺少关于 hooks 最佳实践的讨论与共识,我想这才是「最坏」的一点。今天,我们不妨讨论一下 hooks 所带来的变化以及我们如何去拥抱这些变化。

    当我们讨论 hooks 时到底在讨论什么

    React 16.8 发布以来,Hooks 深入人心,带来最大的变化有三点:思维模式的转变,渲染过程中作用域的变化以及数据流的改变。

    思维模式

    从 React 官网可以了解到,Hooks 的设计动机在于简化组件间状态逻辑的复用,支持开发者将关联的逻辑抽象为更小的函数,并降低认知成本,不用去理解 JS Class 中令人窒息的 this。在这样的动机之下,hooks 中弱化了组件生命周期的概念,强化了状态与行为之间的依赖关系,这容易引导我们更多的关注“做什么”,而非“怎么做”[1]。

    假设有这么一个场景:组件 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>
        );
      }
    }
    

    而在应用了 Hooks 的函数组件中,我们思考“做什么”:不同 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>
      );
    }
    

    在这种主导下,开发者在编码过程中的思维模式也应随之改变,需要考虑数据与数据、数据与行为之间的同步关系。这种模式可以更简洁地将相关代码组合到一起,甚至抽象成自定义 hooks,实现逻辑的共享,似乎有了插拔式编程的味道?。

    虽然 Dan Abramov 在自己的博客中提到,从生命周期的角度思考并决定何时执行副作用是在逆势而为[2],但是了解各个 hooks 在组件渲染过程中的执行时机,有助于我们与 React 保持理解的一致性,能够更加准确地专注于“做什么”。 Donavon 以图表形式梳理对比了 hooks 范式与生命周期范式[3],能够帮助我们理解 hooks 在组件中的工作机制。每次组件发生更新时,都会重新调用组件函数,生成新的作用域,这种变化也对我们开发者提出了新的编码要求。

    当我们讨论 hooks 时到底在讨论什么

    作用域

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

    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>
        );
      }
    }
    

    Hooks 中, renderstate 的关系更像闭包与局部变量。每次渲染时,都会生成新的 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 Hooks 给我们提供了解决方案,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 值时,通常需要将 querygetData 方法一起传递给子组件,子组件通过判断 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 应用。

    本文主要从自身体感出发,对比总结了在开发过程中,hooks 所带来的变化以及如何去应对这种变化。理解有误之处,欢迎指正~

    参考

    [1] React is becoming a black box

    [2] A Complete Guide to useEffect

    [3] hook-flow

    [4] The Iceberg of React Hooks


    起源地下载网 » 当我们讨论 hooks 时到底在讨论什么

    常见问题FAQ

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

    发表评论

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

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

    联系作者

    请选择支付方式

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